Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/lucky-horses-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@opennextjs/aws": patch
---

add: s3 lite override for loading images in the image optimization server

`s3-lite` override for image loading. Uses `aws4fetch` to get the objects from your s3 bucket. This will make the image optimization server work without the aws s3 sdk. Important to note that a new environment variable is required on the image optimization server for this override to work: `BUCKET_REGION`.

```ts
import type { OpenNextConfig } from '@opennextjs/aws/types/open-next';
const config = {
default: {},
imageOptimization: {
loader: 's3-lite', // make sure you have the required env variables defined
},
} satisfies OpenNextConfig;

export default config;
```
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export async function createImageOptimizationBundle(
"const require = topLevelCreateRequire(import.meta.url);",
"import bannerUrl from 'url';",
"const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));",
"globalThis.internalFetch = fetch;",
].join("\n"),
},
},
Expand Down
67 changes: 67 additions & 0 deletions packages/open-next/src/overrides/imageLoader/s3-lite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Readable } from "node:stream";
import type { ReadableStream } from "node:stream/web";
import { AwsClient } from "aws4fetch";

import type { ImageLoader } from "types/overrides";
import { FatalError } from "utils/error";
import { customFetchClient } from "utils/fetch";

let awsClient: AwsClient | null = null;

const getAwsClient = () => {
if (awsClient) {
return awsClient;
}
awsClient = new AwsClient({
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
sessionToken: process.env.AWS_SESSION_TOKEN,
region: process.env.BUCKET_REGION,
});
return awsClient;
};

const { BUCKET_NAME, BUCKET_KEY_PREFIX, BUCKET_REGION } = process.env;

function ensureEnvExists() {
if (!(BUCKET_NAME || BUCKET_REGION || BUCKET_KEY_PREFIX)) {
throw new Error("Bucket name, region and key prefix must be defined!");
}
}

const awsFetch = async (key: string, options: RequestInit) => {
const client = getAwsClient();
const url = `https://${BUCKET_NAME}.s3.${BUCKET_REGION}.amazonaws.com/${key}`;
return customFetchClient(client)(url, options);
};

const s3Loader: ImageLoader = {
name: "s3",
load: async (key: string) => {
ensureEnvExists();
const keyPrefix = BUCKET_KEY_PREFIX?.replace(/^\/|\/$/g, "");
const response = await awsFetch(
keyPrefix
? `${keyPrefix}/${key.replace(/^\//, "")}`
: key.replace(/^\//, ""),
{
method: "GET",
},
);

if (!response.body) {
throw new FatalError("No body in aws4fetch s3 response");
}

// We need to cast it else there will be a TypeError: o.pipe is not a function
const body = Readable.fromWeb(response.body as ReadableStream<Uint8Array>);

return {
body: body,
contentType: response.headers.get("content-type") ?? undefined,
cacheControl: response.headers.get("cache-control") ?? undefined,
};
},
};

export default s3Loader;
7 changes: 6 additions & 1 deletion packages/open-next/src/types/open-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,12 @@ export type IncludedTagCache =
| "fs-dev"
| "dummy";

export type IncludedImageLoader = "s3" | "host" | "fs-dev" | "dummy";
export type IncludedImageLoader =
| "s3"
| "s3-lite"
| "host"
| "fs-dev"
| "dummy";

export type IncludedOriginResolver = "pattern-env" | "dummy";

Expand Down
Loading