@opennextjs/cloudflare 0.2.0 → 0.3.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 (100) hide show
  1. package/README.md +48 -38
  2. package/dist/api/{get-cloudflare-context.d.mts → get-cloudflare-context.d.ts} +4 -4
  3. package/dist/api/get-cloudflare-context.js +39 -0
  4. package/dist/api/index.d.ts +1 -0
  5. package/dist/api/index.js +1 -0
  6. package/dist/api/kvCache.d.ts +27 -0
  7. package/dist/api/kvCache.js +121 -0
  8. package/dist/cli/args.d.ts +5 -0
  9. package/dist/cli/args.js +48 -0
  10. package/dist/cli/build/bundle-server.d.ts +6 -0
  11. package/dist/cli/build/bundle-server.js +188 -0
  12. package/dist/cli/build/index.d.ts +9 -0
  13. package/dist/cli/build/index.js +123 -0
  14. package/dist/cli/build/open-next/compile-env-files.d.ts +5 -0
  15. package/dist/cli/build/open-next/compile-env-files.js +9 -0
  16. package/dist/cli/build/open-next/copyCacheAssets.d.ts +2 -0
  17. package/dist/cli/build/open-next/copyCacheAssets.js +10 -0
  18. package/dist/cli/build/open-next/createServerBundle.d.ts +2 -0
  19. package/dist/cli/build/open-next/createServerBundle.js +216 -0
  20. package/dist/cli/build/patches/index.d.ts +2 -0
  21. package/dist/cli/build/patches/index.js +2 -0
  22. package/dist/cli/build/patches/investigated/copy-package-cli-files.d.ts +6 -0
  23. package/dist/cli/build/patches/investigated/copy-package-cli-files.js +12 -0
  24. package/dist/cli/build/patches/investigated/index.d.ts +4 -0
  25. package/dist/cli/build/patches/investigated/index.js +4 -0
  26. package/dist/cli/build/patches/investigated/patch-cache.d.ts +13 -0
  27. package/dist/cli/build/patches/investigated/patch-cache.js +22 -0
  28. package/dist/cli/build/patches/investigated/patch-require.d.ts +7 -0
  29. package/dist/cli/build/patches/investigated/patch-require.js +9 -0
  30. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.d.ts +13 -0
  31. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.js +82 -0
  32. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.test.d.ts +1 -0
  33. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.test.js +20 -0
  34. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.d.ts +19 -0
  35. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.js +76 -0
  36. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.test.d.ts +1 -0
  37. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.test.js +23 -0
  38. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.d.ts +14 -0
  39. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.js +22 -0
  40. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.test.d.ts +1 -0
  41. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.test.js +15 -0
  42. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.d.ts +8 -0
  43. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.js +22 -0
  44. package/dist/cli/build/patches/to-investigate/index.d.ts +8 -0
  45. package/dist/cli/build/patches/to-investigate/index.js +8 -0
  46. package/dist/cli/build/patches/to-investigate/inline-eval-manifest.d.ts +9 -0
  47. package/dist/cli/build/patches/to-investigate/inline-eval-manifest.js +32 -0
  48. package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.d.ts +6 -0
  49. package/dist/cli/build/patches/to-investigate/inline-middleware-manifest-require.js +13 -0
  50. package/dist/cli/build/patches/to-investigate/inline-next-require.d.ts +6 -0
  51. package/dist/cli/build/patches/to-investigate/inline-next-require.js +36 -0
  52. package/dist/cli/build/patches/to-investigate/patch-exception-bubbling.d.ts +7 -0
  53. package/dist/cli/build/patches/to-investigate/patch-exception-bubbling.js +9 -0
  54. package/dist/cli/build/patches/to-investigate/patch-find-dir.d.ts +8 -0
  55. package/dist/cli/build/patches/to-investigate/patch-find-dir.js +21 -0
  56. package/dist/cli/build/patches/to-investigate/patch-load-instrumentation-module.d.ts +14 -0
  57. package/dist/cli/build/patches/to-investigate/patch-load-instrumentation-module.js +34 -0
  58. package/dist/cli/build/patches/to-investigate/patch-read-file.d.ts +3 -0
  59. package/dist/cli/build/patches/to-investigate/patch-read-file.js +29 -0
  60. package/dist/cli/build/patches/to-investigate/wrangler-deps.d.ts +2 -0
  61. package/dist/cli/build/patches/to-investigate/wrangler-deps.js +54 -0
  62. package/dist/cli/build/utils/extract-project-env-vars.d.ts +18 -0
  63. package/dist/cli/build/utils/extract-project-env-vars.js +32 -0
  64. package/dist/cli/build/utils/extract-project-env-vars.spec.d.ts +1 -0
  65. package/dist/cli/build/utils/extract-project-env-vars.spec.js +57 -0
  66. package/dist/cli/build/utils/index.d.ts +3 -0
  67. package/dist/cli/build/utils/index.js +3 -0
  68. package/dist/cli/build/utils/normalize-path.d.ts +1 -0
  69. package/dist/cli/build/utils/normalize-path.js +4 -0
  70. package/dist/cli/build/utils/read-paths-recursively.d.ts +7 -0
  71. package/dist/cli/build/utils/read-paths-recursively.js +20 -0
  72. package/dist/cli/build/utils/ts-parse-file.d.ts +8 -0
  73. package/dist/cli/build/utils/ts-parse-file.js +12 -0
  74. package/dist/cli/config.d.ts +41 -0
  75. package/dist/cli/config.js +92 -0
  76. package/dist/cli/index.d.ts +2 -0
  77. package/dist/cli/index.js +12 -0
  78. package/dist/cli/templates/shims/empty.d.ts +2 -0
  79. package/dist/cli/templates/shims/env.d.ts +1 -0
  80. package/dist/cli/templates/shims/env.js +1 -0
  81. package/dist/cli/templates/shims/node-fs.d.ts +17 -0
  82. package/dist/cli/templates/shims/node-fs.js +51 -0
  83. package/dist/cli/templates/shims/throw.d.ts +0 -0
  84. package/dist/cli/templates/shims/{throw.ts → throw.js} +1 -0
  85. package/dist/cli/templates/worker.d.ts +5 -0
  86. package/dist/cli/templates/worker.js +67 -0
  87. package/package.json +29 -12
  88. package/dist/api/chunk-VTBEIZPQ.mjs +0 -32
  89. package/dist/api/get-cloudflare-context.mjs +0 -6
  90. package/dist/api/index.d.mts +0 -1
  91. package/dist/api/index.mjs +0 -6
  92. package/dist/cli/constants/incremental-cache.ts +0 -8
  93. package/dist/cli/index.mjs +0 -7424
  94. package/dist/cli/templates/cache-handler/index.ts +0 -1
  95. package/dist/cli/templates/cache-handler/open-next-cache-handler.ts +0 -148
  96. package/dist/cli/templates/cache-handler/utils.ts +0 -41
  97. package/dist/cli/templates/shims/env.ts +0 -1
  98. package/dist/cli/templates/shims/node-fs.ts +0 -69
  99. package/dist/cli/templates/worker.ts +0 -156
  100. /package/dist/cli/templates/shims/{empty.ts → empty.js} +0 -0
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Deploy Next.js apps to Cloudflare!
4
4
 
