@opennextjs/cloudflare 0.3.10 → 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.
Files changed (60) hide show
  1. package/README.md +1 -64
  2. package/dist/api/{get-cloudflare-context.d.ts → cloudflare-context.d.ts} +8 -1
  3. package/dist/api/cloudflare-context.js +114 -0
  4. package/dist/api/index.d.ts +1 -1
  5. package/dist/api/index.js +1 -1
  6. package/dist/api/kvCache.d.ts +0 -5
  7. package/dist/api/kvCache.js +15 -28
  8. package/dist/cli/build/build.d.ts +1 -1
  9. package/dist/cli/build/build.js +2 -12
  10. package/dist/cli/build/bundle-server.d.ts +4 -5
  11. package/dist/cli/build/bundle-server.js +44 -37
  12. package/dist/cli/build/open-next/compile-env-files.d.ts +1 -1
  13. package/dist/cli/build/open-next/compile-env-files.js +2 -2
  14. package/dist/cli/build/patches/ast/optional-deps.d.ts +6 -3
  15. package/dist/cli/build/patches/ast/optional-deps.js +4 -4
  16. package/dist/cli/build/patches/ast/util.d.ts +15 -4
  17. package/dist/cli/build/patches/ast/util.js +16 -5
  18. package/dist/cli/build/patches/ast/vercel-og.d.ts +23 -0
  19. package/dist/cli/build/patches/ast/vercel-og.js +58 -0
  20. package/dist/cli/build/patches/ast/vercel-og.spec.d.ts +1 -0
  21. package/dist/cli/build/patches/ast/vercel-og.spec.js +22 -0
  22. package/dist/cli/build/patches/investigated/copy-package-cli-files.d.ts +4 -3
  23. package/dist/cli/build/patches/investigated/copy-package-cli-files.js +8 -5
  24. package/dist/cli/build/patches/investigated/index.d.ts +1 -0
  25. package/dist/cli/build/patches/investigated/index.js +1 -0
  26. package/dist/cli/build/patches/investigated/patch-cache.d.ts +2 -2
  27. package/dist/cli/build/patches/investigated/patch-cache.js +7 -6
  28. package/dist/cli/build/patches/investigated/patch-require.d.ts +1 -4
  29. package/dist/cli/build/patches/investigated/patch-require.js +1 -4
  30. package/dist/cli/build/patches/investigated/patch-vercel-og-library.d.ts +7 -0
  31. package/dist/cli/build/patches/investigated/patch-vercel-og-library.js +39 -0
  32. package/dist/cli/build/patches/investigated/patch-vercel-og-library.spec.d.ts +1 -0
  33. package/dist/cli/build/patches/investigated/patch-vercel-og-library.spec.js +50 -0
  34. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.d.ts +2 -5
  35. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.js +6 -6
  36. package/dist/cli/build/patches/plugins/require-page.d.ts +6 -0
  37. package/dist/cli/build/patches/plugins/require-page.js +70 -0
  38. package/dist/cli/build/patches/plugins/wrangler-external.d.ts +20 -0
  39. package/dist/cli/build/patches/plugins/wrangler-external.js +36 -0
  40. package/dist/cli/build/patches/to-investigate/index.d.ts +0 -1
  41. package/dist/cli/build/patches/to-investigate/index.js +0 -1
  42. package/dist/cli/build/patches/to-investigate/inline-eval-manifest.d.ts +2 -2
  43. package/dist/cli/build/patches/to-investigate/inline-eval-manifest.js +21 -17
  44. package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.d.ts +2 -2
  45. package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.js +4 -2
  46. package/dist/cli/build/patches/to-investigate/patch-find-dir.d.ts +3 -6
  47. package/dist/cli/build/patches/to-investigate/patch-find-dir.js +11 -11
  48. package/dist/cli/build/patches/to-investigate/patch-read-file.d.ts +3 -3
  49. package/dist/cli/build/patches/to-investigate/patch-read-file.js +15 -16
  50. package/dist/cli/build/patches/to-investigate/wrangler-deps.d.ts +2 -2
  51. package/dist/cli/build/patches/to-investigate/wrangler-deps.js +36 -67
  52. package/dist/cli/build/utils/create-config-files.d.ts +1 -1
  53. package/dist/cli/project-options.d.ts +7 -0
  54. package/dist/cli/project-options.js +1 -0
  55. package/package.json +4 -4
  56. package/dist/api/get-cloudflare-context.js +0 -42
  57. package/dist/cli/build/patches/to-investigate/inline-next-require.d.ts +0 -6
  58. package/dist/cli/build/patches/to-investigate/inline-next-require.js +0 -36
  59. package/dist/cli/config.d.ts +0 -42
  60. package/dist/cli/config.js +0 -92
