@opennextjs/cloudflare 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/cli/args.d.ts +2 -0
  2. package/dist/cli/args.js +37 -19
  3. package/dist/cli/build/build.d.ts +2 -2
  4. package/dist/cli/build/build.js +3 -0
  5. package/dist/cli/build/bundle-server.d.ts +1 -1
  6. package/dist/cli/build/bundle-server.js +4 -7
  7. package/dist/cli/build/open-next/compile-images.d.ts +5 -0
  8. package/dist/cli/build/open-next/compile-images.js +29 -0
  9. package/dist/cli/build/open-next/createServerBundle.js +0 -1
  10. package/dist/cli/build/patches/plugins/next-server.d.ts +0 -2
  11. package/dist/cli/build/patches/plugins/next-server.js +0 -19
  12. package/dist/cli/build/{patches/investigated → utils}/copy-package-cli-files.js +1 -1
  13. package/dist/cli/build/utils/index.d.ts +1 -1
  14. package/dist/cli/build/utils/index.js +1 -1
  15. package/dist/cli/build/utils/test-patch.d.ts +9 -0
  16. package/dist/cli/build/utils/test-patch.js +14 -0
  17. package/dist/cli/build/utils/workerd.js +3 -1
  18. package/dist/cli/templates/images.d.ts +24 -0
  19. package/dist/cli/templates/images.js +82 -0
  20. package/dist/cli/templates/worker.js +3 -3
  21. package/package.json +5 -2
  22. package/dist/api/durable-objects/bucket-cache-purge.spec.d.ts +0 -1
  23. package/dist/api/durable-objects/bucket-cache-purge.spec.js +0 -121
  24. package/dist/api/durable-objects/queue.spec.d.ts +0 -1
  25. package/dist/api/durable-objects/queue.spec.js +0 -287
  26. package/dist/api/durable-objects/sharded-tag-cache.spec.d.ts +0 -1
  27. package/dist/api/durable-objects/sharded-tag-cache.spec.js +0 -37
  28. package/dist/api/overrides/queue/memory-queue.spec.d.ts +0 -1
  29. package/dist/api/overrides/queue/memory-queue.spec.js +0 -76
  30. package/dist/api/overrides/queue/queue-cache.spec.d.ts +0 -1
  31. package/dist/api/overrides/queue/queue-cache.spec.js +0 -92
  32. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.d.ts +0 -1
  33. package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +0 -413
  34. package/dist/api/overrides/tag-cache/tag-cache-filter.spec.d.ts +0 -1
  35. package/dist/api/overrides/tag-cache/tag-cache-filter.spec.js +0 -97
  36. package/dist/cli/build/patches/ast/patch-vercel-og-library.spec.d.ts +0 -1
  37. package/dist/cli/build/patches/ast/patch-vercel-og-library.spec.js +0 -50
  38. package/dist/cli/build/patches/ast/vercel-og.spec.d.ts +0 -1
  39. package/dist/cli/build/patches/ast/vercel-og.spec.js +0 -22
  40. package/dist/cli/build/patches/ast/webpack-runtime.spec.d.ts +0 -1
  41. package/dist/cli/build/patches/ast/webpack-runtime.spec.js +0 -102
  42. package/dist/cli/build/patches/index.d.ts +0 -1
  43. package/dist/cli/build/patches/index.js +0 -1
  44. package/dist/cli/build/patches/investigated/index.d.ts +0 -2
  45. package/dist/cli/build/patches/investigated/index.js +0 -2
  46. package/dist/cli/build/patches/investigated/patch-require.d.ts +0 -4
  47. package/dist/cli/build/patches/investigated/patch-require.js +0 -6
  48. package/dist/cli/build/patches/plugins/instrumentation.spec.d.ts +0 -1
  49. package/dist/cli/build/patches/plugins/instrumentation.spec.js +0 -91
  50. package/dist/cli/build/patches/plugins/next-server.spec.d.ts +0 -1
  51. package/dist/cli/build/patches/plugins/next-server.spec.js +0 -429
  52. package/dist/cli/build/patches/plugins/patch-depd-deprecations.spec.d.ts +0 -1
  53. package/dist/cli/build/patches/plugins/patch-depd-deprecations.spec.js +0 -29
  54. package/dist/cli/build/patches/plugins/res-revalidate.spec.d.ts +0 -1
  55. package/dist/cli/build/patches/plugins/res-revalidate.spec.js +0 -141
  56. package/dist/cli/build/patches/plugins/use-cache.spec.d.ts +0 -1
  57. package/dist/cli/build/patches/plugins/use-cache.spec.js +0 -156
  58. package/dist/cli/build/utils/apply-patches.d.ts +0 -12
  59. package/dist/cli/build/utils/apply-patches.js +0 -22
  60. package/dist/cli/build/utils/extract-project-env-vars.spec.d.ts +0 -1
  61. package/dist/cli/build/utils/extract-project-env-vars.spec.js +0 -67
  62. package/dist/cli/build/utils/workerd.spec.d.ts +0 -1
  63. package/dist/cli/build/utils/workerd.spec.js +0 -188
  64. package/dist/cli/commands/populate-cache.spec.d.ts +0 -1
  65. package/dist/cli/commands/populate-cache.spec.js +0 -61
  66. /package/dist/cli/build/{patches/investigated → utils}/copy-package-cli-files.d.ts +0 -0
