@opennextjs/cloudflare 0.3.9 → 0.4.0

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 (61) 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 +96 -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 +39 -38
  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/open-next/createServerBundle.js +3 -2
  15. package/dist/cli/build/patches/ast/optional-deps.d.ts +13 -0
  16. package/dist/cli/build/patches/ast/optional-deps.js +31 -0
  17. package/dist/cli/build/patches/ast/optional-deps.spec.d.ts +1 -0
  18. package/dist/cli/build/patches/ast/optional-deps.spec.js +89 -0
  19. package/dist/cli/build/patches/ast/util.d.ts +50 -0
  20. package/dist/cli/build/patches/ast/util.js +65 -0
  21. package/dist/cli/build/patches/ast/util.spec.d.ts +1 -0
  22. package/dist/cli/build/patches/ast/util.spec.js +43 -0
  23. package/dist/cli/build/patches/ast/vercel-og.d.ts +23 -0
  24. package/dist/cli/build/patches/ast/vercel-og.js +58 -0
  25. package/dist/cli/build/patches/ast/vercel-og.spec.d.ts +1 -0
  26. package/dist/cli/build/patches/ast/vercel-og.spec.js +22 -0
  27. package/dist/cli/build/patches/investigated/copy-package-cli-files.d.ts +4 -3
  28. package/dist/cli/build/patches/investigated/copy-package-cli-files.js +8 -5
  29. package/dist/cli/build/patches/investigated/index.d.ts +1 -0
  30. package/dist/cli/build/patches/investigated/index.js +1 -0
  31. package/dist/cli/build/patches/investigated/patch-cache.d.ts +2 -2
  32. package/dist/cli/build/patches/investigated/patch-cache.js +7 -6
  33. package/dist/cli/build/patches/investigated/patch-require.d.ts +1 -4
  34. package/dist/cli/build/patches/investigated/patch-require.js +1 -4
  35. package/dist/cli/build/patches/investigated/patch-vercel-og-library.d.ts +7 -0
  36. package/dist/cli/build/patches/investigated/patch-vercel-og-library.js +39 -0
  37. package/dist/cli/build/patches/investigated/patch-vercel-og-library.spec.d.ts +1 -0
  38. package/dist/cli/build/patches/investigated/patch-vercel-og-library.spec.js +50 -0
  39. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.d.ts +2 -5
  40. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.js +6 -6
  41. package/dist/cli/build/patches/to-investigate/inline-eval-manifest.d.ts +2 -2
  42. package/dist/cli/build/patches/to-investigate/inline-eval-manifest.js +21 -17
  43. package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.d.ts +2 -2
  44. package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.js +4 -2
  45. package/dist/cli/build/patches/to-investigate/inline-next-require.d.ts +2 -2
  46. package/dist/cli/build/patches/to-investigate/inline-next-require.js +16 -12
  47. package/dist/cli/build/patches/to-investigate/patch-find-dir.d.ts +3 -6
  48. package/dist/cli/build/patches/to-investigate/patch-find-dir.js +11 -11
  49. package/dist/cli/build/patches/to-investigate/patch-read-file.d.ts +3 -3
  50. package/dist/cli/build/patches/to-investigate/patch-read-file.js +15 -16
  51. package/dist/cli/build/patches/to-investigate/wrangler-deps.d.ts +2 -2
  52. package/dist/cli/build/patches/to-investigate/wrangler-deps.js +36 -67
  53. package/dist/cli/build/utils/create-config-files.d.ts +1 -1
  54. package/dist/cli/build/utils/create-config-files.js +2 -2
  55. package/dist/cli/project-options.d.ts +7 -0
  56. package/dist/cli/project-options.js +1 -0
  57. package/package.json +8 -6
  58. package/templates/defaults/wrangler.json +1 -0
  59. package/dist/api/get-cloudflare-context.js +0 -42
  60. package/dist/cli/config.d.ts +0 -42
  61. 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,96 @@
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 context = await getCloudflareContextFromWrangler();
45
+ addCloudflareContextToNodejsGlobal(context);
46
+ await monkeyPatchVmModuleEdgeContext(context);
47
+ }
48
+ /**
49
+ * Adds the cloudflare context to the global scope in which the Next.js dev node.js process runs in, enabling
50
+ * future calls to `getCloudflareContext` to retrieve and return such context
51
+ *
52
+ * @param cloudflareContext the cloudflare context to add to the node.sj global scope
53
+ */
54
+ function addCloudflareContextToNodejsGlobal(cloudflareContext) {
55
+ const global = globalThis;
56
+ global[cloudflareContextSymbol] = cloudflareContext;
57
+ }
58
+ /**
59
+ * Next.js uses the Node.js vm module's `runInContext()` function to evaluate edge functions
60
+ * in a runtime context that tries to simulate as accurately as possible the actual production runtime
61
+ * behavior, see: https://github.com/vercel/next.js/blob/9a1cd3/packages/next/src/server/web/sandbox/context.ts#L525-L527
62
+ *
63
+ * This function monkey-patches the Node.js `vm` module to override the `runInContext()` function so that the
64
+ * cloudflare context is added to the runtime context's global scope before edge functions are evaluated
65
+ *
66
+ * @param cloudflareContext the cloudflare context to patch onto the "edge" runtime context global scope
67
+ */
68
+ async function monkeyPatchVmModuleEdgeContext(cloudflareContext) {
69
+ const require = (await import(/* webpackIgnore: true */ `${"__module".replaceAll("_", "")}`)).default.createRequire(import.meta.url);
70
+ // eslint-disable-next-line unicorn/prefer-node-protocol -- the `next dev` compiler doesn't accept the node prefix
71
+ const vmModule = require("vm");
72
+ const originalRunInContext = vmModule.runInContext.bind(vmModule);
73
+ vmModule.runInContext = (code, contextifiedObject, options) => {
74
+ const runtimeContext = contextifiedObject;
75
+ runtimeContext[cloudflareContextSymbol] ??= cloudflareContext;
76
+ return originalRunInContext(code, contextifiedObject, options);
77
+ };
78
+ }
79
+ /**
80
+ * Gets a cloudflare context object from wrangler
81
+ *
82
+ * @returns the cloudflare context ready for use
83
+ */
84
+ async function getCloudflareContextFromWrangler() {
85
+ // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
86
+ const { getPlatformProxy } = await import(/* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`);
87
+ const { env, cf, ctx } = await getPlatformProxy({
88
+ // This allows the selection of a wrangler environment while running in next dev mode
89
+ environment: process.env.NEXT_DEV_WRANGLER_ENV,
90
+ });
91
+ return {
92
+ env,
93
+ cf: cf,
94
+ ctx: ctx,
95
+ };
96
+ }
@@ -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;
@@ -2,7 +2,11 @@ import fs from "node:fs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { Lang, parse } from "@ast-grep/napi";
6
+ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
7
+ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
5
8
  import { build } from "esbuild";
9
+ import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
6
10
  import * as patches from "./patches/index.js";
7
11
  import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
8
12
  /** The dist directory of the Cloudflare adapter package */
@@ -10,17 +14,17 @@ const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "
10
14
  /**
11
15
  * Bundle the Open Next server.
12
16
  */
13
- export async function bundleServer(config, openNextOptions) {
14
- patches.copyPackageCliFiles(packageDistDir, config, openNextOptions);
15
- const nextConfigStr = fs
16
- .readFileSync(path.join(config.paths.output.standaloneApp, "server.js"), "utf8")
17
- ?.match(/const nextConfig = ({.+?})\n/)?.[1] ?? {};
17
+ export async function bundleServer(buildOpts) {
18
+ patches.copyPackageCliFiles(packageDistDir, buildOpts);
19
+ const { appPath, outputDir, monorepoRoot } = buildOpts;
20
+ const serverFiles = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/required-server-files.json");
21
+ const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
18
22
  console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
19
- patches.patchWranglerDeps(config);
20
- patches.updateWebpackChunksFile(config);
21
- const { appBuildOutputPath, appPath, outputDir, monorepoRoot } = openNextOptions;
23
+ patches.patchWranglerDeps(buildOpts);
24
+ await patches.updateWebpackChunksFile(buildOpts);
25
+ patches.patchVercelOgLibrary(buildOpts);
22
26
  const outputPath = path.join(outputDir, "server-functions", "default");
23
- const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
27
+ const packagePath = getPackagePath(buildOpts);
24
28
  const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
25
29
  const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
26
30
  await build({
@@ -30,25 +34,25 @@ export async function bundleServer(config, openNextOptions) {
30
34
  format: "esm",
31
35
  target: "esnext",
32
36
  minify: false,
33
- plugins: [createFixRequiresESBuildPlugin(config)],
34
- external: ["./middleware/handler.mjs"],
37
+ plugins: [createFixRequiresESBuildPlugin(buildOpts)],
38
+ external: ["./middleware/handler.mjs", "caniuse-lite"],
35
39
  alias: {
36
40
  // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
37
41
  // eval("require")("bufferutil");
38
42
  // eval("require")("utf-8-validate");
39
- "next/dist/compiled/ws": path.join(config.paths.internal.templates, "shims", "empty.js"),
43
+ "next/dist/compiled/ws": path.join(buildOpts.outputDir, "cloudflare-templates/shims/empty.js"),
40
44
  // Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`:
41
45
  // eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext));
42
46
  // which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63
43
47
  // QUESTION: Why did I encountered this but mhart didn't?
44
- "next/dist/compiled/edge-runtime": path.join(config.paths.internal.templates, "shims", "empty.js"),
48
+ "next/dist/compiled/edge-runtime": path.join(buildOpts.outputDir, "cloudflare-templates/shims/empty.js"),
45
49
  // `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here
46
50
  // source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env
47
- "@next/env": path.join(config.paths.internal.templates, "shims", "env.js"),
51
+ "@next/env": path.join(buildOpts.outputDir, "cloudflare-templates/shims/env.js"),
48
52
  },
49
53
  define: {
50
54
  // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
51
- "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": JSON.stringify(nextConfigStr),
55
+ "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": `${JSON.stringify(nextConfig)}`,
52
56
  // Next.js tried to access __dirname so we need to define it
53
57
  __dirname: '""',
54
58
  // Note: we need the __non_webpack_require__ variable declared as it is used by next-server:
@@ -101,34 +105,29 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
101
105
  `,
102
106
  },
103
107
  });
104
- await updateWorkerBundledCode(openNextServerBundle, config, openNextOptions);
108
+ await updateWorkerBundledCode(openNextServerBundle, buildOpts);
105
109
  const isMonorepo = monorepoRoot !== appPath;
106
110
  if (isMonorepo) {
107
111
  fs.writeFileSync(path.join(outputPath, "handler.mjs"), `export * from "./${normalizePath(packagePath)}/handler.mjs";`);
108
112
  }
109
- console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(openNextOptions)}\` 🚀\n\x1b[0m`);
113
+ console.log(`\x1b[35mWorker saved in \`${getOutputWorkerPath(buildOpts)}\` 🚀\n\x1b[0m`);
110
114
  }
111
115
  /**
112
- * This function applies string replacements on the bundled worker code necessary to get it to run in workerd
113
- *
114
- * Needless to say all the logic in this function is something we should avoid as much as possible!
115
- *
116
- * @param workerOutputFile
117
- * @param config
116
+ * This function applies patches required for the code to run on workers.
118
117
  */
119
- async function updateWorkerBundledCode(workerOutputFile, config, openNextOptions) {
118
+ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
120
119
  const code = await readFile(workerOutputFile, "utf8");
121
120
  const patchedCode = await patchCodeWithValidations(code, [
122
121
  ["require", patches.patchRequire],
123
- ["`buildId` function", (code) => patches.patchBuildId(code, config)],
124
- ["`loadManifest` function", (code) => patches.patchLoadManifest(code, config)],
125
- ["next's require", (code) => patches.inlineNextRequire(code, config)],
126
- ["`findDir` function", (code) => patches.patchFindDir(code, config)],
127
- ["`evalManifest` function", (code) => patches.inlineEvalManifest(code, config)],
128
- ["cacheHandler", (code) => patches.patchCache(code, openNextOptions)],
122
+ ["`buildId` function", (code) => patches.patchBuildId(code, buildOpts)],
123
+ ["`loadManifest` function", (code) => patches.patchLoadManifest(code, buildOpts)],
124
+ ["next's require", (code) => patches.inlineNextRequire(code, buildOpts)],
125
+ ["`findDir` function", (code) => patches.patchFindDir(code, buildOpts)],
126
+ ["`evalManifest` function", (code) => patches.inlineEvalManifest(code, buildOpts)],
127
+ ["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
129
128
  [
130
129
  "'require(this.middlewareManifestPath)'",
131
- (code) => patches.inlineMiddlewareManifestRequire(code, config),
130
+ (code) => patches.inlineMiddlewareManifestRequire(code, buildOpts),
132
131
  ],
133
132
  ["exception bubbling", patches.patchExceptionBubbling],
134
133
  ["`loadInstrumentationModule` function", patches.patchLoadInstrumentationModule],
@@ -149,15 +148,17 @@ async function updateWorkerBundledCode(workerOutputFile, config, openNextOptions
149
148
  (code) => code.replace('require.resolve("./cache.cjs")', '"unused"'),
150
149
  ],
151
150
  ]);
152
- await writeFile(workerOutputFile, patchedCode);
151
+ const bundle = parse(Lang.TypeScript, patchedCode).root();
152
+ const { edits } = patchOptionalDependencies(bundle);
153
+ await writeFile(workerOutputFile, bundle.commitEdits(edits));
153
154
  }
154
- function createFixRequiresESBuildPlugin(config) {
155
+ function createFixRequiresESBuildPlugin(options) {
155
156
  return {
156
157
  name: "replaceRelative",
157
158
  setup(build) {
158
159
  // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
159
- build.onResolve({ filter: /^\.(\/|\\)require-hook$/ }, () => ({
160
- path: path.join(config.paths.internal.templates, "shims", "empty.js"),
160
+ build.onResolve({ filter: getCrossPlatformPathRegex(String.raw `^\./require-hook$`, { escape: false }) }, () => ({
161
+ path: path.join(options.outputDir, "cloudflare-templates/shims/empty.js"),
161
162
  }));
162
163
  },
163
164
  };
@@ -165,9 +166,9 @@ function createFixRequiresESBuildPlugin(config) {
165
166
  /**
166
167
  * Gets the path of the worker.js file generated by the build process
167
168
  *
168
- * @param openNextOptions the open-next build options
169
+ * @param buildOpts the open-next build options
169
170
  * @returns the path of the worker.js file that the build process generates
170
171
  */
171
- export function getOutputWorkerPath(openNextOptions) {
172
- return path.join(openNextOptions.outputDir, "worker.js");
172
+ export function getOutputWorkerPath(buildOpts) {
173
+ return path.join(buildOpts.outputDir, "worker.js");
173
174
  }
@@ -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
  }
@@ -13,6 +13,7 @@ import { minifyAll } from "@opennextjs/aws/minimize-js.js";
13
13
  import { openNextEdgePlugins } from "@opennextjs/aws/plugins/edge.js";
14
14
  import { openNextReplacementPlugin } from "@opennextjs/aws/plugins/replacement.js";
15
15
  import { openNextResolvePlugin } from "@opennextjs/aws/plugins/resolve.js";
16
+ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
16
17
  import { normalizePath } from "../utils/index.js";
17
18
  export async function createServerBundle(options) {
18
19
  const { config } = options;
@@ -117,7 +118,7 @@ async function generateBundle(name, options, fnOptions) {
117
118
  const plugins = [
118
119
  openNextReplacementPlugin({
119
120
  name: `requestHandlerOverride ${name}`,
120
- target: /core(\/|\\)requestHandler\.js/g,
121
+ target: getCrossPlatformPathRegex("core/requestHandler.js"),
121
122
  deletes: [
122
123
  ...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []),
123
124
  ...(disableRouting ? ["withRouting"] : []),
@@ -125,7 +126,7 @@ async function generateBundle(name, options, fnOptions) {
125
126
  }),
126
127
  openNextReplacementPlugin({
127
128
  name: `utilOverride ${name}`,
128
- target: /core(\/|\\)util\.js/g,
129
+ target: getCrossPlatformPathRegex("core/util.js"),
129
130
  deletes: [
130
131
  ...(disableNextPrebundledReact ? ["requireHooks"] : []),
131
132
  ...(isBefore13413 ? ["trustHostHeader"] : ["requestHandlerHost"]),
@@ -0,0 +1,13 @@
1
+ import { type SgNode } from "@ast-grep/napi";
2
+ /**
3
+ * Handle optional dependencies.
4
+ *
5
+ * A top level `require(optionalDep)` would throw when the dep is not installed.
6
+ *
7
+ * So we wrap `require(optionalDep)` in a try/catch (if not already present).
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): {
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
+ };
@@ -0,0 +1,31 @@
1
+ import { applyRule } from "./util.js";
2
+ /**
3
+ * Handle optional dependencies.
4
+ *
5
+ * A top level `require(optionalDep)` would throw when the dep is not installed.
6
+ *
7
+ * So we wrap `require(optionalDep)` in a try/catch (if not already present).
8
+ */
9
+ export const optionalDepRule = `
10
+ rule:
11
+ pattern: $$$LHS = require($$$REQ)
12
+ has:
13
+ pattern: $MOD
14
+ kind: string_fragment
15
+ stopBy: end
16
+ regex: ^caniuse-lite(/|$)
17
+ not:
18
+ inside:
19
+ kind: try_statement
20
+ stopBy: end
21
+
22
+ fix: |-
23
+ try {
24
+ $$$LHS = require($$$REQ);
25
+ } catch {
26
+ throw new Error('The optional dependency "$MOD" is not installed');
27
+ }
28
+ `;
29
+ export function patchOptionalDependencies(root) {
30
+ return applyRule(optionalDepRule, root);
31
+ }
@@ -0,0 +1 @@
1
+ export {};