package/README.md CHANGED
@@ -6,70 +6,7 @@ Deploy Next.js apps to Cloudflare!
6
6
 
7
7
  ## Get started
8
8
 
9
- You can use [`create-next-app`](https://nextjs.org/docs/pages/api-reference/cli/create-next-app) to start a new application or take an existing Next.js application and deploy it to Cloudflare using the following few steps:
10
-
11
- ## Configure your app
12
-
13
- - add the following `devDependencies` to the `package.json`:
14
-
15
- ```bash
16
- npm add -D wrangler@latest @opennextjs/cloudflare
17
- # or
18
- pnpm add -D wrangler@latest @opennextjs/cloudflare
19
- # or
20
- yarn add -D wrangler@latest @opennextjs/cloudflare
21
- # or
22
- bun add -D wrangler@latest @opennextjs/cloudflare
23
- ```
24
-
25
- - add a `wrangler.toml` at the root of your project
26
-
27
- ```toml
28
- #:schema node_modules/wrangler/config-schema.json
29
- name = "<your-app-name>"
30
- main = ".open-next/worker.js"
31
-
32
- compatibility_date = "2024-09-23"
33
- compatibility_flags = ["nodejs_compat"]
34
-
35
- # Use the new Workers + Assets to host the static frontend files
36
- assets = { directory = ".open-next/assets", binding = "ASSETS" }
37
- ```
38
-
39
- - add a `open-next.config.ts` at the root of your project:
40
-
41
- ```ts
42
- import type { OpenNextConfig } from "open-next/types/open-next";
43
-
44
- const config: OpenNextConfig = {
45
- default: {
46
- override: {
47
- wrapper: "cloudflare-node",
48
- converter: "edge",
49
- // Unused implementation
50
- incrementalCache: "dummy",
51
- tagCache: "dummy",
52
- queue: "dummy",
53
- },
54
- },
55
-
56
- middleware: {
57
- external: true,
58
- override: {
59
- wrapper: "cloudflare-edge",
60
- converter: "edge",
61
- proxyExternalRequest: "fetch",
62
- },
63
- },
64
- };
65
-
66
- export default config;
67
- ```
68
-
69
- ## Known issues
70
-
71
- - `▲ [WARNING] Suspicious assignment to defined constant "process.env.NODE_ENV" [assign-to-define]` can safely be ignored
72
- - Maybe more, still experimental...
9
+ To get started with the adapter visit the [official get started documentation](https://opennext.js.org/cloudflare/get-started).
73
10
 
74
11
  ## Local development
75
12
 
@@ -23,4 +23,11 @@ export type CloudflareContext<CfProperties extends Record<string, unknown> = Inc
23
23
  *
24
24
  * @returns the cloudflare context
25
25
  */
26
- export declare function getCloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext>(): Promise<CloudflareContext<CfProperties, Context>>;
26
+ export declare function getCloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext>(): CloudflareContext<CfProperties, Context>;
27
+ /**
28
+ * Performs some initial setup to integrate as best as possible the local Next.js dev server (run via `next dev`)
29
+ * with the open-next Cloudflare adapter
30
+ *
31
+ * Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
32
+ */
33
+ export declare function initOpenNextCloudflareForDev(): Promise<void>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Symbol used as an index in the global scope to set and retrieve the Cloudflare context
3
+ *
4
+ * This is used both in production (in the actual built worker) and in development (`next dev`)
5
+ *
6
+ * Note: this symbol needs to be kept in sync with the one used in `src/cli/templates/worker.ts`
7
+ */
8
+ const cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
9
+ /**
10
+ * Utility to get the current Cloudflare context
11
+ *
12
+ * @returns the cloudflare context
13
+ */
14
+ export function getCloudflareContext() {
15
+ const global = globalThis;
16
+ const cloudflareContext = global[cloudflareContextSymbol];
17
+ if (!cloudflareContext) {
18
+ // the cloudflare context is initialized by the worker and is always present in production/preview
19
+ // during local development (`next dev`) it might be missing only if the developers hasn't called
20
+ // the `initOpenNextCloudflareForDev` function in their Next.js config file
21
+ const getContextFunctionName = getCloudflareContext.name;
22
+ const initFunctionName = initOpenNextCloudflareForDev.name;
23
+ throw new Error(`\n\n\`${getContextFunctionName}\` has been called during development without having called` +
24
+ ` the \`${initFunctionName}\` function inside the Next.js config file.\n\n` +
25
+ `In order to use \`${getContextFunctionName}\` import and call ${initFunctionName} in the Next.js config file.\n\n` +
26
+ "Example: \n ```\n // next.config.mjs\n\n" +
27
+ ` import { ${initFunctionName} } from "@opennextjs/cloudflare";\n\n` +
28
+ ` ${initFunctionName}();\n\n` +
29
+ " /** @type {import('next').NextConfig} */\n" +
30
+ " const nextConfig = {};\n" +
31
+ " export default nextConfig;\n" +
32
+ " ```\n" +
33
+ "\n(note: currently middlewares in Next.js are always run using the edge runtime)\n\n");
34
+ }
35
+ return cloudflareContext;
36
+ }
37
+ /**
38
+ * Performs some initial setup to integrate as best as possible the local Next.js dev server (run via `next dev`)
39
+ * with the open-next Cloudflare adapter
40
+ *
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
+ */
43
+ export async function initOpenNextCloudflareForDev() {
44
+ const shouldInitializationRun = shouldContextInitializationRun();
45
+ if (!shouldInitializationRun)
46
+ return;
47
+ const context = await getCloudflareContextFromWrangler();
48
+ addCloudflareContextToNodejsGlobal(context);
49
+ await monkeyPatchVmModuleEdgeContext(context);
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
+ }
66
+ /**
67
+ * Adds the cloudflare context to the global scope in which the Next.js dev node.js process runs in, enabling
68
+ * future calls to `getCloudflareContext` to retrieve and return such context
69
+ *
70
+ * @param cloudflareContext the cloudflare context to add to the node.sj global scope
71
+ */
72
+ function addCloudflareContextToNodejsGlobal(cloudflareContext) {
73
+ const global = globalThis;
74
+ global[cloudflareContextSymbol] = cloudflareContext;
75
+ }
76
+ /**
77
+ * Next.js uses the Node.js vm module's `runInContext()` function to evaluate edge functions
78
+ * in a runtime context that tries to simulate as accurately as possible the actual production runtime
79
+ * behavior, see: https://github.com/vercel/next.js/blob/9a1cd3/packages/next/src/server/web/sandbox/context.ts#L525-L527
80
+ *
81
+ * This function monkey-patches the Node.js `vm` module to override the `runInContext()` function so that the
82
+ * cloudflare context is added to the runtime context's global scope before edge functions are evaluated
83
+ *
84
+ * @param cloudflareContext the cloudflare context to patch onto the "edge" runtime context global scope
85
+ */
86
+ async function monkeyPatchVmModuleEdgeContext(cloudflareContext) {
87
+ const require = (await import(/* webpackIgnore: true */ `${"__module".replaceAll("_", "")}`)).default.createRequire(import.meta.url);
88
+ // eslint-disable-next-line unicorn/prefer-node-protocol -- the `next dev` compiler doesn't accept the node prefix
89
+ const vmModule = require("vm");
90
+ const originalRunInContext = vmModule.runInContext.bind(vmModule);
91
+ vmModule.runInContext = (code, contextifiedObject, options) => {
92
+ const runtimeContext = contextifiedObject;
93
+ runtimeContext[cloudflareContextSymbol] ??= cloudflareContext;
94
+ return originalRunInContext(code, contextifiedObject, options);
95
+ };
96
+ }
97
+ /**
98
+ * Gets a cloudflare context object from wrangler
99
+ *
100
+ * @returns the cloudflare context ready for use
101
+ */
102
+ async function getCloudflareContextFromWrangler() {
103
+ // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
104
+ const { getPlatformProxy } = await import(/* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`);
105
+ const { env, cf, ctx } = await getPlatformProxy({
106
+ // This allows the selection of a wrangler environment while running in next dev mode
107
+ environment: process.env.NEXT_DEV_WRANGLER_ENV,
108
+ });
109
+ return {
110
+ env,
111
+ cf: cf,
112
+ ctx: ctx,
113
+ };
114
+ }
@@ -1 +1 @@
1
- export * from "./get-cloudflare-context.js";
1
+ export * from "./cloudflare-context.js";
package/dist/api/index.js CHANGED
@@ -1 +1 @@
1
- export * from "./get-cloudflare-context.js";
1
+ export * from "./cloudflare-context.js";
@@ -1,4 +1,3 @@
1
- import type { KVNamespace } from "@cloudflare/workers-types";
2
1
  import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
3
2
  export declare const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
4
3
  export declare const STATUS_DELETED = 1;
@@ -11,9 +10,6 @@ export declare const STATUS_DELETED = 1;
11
10
  */
12
11
  declare class Cache implements IncrementalCache {
13
12
  readonly name = "cloudflare-kv";
14
- protected initialized: boolean;
15
- protected kv: KVNamespace | undefined;
16
- protected assets: Fetcher | undefined;
17
13
  get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>>>;
18
14
  set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
19
15
  delete(key: string): Promise<void>;
@@ -21,7 +17,6 @@ declare class Cache implements IncrementalCache {
21
17
  protected getAssetUrl(key: string, isFetch?: boolean): string;
22
18
  protected debug(...args: unknown[]): void;
23
19
  protected getBuildId(): string;
24
- protected init(): Promise<void>;
25
20
  }
26
21
  declare const _default: Cache;
27
22
  export default _default;
@@ -1,5 +1,5 @@
1
1
  import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
2
- import { getCloudflareContext } from "./get-cloudflare-context.js";
2
+ import { getCloudflareContext } from "./cloudflare-context.js";
3
3
  export const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
4
4
  export const STATUS_DELETED = 1;
5
5
  /**
@@ -11,31 +11,28 @@ export const STATUS_DELETED = 1;
11
11
  */
12
12
  class Cache {
13
13
  name = "cloudflare-kv";
14
- initialized = false;
15
- kv;
16
- assets;
17
14
  async get(key, isFetch) {
18
- if (!this.initialized) {
19
- await this.init();
20
- }
21
- if (!(this.kv || this.assets)) {
15
+ const cfEnv = getCloudflareContext().env;
16
+ const kv = cfEnv.NEXT_CACHE_WORKERS_KV;
17
+ const assets = cfEnv.ASSETS;
18
+ if (!(kv || assets)) {
22
19
  throw new IgnorableError(`No KVNamespace nor Fetcher`);
23
20
  }
24
21
  this.debug(`Get ${key}`);
25
22
  try {
26
23
  let entry = null;
27
- if (this.kv) {
24
+ if (kv) {
28
25
  this.debug(`- From KV`);
29
26
  const kvKey = this.getKVKey(key, isFetch);
30
- entry = await this.kv.get(kvKey, "json");
27
+ entry = await kv.get(kvKey, "json");
31
28
  if (entry?.status === STATUS_DELETED) {
32
29
  return {};
33
30
  }
34
31
  }
35
- if (!entry && this.assets) {
32
+ if (!entry && assets) {
36
33
  this.debug(`- From Assets`);
37
34
  const url = this.getAssetUrl(key, isFetch);
38
- const response = await this.assets.fetch(url);
35
+ const response = await assets.fetch(url);
39
36
  if (response.ok) {
40
37
  // TODO: consider populating KV with the asset value if faster.
41
38
  // This could be optional as KV writes are $$.
@@ -55,10 +52,8 @@ class Cache {
55
52
  }
56
53
  }
57
54
  async set(key, value, isFetch) {
58
- if (!this.initialized) {
59
- await this.init();
60
- }
61
- if (!this.kv) {
55
+ const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
56
+ if (!kv) {
62
57
  throw new IgnorableError(`No KVNamespace`);
63
58
  }
64
59
  this.debug(`Set ${key}`);
@@ -67,7 +62,7 @@ class Cache {
67
62
  // Note: We can not set a TTL as we might fallback to assets,
68
63
  // still removing old data (old BUILD_ID) could help avoiding
69
64
  // the cache growing too big.
70
- await this.kv.put(kvKey, JSON.stringify({
65
+ await kv.put(kvKey, JSON.stringify({
71
66
  value,
72
67
  // Note: `Date.now()` returns the time of the last IO rather than the actual time.
73
68
  // See https://developers.cloudflare.com/workers/reference/security-model/
@@ -79,17 +74,15 @@ class Cache {
79
74
  }
80
75
  }
81
76
  async delete(key) {
82
- if (!this.initialized) {
83
- await this.init();
84
- }
85
- if (!this.kv) {
77
+ const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
78
+ if (!kv) {
86
79
  throw new IgnorableError(`No KVNamespace`);
87
80
  }
88
81
  this.debug(`Delete ${key}`);
89
82
  try {
90
83
  const kvKey = this.getKVKey(key, /* isFetch= */ false);
91
84
  // Do not delete the key as we would then fallback to the assets.
92
- await this.kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
85
+ await kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
93
86
  }
94
87
  catch {
95
88
  throw new RecoverableError(`Failed to delete cache [${key}]`);
@@ -111,11 +104,5 @@ class Cache {
111
104
  getBuildId() {
112
105
  return process.env.NEXT_BUILD_ID ?? "no-build-id";
113
106
  }
114
- async init() {
115
- const env = (await getCloudflareContext()).env;
116
- this.kv = env.NEXT_CACHE_WORKERS_KV;
117
- this.assets = env.ASSETS;
118
- this.initialized = true;
119
- }
120
107
  }
121
108
  export default new Cache();
@@ -1,4 +1,4 @@
1
- import type { ProjectOptions } from "../config.js";
1
+ import type { ProjectOptions } from "../project-options.js";
2
2
  /**
3
3
  * Builds the application in a format that can be passed to workerd
4
4
  *
@@ -1,6 +1,5 @@
1
- import { cpSync } from "node:fs";
2
1
  import { createRequire } from "node:module";
3
- import { dirname, join } from "node:path";
2
+ import { dirname } from "node:path";
4
3
  import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js";
5
4
  import { compileCache } from "@opennextjs/aws/build/compileCache.js";
6
5
  import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
@@ -9,7 +8,6 @@ import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js";
9
8
  import * as buildHelper from "@opennextjs/aws/build/helper.js";
10
9
  import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
11
10
  import logger from "@opennextjs/aws/logger.js";
12
- import { containsDotNextDir, getConfig } from "../config.js";
13
11
  import { bundleServer } from "./bundle-server.js";
14
12
  import { compileEnvFiles } from "./open-next/compile-env-files.js";
15
13
  import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
@@ -52,9 +50,6 @@ export async function build(projectOpts) {
52
50
  setStandaloneBuildMode(options);
53
51
  buildNextjsApp(options);
54
52
  }
55
- if (!containsDotNextDir(projectOpts.sourceDir)) {
56
- throw new Error(`.next folder not found in ${projectOpts.sourceDir}`);
57
- }
58
53
  // Generate deployable bundle
59
54
  printHeader("Generating bundle");
60
55
  buildHelper.initOutputDir(options);
@@ -70,12 +65,7 @@ export async function build(projectOpts) {
70
65
  copyCacheAssets(options);
71
66
  }
72
67
  await createServerBundle(options);
73
- // TODO: drop this copy.
74
- // Copy the .next directory to the output directory so it can be mutated.
75
- cpSync(join(projectOpts.sourceDir, ".next"), join(projectOpts.outputDir, ".next"), { recursive: true });
76
- const projConfig = getConfig(projectOpts);
77
- // TODO: rely on options only.
78
- await bundleServer(projConfig, options);
68
+ await bundleServer(options);
79
69
  if (!projectOpts.skipWranglerConfigCheck) {
80
70
  await createWranglerConfigIfNotExistent(projectOpts);
81
71
  }
@@ -1,13 +1,12 @@
1
- import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
2
- import { Config } from "../config.js";
1
+ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
3
2
  /**
4
3
  * Bundle the Open Next server.
5
4
  */
6
- export declare function bundleServer(config: Config, openNextOptions: BuildOptions): Promise<void>;
5
+ export declare function bundleServer(buildOpts: BuildOptions): Promise<void>;
7
6
  /**
8
7
  * Gets the path of the worker.js file generated by the build process
9
8
  *
10
- * @param openNextOptions the open-next build options
9
+ * @param buildOpts the open-next build options
11
10
  * @returns the path of the worker.js file that the build process generates
12
11
  */
13
- export declare function getOutputWorkerPath(openNextOptions: BuildOptions): string;
12
+ export declare function getOutputWorkerPath(buildOpts: BuildOptions): string;
@@ -3,27 +3,30 @@ import { readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Lang, parse } from "@ast-grep/napi";
6
+ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
6
7
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
7
8
  import { build } from "esbuild";
8
9
  import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
9
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";
10
13
  import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
11
14
  /** The dist directory of the Cloudflare adapter package */
12
15
  const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
13
16
  /**
14
17
  * Bundle the Open Next server.
15
18
  */
16
- export async function bundleServer(config, openNextOptions) {
17
- patches.copyPackageCliFiles(packageDistDir, config, openNextOptions);
18
- const nextConfigStr = fs
19
- .readFileSync(path.join(config.paths.output.standaloneApp, "server.js"), "utf8")
20
- ?.match(/const nextConfig = ({.+?})\n/)?.[1] ?? {};
19
+ export async function bundleServer(buildOpts) {
20
+ patches.copyPackageCliFiles(packageDistDir, buildOpts);
21
+ const { appPath, outputDir, monorepoRoot } = buildOpts;
22
+ const serverFiles = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/required-server-files.json");
23
+ const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
21
24
  console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
22
- patches.patchWranglerDeps(config);
23
- patches.updateWebpackChunksFile(config);
24
- const { appBuildOutputPath, appPath, outputDir, monorepoRoot } = openNextOptions;
25
+ patches.patchWranglerDeps(buildOpts);
26
+ await patches.updateWebpackChunksFile(buildOpts);
27
+ patches.patchVercelOgLibrary(buildOpts);
25
28
  const outputPath = path.join(outputDir, "server-functions", "default");
26
- const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
29
+ const packagePath = getPackagePath(buildOpts);
27
30
  const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
28
31
  const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
29
32
  await build({
@@ -33,25 +36,35 @@ export async function bundleServer(config, openNextOptions) {
33
36
  format: "esm",
34
37
  target: "esnext",
35
38
  minify: false,
36
- plugins: [createFixRequiresESBuildPlugin(config)],
37
- 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
+ ],
38
51
  alias: {
39
52
  // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
40
53
  // eval("require")("bufferutil");
41
54
  // eval("require")("utf-8-validate");
42
- "next/dist/compiled/ws": path.join(config.paths.internal.templates, "shims", "empty.js"),
55
+ "next/dist/compiled/ws": path.join(buildOpts.outputDir, "cloudflare-templates/shims/empty.js"),
43
56
  // Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`:
44
57
  // eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext));
45
58
  // which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63
46
59
  // QUESTION: Why did I encountered this but mhart didn't?
47
- "next/dist/compiled/edge-runtime": path.join(config.paths.internal.templates, "shims", "empty.js"),
60
+ "next/dist/compiled/edge-runtime": path.join(buildOpts.outputDir, "cloudflare-templates/shims/empty.js"),
48
61
  // `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here
49
62
  // source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env
50
- "@next/env": path.join(config.paths.internal.templates, "shims", "env.js"),
63
+ "@next/env": path.join(buildOpts.outputDir, "cloudflare-templates/shims/env.js"),
51
64
  },
52
65
  define: {
53
66
  // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
54
- "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": JSON.stringify(nextConfigStr),
67
+ "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": `${JSON.stringify(nextConfig)}`,
55
68
  // Next.js tried to access __dirname so we need to define it
56
69
  __dirname: '""',
57
70
  // Note: we need the __non_webpack_require__ variable declared as it is used by next-server:
@@ -104,34 +117,28 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
104
117
  `,
105
118
  },
106
119
  });
107
- await updateWorkerBundledCode(openNextServerBundle, config, openNextOptions);
120
+ await updateWorkerBundledCode(openNextServerBundle, buildOpts);
108
121
  const isMonorepo = monorepoRoot !== appPath;
109
122
  if (isMonorepo) {
110
123
  fs.writeFileSync(path.join(outputPath, "handler.mjs"), `export * from "./${normalizePath(packagePath)}/handler.mjs";`);
111
124
  }
112
- console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(openNextOptions)}\` 🚀\n\x1b[0m`);
125
+ console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`);
113
126
  }
114
127
  /**
115
- * This function applies string replacements on the bundled worker code necessary to get it to run in workerd
116
- *
117
- * Needless to say all the logic in this function is something we should avoid as much as possible!
118
- *
119
- * @param workerOutputFile
120
- * @param config
128
+ * This function applies patches required for the code to run on workers.
121
129
  */
122
- async function updateWorkerBundledCode(workerOutputFile, config, openNextOptions) {
130
+ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
123
131
  const code = await readFile(workerOutputFile, "utf8");
124
132
  const patchedCode = await patchCodeWithValidations(code, [
125
133
  ["require", patches.patchRequire],
126
- ["`buildId` function", (code) => patches.patchBuildId(code, config)],
127
- ["`loadManifest` function", (code) => patches.patchLoadManifest(code, config)],
128
- ["next's require", (code) => patches.inlineNextRequire(code, config)],
129
- ["`findDir` function", (code) => patches.patchFindDir(code, config)],
130
- ["`evalManifest` function", (code) => patches.inlineEvalManifest(code, config)],
131
- ["cacheHandler", (code) => patches.patchCache(code, openNextOptions)],
134
+ ["`buildId` function", (code) => patches.patchBuildId(code, buildOpts)],
135
+ ["`loadManifest` function", (code) => patches.patchLoadManifest(code, buildOpts)],
136
+ ["`findDir` function", (code) => patches.patchFindDir(code, buildOpts)],
137
+ ["`evalManifest` function", (code) => patches.inlineEvalManifest(code, buildOpts)],
138
+ ["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
132
139
  [
133
140
  "'require(this.middlewareManifestPath)'",
134
- (code) => patches.inlineMiddlewareManifestRequire(code, config),
141
+ (code) => patches.inlineMiddlewareManifestRequire(code, buildOpts),
135
142
  ],
136
143
  ["exception bubbling", patches.patchExceptionBubbling],
137
144
  ["`loadInstrumentationModule` function", patches.patchLoadInstrumentationModule],
@@ -153,16 +160,16 @@ async function updateWorkerBundledCode(workerOutputFile, config, openNextOptions
153
160
  ],
154
161
  ]);
155
162
  const bundle = parse(Lang.TypeScript, patchedCode).root();
156
- const edits = patchOptionalDependencies(bundle);
163
+ const { edits } = patchOptionalDependencies(bundle);
157
164
  await writeFile(workerOutputFile, bundle.commitEdits(edits));
158
165
  }
159
- function createFixRequiresESBuildPlugin(config) {
166
+ function createFixRequiresESBuildPlugin(options) {
160
167
  return {
161
168
  name: "replaceRelative",
162
169
  setup(build) {
163
170
  // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
164
171
  build.onResolve({ filter: getCrossPlatformPathRegex(String.raw `^\./require-hook$`, { escape: false }) }, () => ({
165
- path: path.join(config.paths.internal.templates, "shims", "empty.js"),
172
+ path: path.join(options.outputDir, "cloudflare-templates/shims/empty.js"),
166
173
  }));
167
174
  },
168
175
  };
@@ -170,9 +177,9 @@ function createFixRequiresESBuildPlugin(config) {
170
177
  /**
171
178
  * Gets the path of the worker.js file generated by the build process
172
179
  *
173
- * @param openNextOptions the open-next build options
180
+ * @param buildOpts the open-next build options
174
181
  * @returns the path of the worker.js file that the build process generates
175
182
  */
176
- export function getOutputWorkerPath(openNextOptions) {
177
- return path.join(openNextOptions.outputDir, "worker.js");
183
+ export function getOutputWorkerPath(buildOpts) {
184
+ return path.join(buildOpts.outputDir, "worker.js");
178
185
  }
@@ -2,4 +2,4 @@ import { BuildOptions } from "@opennextjs/aws/build/helper.js";
2
2
  /**
3
3
  * Compiles the values extracted from the project's env files to the output directory for use in the worker.
4
4
  */
5
- export declare function compileEnvFiles(options: BuildOptions): void;
5
+ export declare function compileEnvFiles(buildOpts: BuildOptions): void;
@@ -4,6 +4,6 @@ import { extractProjectEnvVars } from "../utils/index.js";
4
4
  /**
5
5
  * Compiles the values extracted from the project's env files to the output directory for use in the worker.
6
6
  */
7
- export function compileEnvFiles(options) {
8
- ["production", "development", "test"].forEach((mode) => fs.appendFileSync(path.join(options.outputDir, `.env.mjs`), `export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, options))};\n`));
7
+ export function compileEnvFiles(buildOpts) {
8
+ ["production", "development", "test"].forEach((mode) => fs.appendFileSync(path.join(buildOpts.outputDir, `.env.mjs`), `export const ${mode} = ${JSON.stringify(extractProjectEnvVars(mode, buildOpts))};\n`));
9
9
  }
@@ -1,10 +1,13 @@
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";
10
- export declare function patchOptionalDependencies(root: SgNode): import("@ast-grep/napi").Edit[];
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
+ export declare function patchOptionalDependencies(root: SgNode): {
11
+ edits: import("@ast-grep/napi").Edit[];
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>>[];
13
+ };
@@ -1,6 +1,6 @@
1
- import { getRuleEdits } from "./util.js";
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
@@ -27,5 +27,5 @@ fix: |-
27
27
  }
28
28
  `;
29
29
  export function patchOptionalDependencies(root) {
30
- return getRuleEdits(optionalDepRule, root);
30
+ return applyRule(optionalDepRule, root);
31
31
  }