@@ -1,3 +1,4 @@
1
+ import type { ParseArgsConfig } from "node:util";
1
2
  import type { WranglerTarget } from "./utils/run-wrangler.js";
2
3
  export type Arguments = ({
3
4
  command: "build";
@@ -17,3 +18,4 @@ export type Arguments = ({
17
18
  outputDir?: string;
18
19
  };
19
20
  export declare function getArgs(): Arguments;
21
+ export declare function getPassthroughArgs<T extends ParseArgsConfig>(args: string[], { options }: T): string[];
package/dist/cli/args.js CHANGED
@@ -2,28 +2,30 @@ import { mkdirSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import { parseArgs } from "node:util";
4
4
  import { getWranglerEnvironmentFlag, isWranglerTarget } from "./utils/run-wrangler.js";
5
+ // Config for parsing CLI arguments
6
+ const config = {
7
+ allowPositionals: true,
8
+ strict: false,
9
+ options: {
10
+ skipBuild: { type: "boolean", short: "s", default: false },
11
+ output: { type: "string", short: "o" },
12
+ noMinify: { type: "boolean", default: false },
13
+ skipWranglerConfigCheck: { type: "boolean", default: false },
14
+ cacheChunkSize: { type: "string" },
15
+ },
16
+ };
5
17
  export function getArgs() {
6
- const { positionals, values } = parseArgs({
7
- options: {
8
- skipBuild: { type: "boolean", short: "s", default: false },
9
- output: { type: "string", short: "o" },
10
- noMinify: { type: "boolean", default: false },
11
- skipWranglerConfigCheck: { type: "boolean", default: false },
12
- cacheChunkSize: { type: "string" },
13
- },
14
- allowPositionals: true,
15
- });
16
- const outputDir = values.output ? resolve(values.output) : undefined;
18
+ const { positionals, values } = parseArgs(config);
19
+ const outputDir = typeof values.output === "string" ? resolve(values.output) : undefined;
17
20
  if (outputDir)
18
21
  assertDirArg(outputDir, "output", true);
19
- const passthroughArgs = getPassthroughArgs();
20
22
  switch (positionals[0]) {
21
23
  case "build":
22
24
  return {
23
25
  command: "build",
24
26
  outputDir,
25
- skipNextBuild: values.skipBuild || ["1", "true", "yes"].includes(String(process.env.SKIP_NEXT_APP_BUILD)),
26
- skipWranglerConfigCheck: values.skipWranglerConfigCheck ||
27
+ skipNextBuild: !!values.skipBuild || ["1", "true", "yes"].includes(String(process.env.SKIP_NEXT_APP_BUILD)),
28
+ skipWranglerConfigCheck: !!values.skipWranglerConfigCheck ||
27
29
  ["1", "true", "yes"].includes(String(process.env.SKIP_WRANGLER_CONFIG_CHECK)),
28
30
  minify: !values.noMinify,
29
31
  };
@@ -33,7 +35,7 @@ export function getArgs() {
33
35
  return {
34
36
  command: positionals[0],
35
37
  outputDir,
36
- passthroughArgs,
38
+ passthroughArgs: getPassthroughArgs(process.argv, config),
37
39
  ...(values.cacheChunkSize && { cacheChunkSize: Number(values.cacheChunkSize) }),
38
40
  };
39
41
  case "populateCache":
@@ -44,16 +46,32 @@ export function getArgs() {
44
46
  command: "populateCache",
45
47
  outputDir,
46
48
  target: positionals[1],
47
- environment: getWranglerEnvironmentFlag(passthroughArgs),
49
+ environment: getWranglerEnvironmentFlag(process.argv),
48
50
  ...(values.cacheChunkSize && { cacheChunkSize: Number(values.cacheChunkSize) }),
49
51
  };
50
52
  default:
51
53
  throw new Error("Error: invalid command, expected 'build' | 'preview' | 'deploy' | 'upload' | 'populateCache'");
52
54
  }
53
55
  }
54
- function getPassthroughArgs() {
55
- const passthroughPos = process.argv.indexOf("--");
56
- return passthroughPos === -1 ? [] : process.argv.slice(passthroughPos + 1);
56
+ export function getPassthroughArgs(args, { options = {} }) {
57
+ const passthroughArgs = [];
58
+ for (let i = 0; i < args.length; i++) {
59
+ if (args[i] === "--") {
60
+ passthroughArgs.push(...args.slice(i + 1));
61
+ return passthroughArgs;
62
+ }
63
+ // look for `--arg(=value)`, `-arg(=value)`
64
+ const [, name] = /^--?(\w[\w-]*)(=.+)?$/.exec(args[i]) ?? [];
65
+ if (name && !(name in options)) {
66
+ passthroughArgs.push(args[i]);
67
+ // Array args can have multiple values
68
+ // ref https://github.com/yargs/yargs-parser/blob/main/README.md#greedy-arrays
69
+ while (i < args.length - 1 && !args[i + 1]?.startsWith("-")) {
70
+ passthroughArgs.push(args[++i]);
71
+ }
72
+ }
73
+ }
74
+ return passthroughArgs;
57
75
  }
58
76
  function assertDirArg(path, argName, make) {
59
77
  let dirStats;
@@ -1,4 +1,4 @@
1
- import { BuildOptions } from "@opennextjs/aws/build/helper.js";
1
+ import * as buildHelper from "@opennextjs/aws/build/helper.js";
2
2
  import { OpenNextConfig } from "../../api/config.js";
3
3
  import type { ProjectOptions } from "../project-options.js";
4
4
  /**
@@ -10,4 +10,4 @@ import type { ProjectOptions } from "../project-options.js";
10
10
  * @param config The OpenNext config
11
11
  * @param projectOpts The options for the project
12
12
  */
13
- export declare function build(options: BuildOptions, config: OpenNextConfig, projectOpts: ProjectOptions): Promise<void>;
13
+ export declare function build(options: buildHelper.BuildOptions, config: OpenNextConfig, projectOpts: ProjectOptions): Promise<void>;
@@ -8,6 +8,7 @@ import logger from "@opennextjs/aws/logger.js";
8
8
  import { bundleServer } from "./bundle-server.js";
9
9
  import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js";
10
10
  import { compileEnvFiles } from "./open-next/compile-env-files.js";
11
+ import { compileImages } from "./open-next/compile-images.js";
11
12
  import { compileInit } from "./open-next/compile-init.js";
12
13
  import { compileDurableObjects } from "./open-next/compileDurableObjects.js";
13
14
  import { createServerBundle } from "./open-next/createServerBundle.js";
@@ -52,6 +53,8 @@ export async function build(options, config, projectOpts) {
52
53
  compileEnvFiles(options);
53
54
  // Compile workerd init
54
55
  compileInit(options);
56
+ // Compile image helpers
57
+ compileImages(options);
55
58
  // Compile middleware
56
59
  await createMiddleware(options, { forceOnlyBuildOnce: true });
57
60
  createStaticAssets(options, { useBasePath: true });
@@ -4,7 +4,7 @@ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
4
4
  */
5
5
  export declare function bundleServer(buildOpts: BuildOptions): Promise<void>;
6
6
  /**
7
- * This function applies patches required for the code to run on workers.
7
+ * This function apply updates to the bundled code.
8
8
  */
9
9
  export declare function updateWorkerBundledCode(workerOutputFile: string): Promise<void>;
10
10
  /**
@@ -8,7 +8,6 @@ import { build } from "esbuild";
8
8
  import { getOpenNextConfig } from "../../api/config.js";
9
9
  import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
10
10
  import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js";
11
- import * as patches from "./patches/index.js";
12
11
  import { inlineDynamicRequires } from "./patches/plugins/dynamic-requires.js";
13
12
  import { inlineFindDir } from "./patches/plugins/find-dir.js";
14
13
  import { patchInstrumentation } from "./patches/plugins/instrumentation.js";
@@ -21,7 +20,7 @@ import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations
21
20
  import { fixRequire } from "./patches/plugins/require.js";
22
21
  import { shimRequireHook } from "./patches/plugins/require-hook.js";
23
22
  import { setWranglerExternal } from "./patches/plugins/wrangler-external.js";
24
- import { needsExperimentalReact, normalizePath, patchCodeWithValidations } from "./utils/index.js";
23
+ import { copyPackageCliFiles, needsExperimentalReact, normalizePath } from "./utils/index.js";
25
24
  /** The dist directory of the Cloudflare adapter package */
26
25
  const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
27
26
  /**
@@ -41,7 +40,7 @@ const optionalDependencies = [
41
40
  * Bundle the Open Next server.
42
41
  */
43
42
  export async function bundleServer(buildOpts) {
44
- patches.copyPackageCliFiles(packageDistDir, buildOpts);
43
+ copyPackageCliFiles(packageDistDir, buildOpts);
45
44
  const { appPath, outputDir, monorepoRoot, debug } = buildOpts;
46
45
  const baseManifestPath = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next");
47
46
  const serverFiles = path.join(baseManifestPath, "required-server-files.json");
@@ -142,13 +141,11 @@ export async function bundleServer(buildOpts) {
142
141
  console.log(`\x1b[35mWorker saved in \`${path.relative(buildOpts.appPath, getOutputWorkerPath(buildOpts))}\` 🚀\n\x1b[0m`);
143
142
  }
144
143
  /**
145
- * This function applies patches required for the code to run on workers.
144
+ * This function apply updates to the bundled code.
146
145
  */
147
146
  export async function updateWorkerBundledCode(workerOutputFile) {
148
147
  const code = await readFile(workerOutputFile, "utf8");
149
- const patchedCode = await patchCodeWithValidations(code, [
150
- ["require", patches.patchRequire, { isOptional: true }],
151
- ]);
148
+ const patchedCode = code.replace(/__require\d?\(/g, "require(").replace(/__require\d?\./g, "require.");
152
149
  await writeFile(workerOutputFile, patchedCode);
153
150
  }
154
151
  /**
@@ -0,0 +1,5 @@
1
+ import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
2
+ /**
3
+ * Compiles the initialization code for the workerd runtime
4
+ */
5
+ export declare function compileImages(options: BuildOptions): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { build } from "esbuild";
5
+ /**
6
+ * Compiles the initialization code for the workerd runtime
7
+ */
8
+ export async function compileImages(options) {
9
+ const currentDir = path.join(path.dirname(fileURLToPath(import.meta.url)));
10
+ const templatesDir = path.join(currentDir, "../../templates");
11
+ const imagesPath = path.join(templatesDir, "images.js");
12
+ const imagesManifestPath = path.join(options.appBuildOutputPath, ".next/images-manifest.json");
13
+ const imagesManifest = fs.existsSync(imagesManifestPath)
14
+ ? JSON.parse(fs.readFileSync(imagesManifestPath, { encoding: "utf-8" }))
15
+ : {};
16
+ await build({
17
+ entryPoints: [imagesPath],
18
+ outdir: path.join(options.outputDir, "cloudflare"),
19
+ bundle: false,
20
+ minify: false,
21
+ format: "esm",
22
+ target: "esnext",
23
+ platform: "node",
24
+ define: {
25
+ __IMAGES_REMOTE_PATTERNS__: JSON.stringify(imagesManifest?.images?.remotePatterns ?? []),
26
+ __IMAGES_LOCAL_PATTERNS__: JSON.stringify(imagesManifest?.images?.localPatterns ?? []),
27
+ },
28
+ });
29
+ }
@@ -138,7 +138,6 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
138
138
  awsPatches.patchNextServer,
139
139
  awsPatches.patchEnvVars,
140
140
  awsPatches.patchBackgroundRevalidation,
141
- awsPatches.patchDropBabel,
142
141
  // Cloudflare specific patches
143
142
  patchResRevalidate,
144
143
  patchUseCacheIO,
@@ -4,14 +4,12 @@
4
4
  * Note: we will probably need to revisit the patches when the Next adapter API lands
5
5
  *
6
6
  * - Inline `getBuildId` as it relies on `readFileSync` that is not supported by workerd
7
- * - Inline the middleware manifest
8
7
  * - Override the cache and composable cache handlers
9
8
  */
10
9
  import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
11
10
  import type { ContentUpdater, Plugin } from "@opennextjs/aws/plugins/content-updater.js";
12
11
  export declare function patchNextServer(updater: ContentUpdater, buildOpts: BuildOptions): Plugin;
13
12
  export declare const buildIdRule = "\nrule:\n pattern:\n selector: method_definition\n context: \"class { getBuildId($$$PARAMS) { $$$_ } }\"\nfix: |-\n getBuildId($$$PARAMS) {\n return process.env.NEXT_BUILD_ID;\n }\n";
14
- export declare function createMiddlewareManifestRule(manifest: unknown): string;
15
13
  /**
16
14
  * The cache handler used by Next.js is normally defined in the config file as a path. At runtime,
17
15
  * Next.js would then do a dynamic require on a transformed version of the path to retrieve the
@@ -4,10 +4,8 @@
4
4
  * Note: we will probably need to revisit the patches when the Next adapter API lands
5
5
  *
6
6
  * - Inline `getBuildId` as it relies on `readFileSync` that is not supported by workerd
7
- * - Inline the middleware manifest
8
7
  * - Override the cache and composable cache handlers
9
8
  */
10
- import { existsSync, readFileSync } from "node:fs";
11
9
  import path from "node:path";
12
10
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
13
11
  import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
@@ -24,11 +22,6 @@ export function patchNextServer(updater, buildOpts) {
24
22
  callback: async ({ contents }) => {
25
23
  const { outputDir } = buildOpts;
26
24
  contents = patchCode(contents, buildIdRule);
27
- const manifestPath = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server/middleware-manifest.json");
28
- const manifest = existsSync(manifestPath)
29
- ? JSON.parse(await readFileSync(manifestPath, "utf-8"))
30
- : {};
31
- contents = patchCode(contents, createMiddlewareManifestRule(manifest));
32
25
  const outputPath = path.join(outputDir, "server-functions/default");
33
26
  const cacheHandler = path.join(outputPath, getPackagePath(buildOpts), "cache.cjs");
34
27
  contents = patchCode(contents, createCacheHandlerRule(cacheHandler));
@@ -50,18 +43,6 @@ fix: |-
50
43
  return process.env.NEXT_BUILD_ID;
51
44
  }
52
45
  `;
53
- export function createMiddlewareManifestRule(manifest) {
54
- return `
55
- rule:
56
- pattern:
57
- selector: method_definition
58
- context: "class { getMiddlewareManifest($$$PARAMS) { $$$_ } }"
59
- fix: |-
60
- getMiddlewareManifest($$$PARAMS) {
61
- return ${JSON.stringify(manifest)};
62
- }
63
- `;
64
- }
65
46
  /**
66
47
  * The cache handler used by Next.js is normally defined in the config file as a path. At runtime,
67
48
  * Next.js would then do a dynamic require on a transformed version of the path to retrieve the
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { getOutputWorkerPath } from "../../bundle-server.js";
3
+ import { getOutputWorkerPath } from "../bundle-server.js";
4
4
  /**
5
5
  * Copies
6
6
  * - the template files present in the cloudflare adapter package to `.open-next/cloudflare-templates`
@@ -1,4 +1,4 @@
1
- export * from "./apply-patches.js";
1
+ export * from "./copy-package-cli-files.js";
2
2
  export * from "./create-config-files.js";
3
3
  export * from "./ensure-cf-config.js";
4
4
  export * from "./extract-project-env-vars.js";
@@ -1,4 +1,4 @@
1
- export * from "./apply-patches.js";
1
+ export * from "./copy-package-cli-files.js";
2
2
  export * from "./create-config-files.js";
3
3
  export * from "./ensure-cf-config.js";
4
4
  export * from "./extract-project-env-vars.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Compute the diff resulting of applying the `rule` to `src`.
3
+ *
4
+ * @param filename Filename used in the patch output
5
+ * @param src Content of the source code
6
+ * @param rule ASTgrep rule
7
+ * @returns diff in unified diff format
8
+ */
9
+ export declare function computePatchDiff(filename: string, src: string, rule: string): string;
@@ -0,0 +1,14 @@
1
+ import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
2
+ import { createPatch } from "diff";
3
+ /**
4
+ * Compute the diff resulting of applying the `rule` to `src`.
5
+ *
6
+ * @param filename Filename used in the patch output
7
+ * @param src Content of the source code
8
+ * @param rule ASTgrep rule
9
+ * @returns diff in unified diff format
10
+ */
11
+ export function computePatchDiff(filename, src, rule) {
12
+ const dst = patchCode(src, rule);
13
+ return createPatch(filename, src, dst);
14
+ }
@@ -60,7 +60,9 @@ export async function copyWorkerdPackages(options, nodePackages) {
60
60
  const isNodeModuleRegex = getCrossPlatformPathRegex(`.*/node_modules/(?<pkg>.*)`, { escape: false });
61
61
  // Copy full external packages when they use "workerd" build condition
62
62
  const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
63
- const externalPackages = nextConfig.serverExternalPackages ?? [];
63
+ const externalPackages =
64
+ // @ts-expect-error In Next 14 its under experimental.serverComponentsExternalPackages
65
+ nextConfig.serverExternalPackages ?? nextConfig.experimental.serverComponentsExternalPackages ?? [];
64
66
  for (const [src, dst] of nodePackages.entries()) {
65
67
  try {
66
68
  const pkgJson = JSON.parse(await fs.readFile(path.join(src, "package.json"), "utf8"));
@@ -0,0 +1,24 @@
1
+ export type RemotePattern = {
2
+ protocol?: "http" | "https";
3
+ hostname: string;
4
+ port?: string;
5
+ pathname: string;
6
+ search?: string;
7
+ };
8
+ export type LocalPattern = {
9
+ pathname: string;
10
+ search?: string;
11
+ };
12
+ /**
13
+ * Fetches an images.
14
+ *
15
+ * Local images (starting with a '/' as fetched using the passed fetcher).
16
+ * Remote images should match the configured remote patterns or a 404 response is returned.
17
+ */
18
+ export declare function fetchImage(fetcher: Fetcher | undefined, imageUrl: string): Response | Promise<Response> | undefined;
19
+ export declare function matchRemotePattern(pattern: RemotePattern, url: URL): boolean;
20
+ export declare function matchLocalPattern(pattern: LocalPattern, url: URL): boolean;
21
+ declare global {
22
+ var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
23
+ var __IMAGES_LOCAL_PATTERNS__: LocalPattern[];
24
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Fetches an images.
3
+ *
4
+ * Local images (starting with a '/' as fetched using the passed fetcher).
5
+ * Remote images should match the configured remote patterns or a 404 response is returned.
6
+ */
7
+ export function fetchImage(fetcher, imageUrl) {
8
+ // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208
9
+ if (!imageUrl || imageUrl.length > 3072 || imageUrl.startsWith("//")) {
10
+ return getUrlErrorResponse();
11
+ }
12
+ // Local
13
+ if (imageUrl.startsWith("/")) {
14
+ let pathname;
15
+ let url;
16
+ try {
17
+ // We only need pathname and search
18
+ url = new URL(imageUrl, "http://n");
19
+ pathname = decodeURIComponent(url.pathname);
20
+ }
21
+ catch {
22
+ return getUrlErrorResponse();
23
+ }
24
+ if (/\/_next\/image($|\/)/.test(pathname)) {
25
+ return getUrlErrorResponse();
26
+ }
27
+ // If localPatterns are not defined all local images are allowed.
28
+ if (__IMAGES_LOCAL_PATTERNS__.length > 0 &&
29
+ !__IMAGES_LOCAL_PATTERNS__.some((p) => matchLocalPattern(p, url))) {
30
+ return getUrlErrorResponse();
31
+ }
32
+ return fetcher?.fetch(`http://assets.local${imageUrl}`);
33
+ }
34
+ // Remote
35
+ let url;
36
+ try {
37
+ url = new URL(imageUrl);
38
+ }
39
+ catch {
40
+ return getUrlErrorResponse();
41
+ }
42
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
43
+ return getUrlErrorResponse();
44
+ }
45
+ // The remotePatterns is used to allow images from specific remote external paths and block all others.
46
+ if (!__IMAGES_REMOTE_PATTERNS__.some((p) => matchRemotePattern(p, url))) {
47
+ return getUrlErrorResponse();
48
+ }
49
+ return fetch(imageUrl, { cf: { cacheEverything: true } });
50
+ }
51
+ export function matchRemotePattern(pattern, url) {
52
+ // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts
53
+ if (pattern.protocol !== undefined &&
54
+ pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "")) {
55
+ return false;
56
+ }
57
+ if (pattern.port !== undefined && pattern.port !== url.port) {
58
+ return false;
59
+ }
60
+ if (pattern.hostname === undefined || !new RegExp(pattern.hostname).test(url.hostname)) {
61
+ return false;
62
+ }
63
+ if (pattern.search !== undefined && pattern.search !== url.search) {
64
+ return false;
65
+ }
66
+ // Should be the same as writeImagesManifest()
67
+ return new RegExp(pattern.pathname).test(url.pathname);
68
+ }
69
+ export function matchLocalPattern(pattern, url) {
70
+ // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-local-pattern.ts
71
+ if (pattern.search !== undefined && pattern.search !== url.search) {
72
+ return false;
73
+ }
74
+ return new RegExp(pattern.pathname).test(url.pathname);
75
+ }
76
+ /**
77
+ * @returns same error as Next.js when the url query parameter is not accepted.
78
+ */
79
+ function getUrlErrorResponse() {
80
+ return new Response(`"url" parameter is not allowed`, { status: 400 });
81
+ }
82
+ /* eslint-enable no-var */
@@ -1,4 +1,6 @@
1
1
  //@ts-expect-error: Will be resolved by wrangler build
2
+ import { fetchImage } from "./cloudflare/images.js";
3
+ //@ts-expect-error: Will be resolved by wrangler build
2
4
  import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
3
5
  // @ts-expect-error: Will be resolved by wrangler build
4
6
  import { handler as middlewareHandler } from "./middleware/handler.mjs";
@@ -27,9 +29,7 @@ export default {
27
29
  // Fallback for the Next default image loader.
28
30
  if (url.pathname === `${globalThis.__NEXT_BASE_PATH__}/_next/image`) {
29
31
  const imageUrl = url.searchParams.get("url") ?? "";
30
- return imageUrl.startsWith("/")
31
- ? env.ASSETS?.fetch(`http://assets.local${imageUrl}`)
32
- : fetch(imageUrl, { cf: { cacheEverything: true } });
32
+ return fetchImage(env.ASSETS, imageUrl);
33
33
  }
34
34
  // - `Request`s are handled by the Next server
35
35
  const reqOrResp = await middlewareHandler(request, env, ctx);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennextjs/cloudflare",
3
3
  "description": "Cloudflare builder for next apps",
4
- "version": "1.2.1",
4
+ "version": "1.3.1",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -43,7 +43,7 @@
43
43
  "homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
44
44
  "dependencies": {
45
45
  "@dotenvx/dotenvx": "1.31.0",
46
- "@opennextjs/aws": "3.6.5",
46
+ "@opennextjs/aws": "3.6.6",
47
47
  "enquirer": "^2.4.1",
48
48
  "glob": "^11.0.0",
49
49
  "ts-tqdm": "^0.8.6"
@@ -54,6 +54,8 @@
54
54
  "@tsconfig/strictest": "^2.0.5",
55
55
  "@types/mock-fs": "^4.13.4",
56
56
  "@types/node": "^22.2.0",
57
+ "@types/picomatch": "^4.0.0",
58
+ "diff": "^8.0.2",
57
59
  "esbuild": "^0.25.4",
58
60
  "eslint": "^9.11.1",
59
61
  "eslint-plugin-import": "^2.31.0",
@@ -62,6 +64,7 @@
62
64
  "globals": "^15.9.0",
63
65
  "mock-fs": "^5.4.1",
64
66
  "next": "~14.2.24",
67
+ "picomatch": "^4.0.2",
65
68
  "rimraf": "^6.0.1",
66
69
  "typescript": "^5.7.3",
67
70
  "typescript-eslint": "^8.7.0",
@@ -1 +0,0 @@
1
- export {};
@@ -1,121 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import * as internal from "../overrides/internal";
3
- import { BucketCachePurge } from "./bucket-cache-purge";
4
- vi.mock("cloudflare:workers", () => ({
5
- DurableObject: class {
6
- ctx;
7
- env;
8
- constructor(ctx, env) {
9
- this.ctx = ctx;
10
- this.env = env;
11
- }
12
- },
13
- }));
14
- const createBucketCachePurge = () => {
15
- const mockState = {
16
- waitUntil: vi.fn(),
17
- blockConcurrencyWhile: vi.fn().mockImplementation(async (fn) => fn()),
18
- storage: {
19
- setAlarm: vi.fn(),
20
- getAlarm: vi.fn(),
21
- sql: {
22
- exec: vi.fn().mockImplementation(() => ({
23
- one: vi.fn(),
24
- toArray: vi.fn().mockReturnValue([]),
25
- })),
26
- },
27
- },
28
- };
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- return new BucketCachePurge(mockState, {});
31
- };
32
- describe("BucketCachePurge", () => {
33
- it("should block concurrency while creating the table", async () => {
34
- const cache = createBucketCachePurge();
35
- // @ts-expect-error - testing private method
36
- expect(cache.ctx.blockConcurrencyWhile).toHaveBeenCalled();
37
- // @ts-expect-error - testing private method
38
- expect(cache.ctx.storage.sql.exec).toHaveBeenCalledWith(expect.stringContaining("CREATE TABLE IF NOT EXISTS cache_purge"));
39
- });
40
- describe("purgeCacheByTags", () => {
41
- it("should insert tags into the sql table", async () => {
42
- const cache = createBucketCachePurge();
43
- const tags = ["tag1", "tag2"];
44
- await cache.purgeCacheByTags(tags);
45
- // @ts-expect-error - testing private method
46
- expect(cache.ctx.storage.sql.exec).toHaveBeenCalledWith(expect.stringContaining("INSERT OR REPLACE INTO cache_purge"), [tags[0]]);
47
- // @ts-expect-error - testing private method
48
- expect(cache.ctx.storage.sql.exec).toHaveBeenCalledWith(expect.stringContaining("INSERT OR REPLACE INTO cache_purge"), [tags[1]]);
49
- });
50
- it("should set an alarm if no alarm is set", async () => {
51
- const cache = createBucketCachePurge();
52
- // @ts-expect-error - testing private method
53
- cache.ctx.storage.getAlarm.mockResolvedValueOnce(null);
54
- await cache.purgeCacheByTags(["tag"]);
55
- // @ts-expect-error - testing private method
56
- expect(cache.ctx.storage.setAlarm).toHaveBeenCalled();
57
- });
58
- it("should not set an alarm if one is already set", async () => {
59
- const cache = createBucketCachePurge();
60
- // @ts-expect-error - testing private method
61
- cache.ctx.storage.getAlarm.mockResolvedValueOnce(true);
62
- await cache.purgeCacheByTags(["tag"]);
63
- // @ts-expect-error - testing private method
64
- expect(cache.ctx.storage.setAlarm).not.toHaveBeenCalled();
65
- });
66
- });
67
- describe("alarm", () => {
68
- it("should purge cache by tags and delete them from the sql table", async () => {
69
- const cache = createBucketCachePurge();
70
- // @ts-expect-error - testing private method
71
- cache.ctx.storage.sql.exec.mockReturnValueOnce({
72
- toArray: () => [{ tag: "tag1" }, { tag: "tag2" }],
73
- });
74
- await cache.alarm();
75
- // @ts-expect-error - testing private method
76
- expect(cache.ctx.storage.sql.exec).toHaveBeenCalledWith(expect.stringContaining("DELETE FROM cache_purge"), ["tag1", "tag2"]);
77
- });
78
- it("should not purge cache if no tags are found", async () => {
79
- const cache = createBucketCachePurge();
80
- // @ts-expect-error - testing private method
81
- cache.ctx.storage.sql.exec.mockReturnValueOnce({
82
- toArray: () => [],
83
- });
84
- await cache.alarm();
85
- // @ts-expect-error - testing private method
86
- expect(cache.ctx.storage.sql.exec).not.toHaveBeenCalledWith(expect.stringContaining("DELETE FROM cache_purge"), []);
87
- });
88
- it("should call internalPurgeCacheByTags with the correct tags", async () => {
89
- const cache = createBucketCachePurge();
90
- const tags = ["tag1", "tag2"];
91
- // @ts-expect-error - testing private method
92
- cache.ctx.storage.sql.exec.mockReturnValueOnce({
93
- toArray: () => tags.map((tag) => ({ tag })),
94
- });
95
- const internalPurgeCacheByTagsSpy = vi.spyOn(internal, "internalPurgeCacheByTags");
96
- await cache.alarm();
97
- expect(internalPurgeCacheByTagsSpy).toHaveBeenCalledWith(
98
- // @ts-expect-error - testing private method
99
- cache.env, tags);
100
- // @ts-expect-error - testing private method 1st is constructor, 2nd is to get the tags and 3rd is to delete them
101
- expect(cache.ctx.storage.sql.exec).toHaveBeenCalledTimes(3);
102
- });
103
- it("should continue until all tags are purged", async () => {
104
- const cache = createBucketCachePurge();
105
- const tags = Array.from({ length: 100 }, (_, i) => `tag${i}`);
106
- // @ts-expect-error - testing private method
107
- cache.ctx.storage.sql.exec.mockReturnValueOnce({
108
- toArray: () => tags.map((tag) => ({ tag })),
109
- });
110
- const internalPurgeCacheByTagsSpy = vi.spyOn(internal, "internalPurgeCacheByTags");
111
- await cache.alarm();
112
- expect(internalPurgeCacheByTagsSpy).toHaveBeenCalledWith(
113
- // @ts-expect-error - testing private method
114
- cache.env, tags);
115
- // @ts-expect-error - testing private method 1st is constructor, 2nd is to get the tags and 3rd is to delete them, 4th is to get the next 100 tags
116
- expect(cache.ctx.storage.sql.exec).toHaveBeenCalledTimes(4);
117
- // @ts-expect-error - testing private method
118
- expect(cache.ctx.storage.sql.exec).toHaveBeenLastCalledWith(expect.stringContaining("SELECT * FROM cache_purge LIMIT 100"));
119
- });
120
- });
121
- });
@@ -1 +0,0 @@
1
- export {};