@opennextjs/cloudflare 0.4.0 → 0.4.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.
@@ -41,10 +41,28 @@ export function getCloudflareContext() {
41
41
  * Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
42
42
  */
43
43
  export async function initOpenNextCloudflareForDev() {
44
+ const shouldInitializationRun = shouldContextInitializationRun();
45
+ if (!shouldInitializationRun)
46
+ return;
44
47
  const context = await getCloudflareContextFromWrangler();
45
48
  addCloudflareContextToNodejsGlobal(context);
46
49
  await monkeyPatchVmModuleEdgeContext(context);
47
50
  }
51
+ /**
52
+ * Next dev server imports the config file twice (in two different processes, making it hard to track),
53
+ * this causes the initialization to run twice as well, to keep things clean, not allocate extra
54
+ * resources (i.e. instantiate two miniflare instances) and avoid extra potential logs, it would be best
55
+ * to run the initialization only once, this function is used to try to make it so that it does, it returns
56
+ * a flag which indicates if the initialization should run in the current process or not.
57
+ *
58
+ * @returns boolean indicating if the initialization should run
59
+ */
60
+ function shouldContextInitializationRun() {
61
+ // via debugging we've seen that AsyncLocalStorage is only set in one of the
62
+ // two processes so we're using it as the differentiator between the two
63
+ const AsyncLocalStorage = globalThis["AsyncLocalStorage"];
64
+ return !!AsyncLocalStorage;
65
+ }
48
66
  /**
49
67
  * Adds the cloudflare context to the global scope in which the Next.js dev node.js process runs in, enabling
50
68
  * future calls to `getCloudflareContext` to retrieve and return such context
@@ -8,6 +8,8 @@ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
8
8
  import { build } from "esbuild";
9
9
  import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
10
10
  import * as patches from "./patches/index.js";
11
+ import inlineRequirePagePlugin from "./patches/plugins/require-page.js";
12
+ import setWranglerExternal from "./patches/plugins/wrangler-external.js";
11
13
  import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
12
14
  /** The dist directory of the Cloudflare adapter package */
13
15
  const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -34,8 +36,18 @@ export async function bundleServer(buildOpts) {
34
36
  format: "esm",
35
37
  target: "esnext",
36
38
  minify: false,
37
- plugins: [createFixRequiresESBuildPlugin(buildOpts)],
38
- external: ["./middleware/handler.mjs", "caniuse-lite"],
39
+ plugins: [
40
+ createFixRequiresESBuildPlugin(buildOpts),
41
+ inlineRequirePagePlugin(buildOpts),
42
+ setWranglerExternal(),
43
+ ],
44
+ external: [
45
+ "./middleware/handler.mjs",
46
+ // Next optional dependencies.
47
+ "caniuse-lite",
48
+ "jimp",
49
+ "probe-image-size",
50
+ ],
39
51
  alias: {
40
52
  // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
41
53
  // eval("require")("bufferutil");
@@ -121,7 +133,6 @@ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
121
133
  ["require", patches.patchRequire],
122
134
  ["`buildId` function", (code) => patches.patchBuildId(code, buildOpts)],
123
135
  ["`loadManifest` function", (code) => patches.patchLoadManifest(code, buildOpts)],
124
- ["next's require", (code) => patches.inlineNextRequire(code, buildOpts)],
125
136
  ["`findDir` function", (code) => patches.patchFindDir(code, buildOpts)],
126
137
  ["`evalManifest` function", (code) => patches.inlineEvalManifest(code, buildOpts)],
127
138
  ["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
@@ -1,12 +1,12 @@
1
1
  import { type SgNode } from "@ast-grep/napi";
2
2
  /**
3
- * Handle optional dependencies.
3
+ * Handles optional dependencies.
4
4
  *
5
5
  * A top level `require(optionalDep)` would throw when the dep is not installed.
6
6
  *
7
7
  * So we wrap `require(optionalDep)` in a try/catch (if not already present).
8
8
  */
9
- export declare const optionalDepRule = "\nrule:\n pattern: $$$LHS = require($$$REQ)\n has:\n pattern: $MOD\n kind: string_fragment\n stopBy: end\n regex: ^caniuse-lite(/|$)\n not:\n inside:\n kind: try_statement\n stopBy: end\n\nfix: |-\n try {\n $$$LHS = require($$$REQ);\n } catch {\n throw new Error('The optional dependency \"$MOD\" is not installed');\n }\n";
9
+ export declare const optionalDepRule = "\nrule:\n pattern: $$$LHS = require($$$REQ)\n has:\n pattern: $MOD\n kind: string_fragment\n stopBy: end\n regex: ^(caniuse-lite|jimp|probe-image-size)(/|$)\n not:\n inside:\n kind: try_statement\n stopBy: end\n\nfix: |-\n try {\n $$$LHS = require($$$REQ);\n } catch {\n throw new Error('The optional dependency \"$MOD\" is not installed');\n }\n";
10
10
  export declare function patchOptionalDependencies(root: SgNode): {
11
11
  edits: import("@ast-grep/napi").Edit[];
12
12
  matches: SgNode<import("@ast-grep/napi/types/staticTypes.js").TypesMap, import("@ast-grep/napi/types/staticTypes.js").Kinds<import("@ast-grep/napi/types/staticTypes.js").TypesMap>>[];
@@ -1,6 +1,6 @@
1
1
  import { applyRule } from "./util.js";
2
2
  /**
3
- * Handle optional dependencies.
3
+ * Handles optional dependencies.
4
4
  *
5
5
  * A top level `require(optionalDep)` would throw when the dep is not installed.
6
6
  *
@@ -13,7 +13,7 @@ rule:
13
13
  pattern: $MOD
14
14
  kind: string_fragment
15
15
  stopBy: end
16
- regex: ^caniuse-lite(/|$)
16
+ regex: ^(caniuse-lite|jimp|probe-image-size)(/|$)
17
17
  not:
18
18
  inside:
19
19
  kind: try_statement
@@ -0,0 +1,6 @@
1
+ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
+ import type { PluginBuild } from "esbuild";
3
+ export default function inlineRequirePagePlugin(buildOpts: BuildOptions): {
4
+ name: string;
5
+ setup: (build: PluginBuild) => Promise<void>;
6
+ };
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
5
+ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
6
+ import { patchCode } from "../ast/util.js";
7
+ export default function inlineRequirePagePlugin(buildOpts) {
8
+ return {
9
+ name: "inline-require-page",
10
+ setup: async (build) => {
11
+ build.onLoad({
12
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/require\.js$`, { escape: false }),
13
+ }, async ({ path }) => {
14
+ const jsCode = await readFile(path, "utf8");
15
+ if (/function requirePage\(/.test(jsCode)) {
16
+ return { contents: patchCode(jsCode, getRule(buildOpts)) };
17
+ }
18
+ });
19
+ },
20
+ };
21
+ }
22
+ function getRule(buildOpts) {
23
+ const { outputDir } = buildOpts;
24
+ const serverDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
25
+ const pagesManifestFile = join(serverDir, "pages-manifest.json");
26
+ const appPathsManifestFile = join(serverDir, "app-paths-manifest.json");
27
+ const pagesManifests = existsSync(pagesManifestFile)
28
+ ? Object.values(JSON.parse(readFileSync(pagesManifestFile, "utf-8")))
29
+ : [];
30
+ const appPathsManifests = existsSync(appPathsManifestFile)
31
+ ? Object.values(JSON.parse(readFileSync(appPathsManifestFile, "utf-8")))
32
+ : [];
33
+ const manifests = pagesManifests.concat(appPathsManifests);
34
+ const htmlFiles = manifests.filter((file) => file.endsWith(".html"));
35
+ const jsFiles = manifests.filter((file) => file.endsWith(".js"));
36
+ // Inline fs access and dynamic require that are not supported by workerd.
37
+ const fnBody = `
38
+ // html
39
+ ${htmlFiles
40
+ .map((file) => `if (pagePath.endsWith("${file}")) {
41
+ return ${JSON.stringify(readFileSync(join(serverDir, file), "utf-8"))};
42
+ }`)
43
+ .join("\n")}
44
+ // js
45
+ process.env.__NEXT_PRIVATE_RUNTIME_TYPE = isAppPath ? 'app' : 'pages';
46
+ try {
47
+ ${jsFiles
48
+ .map((file) => `if (pagePath.endsWith("${file}")) {
49
+ return require(${JSON.stringify(join(serverDir, file))});
50
+ }`)
51
+ .join("\n")}
52
+ } finally {
53
+ process.env.__NEXT_PRIVATE_RUNTIME_TYPE = '';
54
+ }
55
+ `;
56
+ return {
57
+ rule: {
58
+ pattern: `
59
+ function requirePage($PAGE, $DIST_DIR, $IS_APPP_ATH) {
60
+ const $_ = getPagePath($$$ARGS);
61
+ $$$_BODY
62
+ }`,
63
+ },
64
+ fix: `
65
+ function requirePage($PAGE, $DIST_DIR, $IS_APPP_ATH) {
66
+ const pagePath = getPagePath($$$ARGS);
67
+ ${fnBody}
68
+ }`,
69
+ };
70
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ESBuild plugin to mark files bundled by wrangler as external.
3
+ *
4
+ * `.wasm` and `.bin` will ultimately be bundled by wrangler.
5
+ * We should only mark them as external in the adapter.
6
+ *
7
+ * However simply marking them as external would copy the import path to the bundle,
8
+ * i.e. `import("./file.wasm?module")` and given than the bundle is generated in a
9
+ * different location than the input files, the relative path would not be valid.
10
+ *
11
+ * This ESBuild plugin convert relative paths to absolute paths so that they are
12
+ * still valid from inside the bundle.
13
+ *
14
+ * ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15
+ */
16
+ import type { PluginBuild } from "esbuild";
17
+ export default function setWranglerExternal(): {
18
+ name: string;
19
+ setup: (build: PluginBuild) => Promise<void>;
20
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ESBuild plugin to mark files bundled by wrangler as external.
3
+ *
4
+ * `.wasm` and `.bin` will ultimately be bundled by wrangler.
5
+ * We should only mark them as external in the adapter.
6
+ *
7
+ * However simply marking them as external would copy the import path to the bundle,
8
+ * i.e. `import("./file.wasm?module")` and given than the bundle is generated in a
9
+ * different location than the input files, the relative path would not be valid.
10
+ *
11
+ * This ESBuild plugin convert relative paths to absolute paths so that they are
12
+ * still valid from inside the bundle.
13
+ *
14
+ * ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15
+ */
16
+ import { dirname, resolve } from "node:path";
17
+ export default function setWranglerExternal() {
18
+ return {
19
+ name: "wrangler-externals",
20
+ setup: async (build) => {
21
+ const namespace = "wrangler-externals-plugin";
22
+ build.onResolve({ filter: /(\.bin|\.wasm\?module)$/ }, ({ path, importer }) => {
23
+ return {
24
+ path: resolve(dirname(importer), path),
25
+ namespace,
26
+ external: true,
27
+ };
28
+ });
29
+ build.onLoad({ filter: /.*/, namespace }, async ({ path }) => {
30
+ return {
31
+ contents: `export * from '${path}';`,
32
+ };
33
+ });
34
+ },
35
+ };
36
+ }
@@ -1,6 +1,5 @@
1
1
  export * from "./inline-eval-manifest.js";
2
2
  export * from "./inline-middleware-manifest-require.js";
3
- export * from "./inline-next-require.js";
4
3
  export * from "./patch-exception-bubbling.js";
5
4
  export * from "./patch-find-dir.js";
6
5
  export * from "./patch-load-instrumentation-module.js";
@@ -1,6 +1,5 @@
1
1
  export * from "./inline-eval-manifest.js";
2
2
  export * from "./inline-middleware-manifest-require.js";
3
- export * from "./inline-next-require.js";
4
3
  export * from "./patch-exception-bubbling.js";
5
4
  export * from "./patch-find-dir.js";
6
5
  export * from "./patch-load-instrumentation-module.js";
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": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -1,6 +0,0 @@
1
- import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
- /**
3
- * The following avoid various Next.js specific files `require`d at runtime since we can just read
4
- * and inline their content during build time
5
- */
6
- export declare function inlineNextRequire(code: string, buildOpts: BuildOptions): string;
@@ -1,40 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { getPackagePath } from "@opennextjs/aws/build/helper.js";
4
- /**
5
- * The following avoid various Next.js specific files `require`d at runtime since we can just read
6
- * and inline their content during build time
7
- */
8
- // TODO(vicb): __NEXT_PRIVATE_RUNTIME_TYPE is not handled by this patch
9
- export function inlineNextRequire(code, buildOpts) {
10
- const { outputDir } = buildOpts;
11
- const serverDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
12
- const pagesManifestFile = join(serverDir, "pages-manifest.json");
13
- const appPathsManifestFile = join(serverDir, "app-paths-manifest.json");
14
- const pagesManifests = existsSync(pagesManifestFile)
15
- ? Object.values(JSON.parse(readFileSync(pagesManifestFile, "utf-8")))
16
- : [];
17
- const appPathsManifests = existsSync(appPathsManifestFile)
18
- ? Object.values(JSON.parse(readFileSync(appPathsManifestFile, "utf-8")))
19
- : [];
20
- const manifests = pagesManifests.concat(appPathsManifests);
21
- const htmlPages = manifests.filter((file) => file.endsWith(".html"));
22
- const pageModules = manifests.filter((file) => file.endsWith(".js"));
23
- return code.replace(/const pagePath = getPagePath\(.+?\);/, `$&
24
- ${htmlPages
25
- .map((htmlPage) => `
26
- if (pagePath.endsWith("${htmlPage}")) {
27
- return ${JSON.stringify(readFileSync(join(serverDir, htmlPage), "utf-8"))};
28
- }
29
- `)
30
- .join("\n")}
31
- ${pageModules
32
- .map((module) => `
33
- if (pagePath.endsWith("${module}")) {
34
- return require(${JSON.stringify(join(serverDir, module))});
35
- }
36
- `)
37
- .join("\n")}
38
- throw new Error("Unknown pagePath: " + pagePath);
39
- `);
40
- }