5
- [OpenNext for Cloudflare](https://opennext.js.org/cloudflare) is Cloudflare specific adapter that enables deployment of Next.js applications to Cloudflare.
5
+ [OpenNext for Cloudflare](https://opennext.js.org/cloudflare) is a Cloudflare specific adapter that enables deployment of Next.js applications to Cloudflare.
6
6
 
7
- ## Getting started
7
+ ## Get started
8
8
 
9
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
10
 
@@ -16,9 +16,9 @@ You can use [`create-next-app`](https://nextjs.org/docs/pages/api-reference/cli/
16
16
  npm add -D wrangler@latest @opennextjs/cloudflare
17
17
  # or
18
18
  pnpm add -D wrangler@latest @opennextjs/cloudflare
19
- # or
19
+ # or
20
20
  yarn add -D wrangler@latest @opennextjs/cloudflare
21
- # or
21
+ # or
22
22
  bun add -D wrangler@latest @opennextjs/cloudflare
23
23
  ```
24
24
 
@@ -27,39 +27,49 @@ You can use [`create-next-app`](https://nextjs.org/docs/pages/api-reference/cli/
27
27
  ```toml
28
28
  #:schema node_modules/wrangler/config-schema.json
29
29
  name = "<your-app-name>"
30
- main = ".worker-next/index.mjs"
30
+ main = ".open-next/worker.js"
31
31
 
32
32
  compatibility_date = "2024-09-23"
33
33
  compatibility_flags = ["nodejs_compat"]
34
34
 
35
35
  # Use the new Workers + Assets to host the static frontend files
36
- assets = { directory = ".worker-next/assets", binding = "ASSETS" }
37
- ```
38
-
39
- You can enable Incremental Static Regeneration ([ISR](https://nextjs.org/docs/app/building-your-application/data-fetching/incremental-static-regeneration)) by adding a KV binding named `NEXT_CACHE_WORKERS_KV` to your `wrangler.toml`:
40
-
41
- - Create the binding
42
-
43
- ```bash
44
- npx wrangler kv namespace create NEXT_CACHE_WORKERS_KV
45
- # or
46
- pnpm wrangler kv namespace create NEXT_CACHE_WORKERS_KV
47
- # or
48
- yarn wrangler kv namespace create NEXT_CACHE_WORKERS_KV
49
- # or
50
- bun wrangler kv namespace create NEXT_CACHE_WORKERS_KV
51
- ```
52
-
53
- - Paste the snippet to your `wrangler.toml`:
54
-
55
- ```bash
56
- [[kv_namespaces]]
57
- binding = "NEXT_CACHE_WORKERS_KV"
58
- id = "..."
36
+ assets = { directory = ".open-next/assets", binding = "ASSETS" }
59
37
  ```
60
38
 
61
- > [!WARNING]
62
- > The current support for ISR is limited.
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...
63
73
 
64
74
  ## Local development
65
75
 
@@ -72,13 +82,13 @@ Run the following commands to preview the production build of your application l
72
82
  - build the app and adapt it for Cloudflare
73
83
 
74
84
  ```bash
75
- npx cloudflare
85
+ npx opennextjs-cloudflare
76
86
  # or
77
- pnpm cloudflare
87
+ pnpm opennextjs-cloudflare
78
88
  # or
79
- yarn cloudflare
89
+ yarn opennextjs-cloudflare
80
90
  # or
81
- bun cloudflare
91
+ bun opennextjs-cloudflare
82
92
  ```
83
93
 
84
94
  - Preview the app in Wrangler
@@ -100,11 +110,11 @@ Deploy your application to production with the following:
100
110
  - build the app and adapt it for Cloudflare
101
111
 
102
112
  ```bash
103
- npx cloudflare && npx wrangler deploy
113
+ npx opennextjs-cloudflare && npx wrangler deploy
104
114
  # or
105
- pnpm cloudflare && pnpm wrangler deploy
115
+ pnpm opennextjs-cloudflare && pnpm wrangler deploy
106
116
  # or
107
- yarn cloudflare && yarn wrangler deploy
117
+ yarn opennextjs-cloudflare && yarn wrangler deploy
108
118
  # or
109
- bun cloudflare && bun wrangler deploy
119
+ bun opennextjs-cloudflare && bun wrangler deploy
110
120
  ```
@@ -1,8 +1,10 @@
1
1
  declare global {
2
2
  interface CloudflareEnv {
3
+ NEXT_CACHE_WORKERS_KV?: KVNamespace;
4
+ ASSETS?: Fetcher;
3
5
  }
4
6
  }
5
- type CloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext> = {
7
+ export type CloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext> = {
6
8
  /**
7
9
  * the worker's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
8
10
  */
@@ -21,6 +23,4 @@ type CloudflareContext<CfProperties extends Record<string, unknown> = IncomingRe
21
23
  *
22
24
  * @returns the cloudflare context
23
25
  */
24
- declare function getCloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext>(): Promise<CloudflareContext<CfProperties, Context>>;
25
-
26
- export { type CloudflareContext, getCloudflareContext };
26
+ export declare function getCloudflareContext<CfProperties extends Record<string, unknown> = IncomingRequestCfProperties, Context = ExecutionContext>(): Promise<CloudflareContext<CfProperties, Context>>;
@@ -0,0 +1,39 @@
1
+ // Note: this symbol needs to be kept in sync with the one used in `src/cli/templates/worker.ts`
2
+ const cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
3
+ /**
4
+ * Utility to get the current Cloudflare context
5
+ *
6
+ * @returns the cloudflare context
7
+ */
8
+ export async function getCloudflareContext() {
9
+ const global = globalThis;
10
+ const cloudflareContext = global[cloudflareContextSymbol];
11
+ if (!cloudflareContext) {
12
+ // the cloudflare context is initialized by the worker and is always present in production/preview,
13
+ // so, it not being present means that the application is running under `next dev`
14
+ return getCloudflareContextInNextDev();
15
+ }
16
+ return cloudflareContext;
17
+ }
18
+ const cloudflareContextInNextDevSymbol = Symbol.for("__next-dev/cloudflare-context__");
19
+ /**
20
+ * Gets a local proxy version of the cloudflare context (created using `getPlatformProxy`) when
21
+ * running in the standard next dev server (via `next dev`)
22
+ *
23
+ * @returns the local proxy version of the cloudflare context
24
+ */
25
+ async function getCloudflareContextInNextDev() {
26
+ const global = globalThis;
27
+ if (!global[cloudflareContextInNextDevSymbol]) {
28
+ // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
29
+ const { getPlatformProxy } = await import(
30
+ /* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`);
31
+ const { env, cf, ctx } = await getPlatformProxy();
32
+ global[cloudflareContextInNextDevSymbol] = {
33
+ env,
34
+ cf: cf,
35
+ ctx: ctx,
36
+ };
37
+ }
38
+ return global[cloudflareContextInNextDevSymbol];
39
+ }
@@ -0,0 +1 @@
1
+ export * from "./get-cloudflare-context.js";
@@ -0,0 +1 @@
1
+ export * from "./get-cloudflare-context.js";
@@ -0,0 +1,27 @@
1
+ import type { KVNamespace } from "@cloudflare/workers-types";
2
+ import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
3
+ export declare const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
4
+ export declare const STATUS_DELETED = 1;
5
+ /**
6
+ * Open Next cache based on cloudflare KV and Assets.
7
+ *
8
+ * Note: The class is instantiated outside of the request context.
9
+ * The cloudflare context and process.env are not initialzed yet
10
+ * when the constructor is called.
11
+ */
12
+ declare class Cache implements IncrementalCache {
13
+ readonly name = "cloudflare-kv";
14
+ protected initialized: boolean;
15
+ protected kv: KVNamespace | undefined;
16
+ protected assets: Fetcher | undefined;
17
+ get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>>>;
18
+ set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
19
+ delete(key: string): Promise<void>;
20
+ protected getKVKey(key: string, isFetch?: boolean): string;
21
+ protected getAssetUrl(key: string, isFetch?: boolean): string;
22
+ protected debug(...args: unknown[]): void;
23
+ protected getBuildId(): string;
24
+ protected init(): Promise<void>;
25
+ }
26
+ declare const _default: Cache;
27
+ export default _default;
@@ -0,0 +1,121 @@
1
+ import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
2
+ import { getCloudflareContext } from "./get-cloudflare-context.js";
3
+ export const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
4
+ export const STATUS_DELETED = 1;
5
+ /**
6
+ * Open Next cache based on cloudflare KV and Assets.
7
+ *
8
+ * Note: The class is instantiated outside of the request context.
9
+ * The cloudflare context and process.env are not initialzed yet
10
+ * when the constructor is called.
11
+ */
12
+ class Cache {
13
+ name = "cloudflare-kv";
14
+ initialized = false;
15
+ kv;
16
+ assets;
17
+ async get(key, isFetch) {
18
+ if (!this.initialized) {
19
+ await this.init();
20
+ }
21
+ if (!(this.kv || this.assets)) {
22
+ throw new IgnorableError(`No KVNamespace nor Fetcher`);
23
+ }
24
+ this.debug(`Get ${key}`);
25
+ try {
26
+ let entry = null;
27
+ if (this.kv) {
28
+ this.debug(`- From KV`);
29
+ const kvKey = this.getKVKey(key, isFetch);
30
+ entry = await this.kv.get(kvKey, "json");
31
+ if (entry?.status === STATUS_DELETED) {
32
+ return {};
33
+ }
34
+ }
35
+ if (!entry && this.assets) {
36
+ this.debug(`- From Assets`);
37
+ const url = this.getAssetUrl(key, isFetch);
38
+ const response = await this.assets.fetch(url);
39
+ if (response.ok) {
40
+ // TODO: consider populating KV with the asset value if faster.
41
+ // This could be optional as KV writes are $$.
42
+ // See https://github.com/opennextjs/opennextjs-cloudflare/pull/194#discussion_r1893166026
43
+ entry = {
44
+ value: await response.json(),
45
+ // __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
46
+ lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
47
+ };
48
+ }
49
+ }
50
+ this.debug(entry ? `-> hit` : `-> miss`);
51
+ return { value: entry?.value, lastModified: entry?.lastModified };
52
+ }
53
+ catch {
54
+ throw new RecoverableError(`Failed to get cache [${key}]`);
55
+ }
56
+ }
57
+ async set(key, value, isFetch) {
58
+ if (!this.initialized) {
59
+ await this.init();
60
+ }
61
+ if (!this.kv) {
62
+ throw new IgnorableError(`No KVNamespace`);
63
+ }
64
+ this.debug(`Set ${key}`);
65
+ try {
66
+ const kvKey = this.getKVKey(key, isFetch);
67
+ // Note: We can not set a TTL as we might fallback to assets,
68
+ // still removing old data (old BUILD_ID) could help avoiding
69
+ // the cache growing too big.
70
+ await this.kv.put(kvKey, JSON.stringify({
71
+ value,
72
+ // Note: `Date.now()` returns the time of the last IO rather than the actual time.
73
+ // See https://developers.cloudflare.com/workers/reference/security-model/
74
+ lastModified: Date.now(),
75
+ }));
76
+ }
77
+ catch {
78
+ throw new RecoverableError(`Failed to set cache [${key}]`);
79
+ }
80
+ }
81
+ async delete(key) {
82
+ if (!this.initialized) {
83
+ await this.init();
84
+ }
85
+ if (!this.kv) {
86
+ throw new IgnorableError(`No KVNamespace`);
87
+ }
88
+ this.debug(`Delete ${key}`);
89
+ try {
90
+ const kvKey = this.getKVKey(key, /* isFetch= */ false);
91
+ // Do not delete the key as we would then fallback to the assets.
92
+ await this.kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
93
+ }
94
+ catch {
95
+ throw new RecoverableError(`Failed to delete cache [${key}]`);
96
+ }
97
+ }
98
+ getKVKey(key, isFetch) {
99
+ return `${this.getBuildId()}/${key}.${isFetch ? "fetch" : "cache"}`;
100
+ }
101
+ getAssetUrl(key, isFetch) {
102
+ return isFetch
103
+ ? `http://assets.local/${CACHE_ASSET_DIR}/__fetch/${this.getBuildId()}/${key}`
104
+ : `http://assets.local/${CACHE_ASSET_DIR}/${this.getBuildId()}/${key}.cache`;
105
+ }
106
+ debug(...args) {
107
+ if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
108
+ console.log(`[Cache ${this.name}] `, ...args);
109
+ }
110
+ }
111
+ getBuildId() {
112
+ return process.env.NEXT_BUILD_ID ?? "no-build-id";
113
+ }
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
+ }
121
+ export default new Cache();
@@ -0,0 +1,5 @@
1
+ export declare function getArgs(): {
2
+ skipNextBuild: boolean;
3
+ outputDir?: string;
4
+ minify: boolean;
5
+ };
@@ -0,0 +1,48 @@
1
+ import { mkdirSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ export function getArgs() {
5
+ const { skipBuild, output, noMinify } = parseArgs({
6
+ options: {
7
+ skipBuild: {
8
+ type: "boolean",
9
+ short: "s",
10
+ default: false,
11
+ },
12
+ output: {
13
+ type: "string",
14
+ short: "o",
15
+ },
16
+ noMinify: {
17
+ type: "boolean",
18
+ default: false,
19
+ },
20
+ },
21
+ allowPositionals: false,
22
+ }).values;
23
+ const outputDir = output ? resolve(output) : undefined;
24
+ if (outputDir) {
25
+ assertDirArg(outputDir, "output", true);
26
+ }
27
+ return {
28
+ outputDir,
29
+ skipNextBuild: skipBuild || ["1", "true", "yes"].includes(String(process.env.SKIP_NEXT_APP_BUILD)),
30
+ minify: !noMinify,
31
+ };
32
+ }
33
+ function assertDirArg(path, argName, make) {
34
+ let dirStats;
35
+ try {
36
+ dirStats = statSync(path);
37
+ }
38
+ catch {
39
+ if (!make) {
40
+ throw new Error(`Error: the provided${argName ? ` "${argName}"` : ""} input is not a valid path`);
41
+ }
42
+ mkdirSync(path);
43
+ return;
44
+ }
45
+ if (!dirStats.isDirectory()) {
46
+ throw new Error(`Error: the provided${argName ? ` "${argName}"` : ""} input is not a directory`);
47
+ }
48
+ }
@@ -0,0 +1,6 @@
1
+ import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
2
+ import { Config } from "../config.js";
3
+ /**
4
+ * Bundle the Open Next server.
5
+ */
6
+ export declare function bundleServer(config: Config, openNextOptions: BuildOptions): Promise<void>;
@@ -0,0 +1,188 @@
1
+ import fs from "node:fs";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { build } from "esbuild";
6
+ import * as patches from "./patches/index.js";
7
+ /** The dist directory of the Cloudflare adapter package */
8
+ const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
9
+ /**
10
+ * Bundle the Open Next server.
11
+ */
12
+ export async function bundleServer(config, openNextOptions) {
13
+ patches.copyPackageCliFiles(packageDistDir, config, openNextOptions);
14
+ const nextConfigStr = fs
15
+ .readFileSync(path.join(config.paths.output.standaloneApp, "server.js"), "utf8")
16
+ ?.match(/const nextConfig = ({.+?})\n/)?.[1] ?? {};
17
+ console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
18
+ patches.patchWranglerDeps(config);
19
+ patches.updateWebpackChunksFile(config);
20
+ const { appBuildOutputPath, appPath, outputDir, monorepoRoot } = openNextOptions;
21
+ const outputPath = path.join(outputDir, "server-functions", "default");
22
+ const packagePath = path.relative(monorepoRoot, appBuildOutputPath);
23
+ const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
24
+ const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
25
+ await build({
26
+ entryPoints: [openNextServer],
27
+ bundle: true,
28
+ outfile: openNextServerBundle,
29
+ format: "esm",
30
+ target: "esnext",
31
+ minify: false,
32
+ plugins: [createFixRequiresESBuildPlugin(config)],
33
+ external: ["./middleware/handler.mjs"],
34
+ alias: {
35
+ // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
36
+ // eval("require")("bufferutil");
37
+ // eval("require")("utf-8-validate");
38
+ "next/dist/compiled/ws": path.join(config.paths.internal.templates, "shims", "empty.js"),
39
+ // Note: we apply an empty shim to next/dist/compiled/edge-runtime since (amongst others) it generated the following `eval`:
40
+ // eval(getModuleCode)(module, module.exports, throwingRequire, params.context, ...Object.values(params.scopedContext));
41
+ // which comes from https://github.com/vercel/edge-runtime/blob/6e96b55f/packages/primitives/src/primitives/load.js#L57-L63
42
+ // QUESTION: Why did I encountered this but mhart didn't?
43
+ "next/dist/compiled/edge-runtime": path.join(config.paths.internal.templates, "shims", "empty.js"),
44
+ // `@next/env` is a library Next.js uses for loading dotenv files, for obvious reasons we need to stub it here
45
+ // source: https://github.com/vercel/next.js/tree/0ac10d79720/packages/next-env
46
+ "@next/env": path.join(config.paths.internal.templates, "shims", "env.js"),
47
+ },
48
+ define: {
49
+ // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
50
+ "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": JSON.stringify(nextConfigStr),
51
+ // Next.js tried to access __dirname so we need to define it
52
+ __dirname: '""',
53
+ // Note: we need the __non_webpack_require__ variable declared as it is used by next-server:
54
+ // https://github.com/vercel/next.js/blob/be0c3283/packages/next/src/server/next-server.ts#L116-L119
55
+ __non_webpack_require__: "require",
56
+ // Ask mhart if he can explain why the `define`s below are necessary
57
+ "process.env.NEXT_RUNTIME": '"nodejs"',
58
+ "process.env.NODE_ENV": '"production"',
59
+ "process.env.NEXT_MINIMAL": "true",
60
+ },
61
+ // We need to set platform to node so that esbuild doesn't complain about the node imports
62
+ platform: "node",
63
+ banner: {
64
+ js: `
65
+ // __dirname is used by unbundled js files (which don't inherit the __dirname present in the define field)
66
+ // so we also need to set it on the global scope
67
+ // Note: this was hit in the next/dist/compiled/@opentelemetry/api module
68
+ globalThis.__dirname ??= "";
69
+
70
+ // Do not crash on cache not supported
71
+ // https://github.com/cloudflare/workerd/pull/2434
72
+ // compatibility flag "cache_option_enabled" -> does not support "force-cache"
73
+ const curFetch = globalThis.fetch;
74
+ globalThis.fetch = (input, init) => {
75
+ if (init) {
76
+ delete init.cache;
77
+ }
78
+ return curFetch(input, init);
79
+ };
80
+ import __cf_stream from 'node:stream';
81
+ fetch = globalThis.fetch;
82
+ const CustomRequest = class extends globalThis.Request {
83
+ constructor(input, init) {
84
+ if (init) {
85
+ init = {
86
+ ...init,
87
+ cache: undefined,
88
+ // https://github.com/cloudflare/workerd/issues/2746
89
+ // https://github.com/cloudflare/workerd/issues/3245
90
+ body: init.body instanceof __cf_stream.Readable ? ReadableStream.from(init.body) : init.body,
91
+ };
92
+ }
93
+ super(input, init);
94
+ }
95
+ };
96
+ globalThis.Request = CustomRequest;
97
+ Request = globalThis.Request;
98
+ // Makes the edge converter returns either a Response or a Request.
99
+ globalThis.__dangerous_ON_edge_converter_returns_request = true;
100
+ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
101
+ `,
102
+ },
103
+ });
104
+ await updateWorkerBundledCode(openNextServerBundle, config, openNextOptions);
105
+ const isMonorepo = monorepoRoot !== appPath;
106
+ if (isMonorepo) {
107
+ const packagePosixPath = packagePath.split(path.sep).join(path.posix.sep);
108
+ fs.writeFileSync(path.join(outputPath, "handler.mjs"), `export * from "./${packagePosixPath}/handler.mjs";`);
109
+ }
110
+ console.log(`\x1b[35mWorker saved in \`${openNextServerBundle}\` 🚀\n\x1b[0m`);
111
+ }
112
+ /**
113
+ * This function applies string replacements on the bundled worker code necessary to get it to run in workerd
114
+ *
115
+ * Needless to say all the logic in this function is something we should avoid as much as possible!
116
+ *
117
+ * @param workerOutputFile
118
+ * @param config
119
+ */
120
+ async function updateWorkerBundledCode(workerOutputFile, config, openNextOptions) {
121
+ const code = await readFile(workerOutputFile, "utf8");
122
+ const patchedCode = await patchCodeWithValidations(code, [
123
+ ["require", patches.patchRequire],
124
+ ["`buildId` function", (code) => patches.patchBuildId(code, config)],
125
+ ["`loadManifest` function", (code) => patches.patchLoadManifest(code, config)],
126
+ ["next's require", (code) => patches.inlineNextRequire(code, config)],
127
+ ["`findDir` function", (code) => patches.patchFindDir(code, config)],
128
+ ["`evalManifest` function", (code) => patches.inlineEvalManifest(code, config)],
129
+ ["cacheHandler", (code) => patches.patchCache(code, openNextOptions)],
130
+ [
131
+ "'require(this.middlewareManifestPath)'",
132
+ (code) => patches.inlineMiddlewareManifestRequire(code, config),
133
+ ],
134
+ ["exception bubbling", patches.patchExceptionBubbling],
135
+ ["`loadInstrumentationModule` function", patches.patchLoadInstrumentationModule],
136
+ [
137
+ "`patchAsyncStorage` call",
138
+ (code) => code
139
+ // TODO: implement for cf (possibly in @opennextjs/aws)
140
+ .replace("patchAsyncStorage();", "//patchAsyncStorage();"),
141
+ ],
142
+ [
143
+ '`eval("require")` calls',
144
+ (code) => code.replaceAll('eval("require")', "require"),
145
+ { isOptional: true },
146
+ ],
147
+ [
148
+ "`require.resolve` call",
149
+ // workers do not support dynamic require nor require.resolve
150
+ (code) => code.replace('require.resolve("./cache.cjs")', '"unused"'),
151
+ ],
152
+ ]);
153
+ await writeFile(workerOutputFile, patchedCode);
154
+ }
155
+ function createFixRequiresESBuildPlugin(config) {
156
+ return {
157
+ name: "replaceRelative",
158
+ setup(build) {
159
+ // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
160
+ build.onResolve({ filter: /^\.\/require-hook$/ }, () => ({
161
+ path: path.join(config.paths.internal.templates, "shims", "empty.js"),
162
+ }));
163
+ },
164
+ };
165
+ }
166
+ /**
167
+ * Applies multiple code patches in order to a given piece of code, at each step it validates that the code
168
+ * has actually been patched/changed, if not an error is thrown
169
+ *
170
+ * @param code the code to apply the patches to
171
+ * @param patches array of tuples, containing a string indicating the target of the patching (for logging) and
172
+ * a patching function that takes a string (pre-patch code) and returns a string (post-patch code)
173
+ * @returns the patched code
174
+ */
175
+ async function patchCodeWithValidations(code, patches) {
176
+ console.log(`Applying code patches:`);
177
+ let patchedCode = code;
178
+ for (const [target, patchFunction, opts] of patches) {
179
+ console.log(` - patching ${target}`);
180
+ const prePatchCode = patchedCode;
181
+ patchedCode = await patchFunction(patchedCode);
182
+ if (!opts?.isOptional && prePatchCode === patchedCode) {
183
+ throw new Error(`Failed to patch ${target}`);
184
+ }
185
+ }
186
+ console.log(`All ${patches.length} patches applied\n`);
187
+ return patchedCode;
188
+ }
@@ -0,0 +1,9 @@
1
+ import type { ProjectOptions } from "../config.js";
2
+ /**
3
+ * Builds the application in a format that can be passed to workerd
4
+ *
5
+ * It saves the output in a `.worker-next` directory
6
+ *
7
+ * @param projectOpts The options for the project
8
+ */
9
+ export declare function build(projectOpts: ProjectOptions): Promise<void>;