@opennextjs/cloudflare 0.4.6 → 0.4.8

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 (48) hide show
  1. package/dist/api/kv-cache.d.ts +22 -0
  2. package/dist/api/kv-cache.js +123 -0
  3. package/dist/api/kvCache.d.ts +3 -20
  4. package/dist/api/kvCache.js +3 -106
  5. package/dist/cli/build/bundle-server.js +12 -17
  6. package/dist/cli/build/open-next/copyCacheAssets.js +1 -1
  7. package/dist/cli/build/patches/ast/webpack-runtime.d.ts +23 -0
  8. package/dist/cli/build/patches/ast/webpack-runtime.js +57 -0
  9. package/dist/cli/build/patches/ast/webpack-runtime.spec.js +57 -0
  10. package/dist/cli/build/patches/investigated/index.d.ts +0 -1
  11. package/dist/cli/build/patches/investigated/index.js +0 -1
  12. package/dist/cli/build/patches/plugins/content-updater.d.ts +34 -0
  13. package/dist/cli/build/patches/plugins/content-updater.js +52 -0
  14. package/dist/cli/build/patches/plugins/load-instrumentation.d.ts +6 -0
  15. package/dist/cli/build/patches/plugins/load-instrumentation.js +20 -0
  16. package/dist/cli/build/patches/plugins/load-instrumentation.spec.js +45 -0
  17. package/dist/cli/build/patches/plugins/require-hook.d.ts +3 -0
  18. package/dist/cli/build/patches/plugins/require-hook.js +13 -0
  19. package/dist/cli/build/patches/plugins/require-page.d.ts +2 -5
  20. package/dist/cli/build/patches/plugins/require-page.js +30 -29
  21. package/dist/cli/build/patches/plugins/require.d.ts +2 -5
  22. package/dist/cli/build/patches/plugins/require.js +36 -43
  23. package/dist/cli/build/patches/to-investigate/index.d.ts +0 -1
  24. package/dist/cli/build/patches/to-investigate/index.js +0 -1
  25. package/dist/cli/build/utils/ensure-cf-config.js +7 -2
  26. package/dist/cli/build/utils/index.d.ts +0 -1
  27. package/dist/cli/build/utils/index.js +0 -1
  28. package/dist/cli/utils/ask-confirmation.js +2 -1
  29. package/package.json +3 -4
  30. package/templates/defaults/open-next.config.ts +1 -1
  31. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.d.ts +0 -13
  32. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.js +0 -82
  33. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.test.js +0 -20
  34. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.d.ts +0 -19
  35. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.js +0 -84
  36. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.test.js +0 -25
  37. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.d.ts +0 -14
  38. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.js +0 -22
  39. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.test.d.ts +0 -1
  40. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/get-updated-webpack-chunks-file-content.test.js +0 -15
  41. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.d.ts +0 -5
  42. package/dist/cli/build/patches/investigated/update-webpack-chunks-file/index.js +0 -22
  43. package/dist/cli/build/patches/to-investigate/patch-load-instrumentation-module.d.ts +0 -14
  44. package/dist/cli/build/patches/to-investigate/patch-load-instrumentation-module.js +0 -34
  45. package/dist/cli/build/utils/ts-parse-file.d.ts +0 -8
  46. package/dist/cli/build/utils/ts-parse-file.js +0 -12
  47. /package/dist/cli/build/patches/{investigated/update-webpack-chunks-file/get-chunk-installation-identifiers.test.d.ts → ast/webpack-runtime.spec.d.ts} +0 -0
  48. /package/dist/cli/build/patches/{investigated/update-webpack-chunks-file/get-file-content-with-updated-webpack-f-require-code.test.d.ts → plugins/load-instrumentation.spec.d.ts} +0 -0
@@ -0,0 +1,22 @@
1
+ import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
2
+ export declare const CACHE_ASSET_DIR = "cdn-cgi/_next_cache";
3
+ export declare const STATUS_DELETED = 1;
4
+ /**
5
+ * Open Next cache based on cloudflare KV and Assets.
6
+ *
7
+ * Note: The class is instantiated outside of the request context.
8
+ * The cloudflare context and process.env are not initialzed yet
9
+ * when the constructor is called.
10
+ */
11
+ declare class Cache implements IncrementalCache {
12
+ readonly name = "cloudflare-kv";
13
+ get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>> | null>;
14
+ set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
15
+ delete(key: string): Promise<void>;
16
+ protected getKVKey(key: string, isFetch?: boolean): string;
17
+ protected getAssetUrl(key: string, isFetch?: boolean): string;
18
+ protected debug(...args: unknown[]): void;
19
+ protected getBuildId(): string;
20
+ }
21
+ declare const _default: Cache;
22
+ export default _default;
@@ -0,0 +1,123 @@
1
+ import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
2
+ import { getCloudflareContext } from "./cloudflare-context.js";
3
+ export const CACHE_ASSET_DIR = "cdn-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
+ async get(key, isFetch) {
15
+ const cfEnv = getCloudflareContext().env;
16
+ const kv = cfEnv.NEXT_CACHE_WORKERS_KV;
17
+ const assets = cfEnv.ASSETS;
18
+ if (!(kv || assets)) {
19
+ throw new IgnorableError(`No KVNamespace nor Fetcher`);
20
+ }
21
+ this.debug(`Get ${key}`);
22
+ try {
23
+ let entry = null;
24
+ if (kv) {
25
+ this.debug(`- From KV`);
26
+ const kvKey = this.getKVKey(key, isFetch);
27
+ entry = await kv.get(kvKey, "json");
28
+ if (entry?.status === STATUS_DELETED) {
29
+ return null;
30
+ }
31
+ }
32
+ if (!entry && assets) {
33
+ this.debug(`- From Assets`);
34
+ const url = this.getAssetUrl(key, isFetch);
35
+ const response = await assets.fetch(url);
36
+ if (response.ok) {
37
+ // TODO: consider populating KV with the asset value if faster.
38
+ // This could be optional as KV writes are $$.
39
+ // See https://github.com/opennextjs/opennextjs-cloudflare/pull/194#discussion_r1893166026
40
+ entry = {
41
+ value: await response.json(),
42
+ // __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
43
+ lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
44
+ };
45
+ }
46
+ if (!kv) {
47
+ // The cache can not be updated when there is no KV
48
+ // As we don't want to keep serving stale data for ever,
49
+ // we pretend the entry is not in cache
50
+ if (entry?.value &&
51
+ "kind" in entry.value &&
52
+ entry.value.kind === "FETCH" &&
53
+ entry.value.data?.headers?.expires) {
54
+ const expiresTime = new Date(entry.value.data.headers.expires).getTime();
55
+ if (!isNaN(expiresTime) && expiresTime <= Date.now()) {
56
+ this.debug(`found expired entry (expire time: ${entry.value.data.headers.expires})`);
57
+ return null;
58
+ }
59
+ }
60
+ }
61
+ }
62
+ this.debug(entry ? `-> hit` : `-> miss`);
63
+ return { value: entry?.value, lastModified: entry?.lastModified };
64
+ }
65
+ catch {
66
+ throw new RecoverableError(`Failed to get cache [${key}]`);
67
+ }
68
+ }
69
+ async set(key, value, isFetch) {
70
+ const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
71
+ if (!kv) {
72
+ throw new IgnorableError(`No KVNamespace`);
73
+ }
74
+ this.debug(`Set ${key}`);
75
+ try {
76
+ const kvKey = this.getKVKey(key, isFetch);
77
+ // Note: We can not set a TTL as we might fallback to assets,
78
+ // still removing old data (old BUILD_ID) could help avoiding
79
+ // the cache growing too big.
80
+ await kv.put(kvKey, JSON.stringify({
81
+ value,
82
+ // Note: `Date.now()` returns the time of the last IO rather than the actual time.
83
+ // See https://developers.cloudflare.com/workers/reference/security-model/
84
+ lastModified: Date.now(),
85
+ }));
86
+ }
87
+ catch {
88
+ throw new RecoverableError(`Failed to set cache [${key}]`);
89
+ }
90
+ }
91
+ async delete(key) {
92
+ const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
93
+ if (!kv) {
94
+ throw new IgnorableError(`No KVNamespace`);
95
+ }
96
+ this.debug(`Delete ${key}`);
97
+ try {
98
+ const kvKey = this.getKVKey(key, /* isFetch= */ false);
99
+ // Do not delete the key as we would then fallback to the assets.
100
+ await kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
101
+ }
102
+ catch {
103
+ throw new RecoverableError(`Failed to delete cache [${key}]`);
104
+ }
105
+ }
106
+ getKVKey(key, isFetch) {
107
+ return `${this.getBuildId()}/${key}.${isFetch ? "fetch" : "cache"}`;
108
+ }
109
+ getAssetUrl(key, isFetch) {
110
+ return isFetch
111
+ ? `http://assets.local/${CACHE_ASSET_DIR}/__fetch/${this.getBuildId()}/${key}`
112
+ : `http://assets.local/${CACHE_ASSET_DIR}/${this.getBuildId()}/${key}.cache`;
113
+ }
114
+ debug(...args) {
115
+ if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
116
+ console.log(`[Cache ${this.name}] `, ...args);
117
+ }
118
+ }
119
+ getBuildId() {
120
+ return process.env.NEXT_BUILD_ID ?? "no-build-id";
121
+ }
122
+ }
123
+ export default new Cache();
@@ -1,22 +1,5 @@
1
- import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
2
- export declare const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
3
- export declare const STATUS_DELETED = 1;
1
+ import cache from "./kv-cache.js";
4
2
  /**
5
- * Open Next cache based on cloudflare KV and Assets.
6
- *
7
- * Note: The class is instantiated outside of the request context.
8
- * The cloudflare context and process.env are not initialzed yet
9
- * when the constructor is called.
3
+ * @deprecated Please import from `kv-cache` instead of `kvCache`.
10
4
  */
11
- declare class Cache implements IncrementalCache {
12
- readonly name = "cloudflare-kv";
13
- get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>>>;
14
- set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
15
- delete(key: string): Promise<void>;
16
- protected getKVKey(key: string, isFetch?: boolean): string;
17
- protected getAssetUrl(key: string, isFetch?: boolean): string;
18
- protected debug(...args: unknown[]): void;
19
- protected getBuildId(): string;
20
- }
21
- declare const _default: Cache;
22
- export default _default;
5
+ export default cache;
@@ -1,108 +1,5 @@
1
- import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
2
- import { getCloudflareContext } from "./cloudflare-context.js";
3
- export const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
4
- export const STATUS_DELETED = 1;
1
+ import cache from "./kv-cache.js";
5
2
  /**
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.
3
+ * @deprecated Please import from `kv-cache` instead of `kvCache`.
11
4
  */
12
- class Cache {
13
- name = "cloudflare-kv";
14
- async get(key, isFetch) {
15
- const cfEnv = getCloudflareContext().env;
16
- const kv = cfEnv.NEXT_CACHE_WORKERS_KV;
17
- const assets = cfEnv.ASSETS;
18
- if (!(kv || assets)) {
19
- throw new IgnorableError(`No KVNamespace nor Fetcher`);
20
- }
21
- this.debug(`Get ${key}`);
22
- try {
23
- let entry = null;
24
- if (kv) {
25
- this.debug(`- From KV`);
26
- const kvKey = this.getKVKey(key, isFetch);
27
- entry = await kv.get(kvKey, "json");
28
- if (entry?.status === STATUS_DELETED) {
29
- return {};
30
- }
31
- }
32
- if (!entry && assets) {
33
- this.debug(`- From Assets`);
34
- const url = this.getAssetUrl(key, isFetch);
35
- const response = await assets.fetch(url);
36
- if (response.ok) {
37
- // TODO: consider populating KV with the asset value if faster.
38
- // This could be optional as KV writes are $$.
39
- // See https://github.com/opennextjs/opennextjs-cloudflare/pull/194#discussion_r1893166026
40
- entry = {
41
- value: await response.json(),
42
- // __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
43
- lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
44
- };
45
- }
46
- }
47
- this.debug(entry ? `-> hit` : `-> miss`);
48
- return { value: entry?.value, lastModified: entry?.lastModified };
49
- }
50
- catch {
51
- throw new RecoverableError(`Failed to get cache [${key}]`);
52
- }
53
- }
54
- async set(key, value, isFetch) {
55
- const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
56
- if (!kv) {
57
- throw new IgnorableError(`No KVNamespace`);
58
- }
59
- this.debug(`Set ${key}`);
60
- try {
61
- const kvKey = this.getKVKey(key, isFetch);
62
- // Note: We can not set a TTL as we might fallback to assets,
63
- // still removing old data (old BUILD_ID) could help avoiding
64
- // the cache growing too big.
65
- await kv.put(kvKey, JSON.stringify({
66
- value,
67
- // Note: `Date.now()` returns the time of the last IO rather than the actual time.
68
- // See https://developers.cloudflare.com/workers/reference/security-model/
69
- lastModified: Date.now(),
70
- }));
71
- }
72
- catch {
73
- throw new RecoverableError(`Failed to set cache [${key}]`);
74
- }
75
- }
76
- async delete(key) {
77
- const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
78
- if (!kv) {
79
- throw new IgnorableError(`No KVNamespace`);
80
- }
81
- this.debug(`Delete ${key}`);
82
- try {
83
- const kvKey = this.getKVKey(key, /* isFetch= */ false);
84
- // Do not delete the key as we would then fallback to the assets.
85
- await kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
86
- }
87
- catch {
88
- throw new RecoverableError(`Failed to delete cache [${key}]`);
89
- }
90
- }
91
- getKVKey(key, isFetch) {
92
- return `${this.getBuildId()}/${key}.${isFetch ? "fetch" : "cache"}`;
93
- }
94
- getAssetUrl(key, isFetch) {
95
- return isFetch
96
- ? `http://assets.local/${CACHE_ASSET_DIR}/__fetch/${this.getBuildId()}/${key}`
97
- : `http://assets.local/${CACHE_ASSET_DIR}/${this.getBuildId()}/${key}.cache`;
98
- }
99
- debug(...args) {
100
- if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
101
- console.log(`[Cache ${this.name}] `, ...args);
102
- }
103
- }
104
- getBuildId() {
105
- return process.env.NEXT_BUILD_ID ?? "no-build-id";
106
- }
107
- }
108
- export default new Cache();
5
+ export default cache;
@@ -3,12 +3,15 @@ import { readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
6
- import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
7
6
  import { build } from "esbuild";
8
7
  import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
8
+ import { patchWebpackRuntime } from "./patches/ast/webpack-runtime.js";
9
9
  import * as patches from "./patches/index.js";
10
+ import { ContentUpdater } from "./patches/plugins/content-updater.js";
11
+ import { patchLoadInstrumentation } from "./patches/plugins/load-instrumentation.js";
10
12
  import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
11
13
  import { fixRequire } from "./patches/plugins/require.js";
14
+ import { shimRequireHook } from "./patches/plugins/require-hook.js";
12
15
  import { inlineRequirePagePlugin } from "./patches/plugins/require-page.js";
13
16
  import { setWranglerExternal } from "./patches/plugins/wrangler-external.js";
14
17
  import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
@@ -36,12 +39,13 @@ export async function bundleServer(buildOpts) {
36
39
  const serverFiles = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/required-server-files.json");
37
40
  const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
38
41
  console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
39
- await patches.updateWebpackChunksFile(buildOpts);
42
+ await patchWebpackRuntime(buildOpts);
40
43
  patchVercelOgLibrary(buildOpts);
41
44
  const outputPath = path.join(outputDir, "server-functions", "default");
42
45
  const packagePath = getPackagePath(buildOpts);
43
46
  const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
44
47
  const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
48
+ const updater = new ContentUpdater();
45
49
  const result = await build({
46
50
  entryPoints: [openNextServer],
47
51
  bundle: true,
@@ -61,11 +65,14 @@ export async function bundleServer(buildOpts) {
61
65
  // - ESBuild `node` platform: https://esbuild.github.io/api/#platform
62
66
  conditions: [],
63
67
  plugins: [
64
- createFixRequiresESBuildPlugin(buildOpts),
65
- inlineRequirePagePlugin(buildOpts),
68
+ shimRequireHook(buildOpts),
69
+ inlineRequirePagePlugin(updater, buildOpts),
66
70
  setWranglerExternal(),
67
- fixRequire(),
71
+ fixRequire(updater),
68
72
  handleOptionalDependencies(optionalDependencies),
73
+ patchLoadInstrumentation(updater),
74
+ // Apply updater updaters, must be the last plugin
75
+ updater.plugin,
69
76
  ],
70
77
  external: ["./middleware/handler.mjs"],
71
78
  alias: {
@@ -161,7 +168,6 @@ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
161
168
  (code) => patches.inlineMiddlewareManifestRequire(code, buildOpts),
162
169
  ],
163
170
  ["exception bubbling", patches.patchExceptionBubbling],
164
- ["`loadInstrumentationModule` function", patches.patchLoadInstrumentationModule],
165
171
  [
166
172
  "`patchAsyncStorage` call",
167
173
  (code) => code
@@ -176,17 +182,6 @@ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
176
182
  ]);
177
183
  await writeFile(workerOutputFile, patchedCode);
178
184
  }
179
- function createFixRequiresESBuildPlugin(options) {
180
- return {
181
- name: "replaceRelative",
182
- setup(build) {
183
- // Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
184
- build.onResolve({ filter: getCrossPlatformPathRegex(String.raw `^\./require-hook$`, { escape: false }) }, () => ({
185
- path: path.join(options.outputDir, "cloudflare-templates/shims/empty.js"),
186
- }));
187
- },
188
- };
189
- }
190
185
  /**
191
186
  * Gets the path of the worker.js file generated by the build process
192
187
  *
@@ -1,6 +1,6 @@
1
1
  import { cpSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { CACHE_ASSET_DIR } from "../../../api/kvCache.js";
3
+ import { CACHE_ASSET_DIR } from "../../../api/kv-cache.js";
4
4
  export function copyCacheAssets(options) {
5
5
  const { outputDir } = options;
6
6
  const srcPath = join(outputDir, "cache");
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Inline dynamic requires in the webpack runtime.
3
+ *
4
+ * The webpack runtime has dynamic requires that would not be bundled by ESBuild:
5
+ *
6
+ * installChunk(require("./chunks/" + __webpack_require__.u(chunkId)));
7
+ *
8
+ * This patch unrolls the dynamic require for all the existing chunks:
9
+ *
10
+ * switch (chunkId) {
11
+ * case ID1: installChunk(require("./chunks/ID1"); break;
12
+ * case ID2: installChunk(require("./chunks/ID2"); break;
13
+ * // ...
14
+ * case SELF_ID: installedChunks[chunkId] = 1; break;
15
+ * default: throw new Error(`Unknown chunk ${chunkId}`);
16
+ * }
17
+ */
18
+ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
19
+ export declare function buildInlineChunksRule(chunks: number[]): string;
20
+ /**
21
+ * Fixes the webpack-runtime.js file by removing its webpack dynamic requires.
22
+ */
23
+ export declare function patchWebpackRuntime(buildOpts: BuildOptions): Promise<void>;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Inline dynamic requires in the webpack runtime.
3
+ *
4
+ * The webpack runtime has dynamic requires that would not be bundled by ESBuild:
5
+ *
6
+ * installChunk(require("./chunks/" + __webpack_require__.u(chunkId)));
7
+ *
8
+ * This patch unrolls the dynamic require for all the existing chunks:
9
+ *
10
+ * switch (chunkId) {
11
+ * case ID1: installChunk(require("./chunks/ID1"); break;
12
+ * case ID2: installChunk(require("./chunks/ID2"); break;
13
+ * // ...
14
+ * case SELF_ID: installedChunks[chunkId] = 1; break;
15
+ * default: throw new Error(`Unknown chunk ${chunkId}`);
16
+ * }
17
+ */
18
+ import { readdirSync, readFileSync, writeFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
21
+ import { patchCode } from "./util.js";
22
+ export function buildInlineChunksRule(chunks) {
23
+ return `
24
+ rule:
25
+ pattern: ($CHUNK_ID, $_PROMISES) => { $$$ }
26
+ inside: {pattern: $_.$_.require = $$$_, stopBy: end}
27
+ all:
28
+ - has: {pattern: $INSTALL(require("./chunks/" + $$$)), stopBy: end}
29
+ - has: {pattern: $SELF_ID != $CHUNK_ID, stopBy: end}
30
+ - has: {pattern: "$INSTALLED_CHUNK[$CHUNK_ID] = 1", stopBy: end}
31
+ fix: |
32
+ ($CHUNK_ID, _) => {
33
+ if (!$INSTALLED_CHUNK[$CHUNK_ID]) {
34
+ switch ($CHUNK_ID) {
35
+ ${chunks.map((chunk) => ` case ${chunk}: $INSTALL(require("./chunks/${chunk}.js")); break;`).join("\n")}
36
+ case $SELF_ID: $INSTALLED_CHUNK[$CHUNK_ID] = 1; break;
37
+ default: throw new Error(\`Unknown chunk \${$CHUNK_ID}\`);
38
+ }
39
+ }
40
+ }`;
41
+ }
42
+ /**
43
+ * Fixes the webpack-runtime.js file by removing its webpack dynamic requires.
44
+ */
45
+ export async function patchWebpackRuntime(buildOpts) {
46
+ const { outputDir } = buildOpts;
47
+ const dotNextServerDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
48
+ const runtimeFile = join(dotNextServerDir, "webpack-runtime.js");
49
+ const runtimeCode = readFileSync(runtimeFile, "utf-8");
50
+ // Look for all the chunks.
51
+ const chunks = readdirSync(join(dotNextServerDir, "chunks"))
52
+ .filter((chunk) => /^\d+\.js$/.test(chunk))
53
+ .map((chunk) => {
54
+ return Number(chunk.replace(/\.js$/, ""));
55
+ });
56
+ writeFileSync(runtimeFile, patchCode(runtimeCode, buildInlineChunksRule(chunks)));
57
+ }
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { patchCode } from "./util.js";
3
+ import { buildInlineChunksRule } from "./webpack-runtime.js";
4
+ describe("webpack runtime", () => {
5
+ test("patch runtime", () => {
6
+ const code = `
7
+ /******/ // require() chunk loading for javascript
8
+ /******/ __webpack_require__.f.require = (chunkId, promises) => {
9
+ /******/ // "1" is the signal for "already loaded"
10
+ /******/ if (!installedChunks[chunkId]) {
11
+ /******/ if (658 != chunkId) {
12
+ /******/ installChunk(require("./chunks/" + __webpack_require__.u(chunkId)));
13
+ /******/
14
+ } else installedChunks[chunkId] = 1;
15
+ /******/
16
+ }
17
+ /******/
18
+ };
19
+ `;
20
+ expect(patchCode(code, buildInlineChunksRule([1, 2, 3]))).toMatchInlineSnapshot(`
21
+ "/******/ // require() chunk loading for javascript
22
+ /******/ __webpack_require__.f.require = (chunkId, _) => {
23
+ if (!installedChunks[chunkId]) {
24
+ switch (chunkId) {
25
+ case 1: installChunk(require("./chunks/1.js")); break;
26
+ case 2: installChunk(require("./chunks/2.js")); break;
27
+ case 3: installChunk(require("./chunks/3.js")); break;
28
+ case 658: installedChunks[chunkId] = 1; break;
29
+ default: throw new Error(\`Unknown chunk \${chunkId}\`);
30
+ }
31
+ }
32
+ }
33
+ ;
34
+ "
35
+ `);
36
+ });
37
+ test("patch minified runtime", () => {
38
+ const code = `
39
+ t.f.require=(o,n)=>{e[o]||(658!=o?r(require("./chunks/"+t.u(o))):e[o]=1)}
40
+ `;
41
+ expect(patchCode(code, buildInlineChunksRule([1, 2, 3]))).toMatchInlineSnapshot(`
42
+ "t.f.require=(o, _) => {
43
+ if (!e[o]) {
44
+ switch (o) {
45
+ case 1: r(require("./chunks/1.js")); break;
46
+ case 2: r(require("./chunks/2.js")); break;
47
+ case 3: r(require("./chunks/3.js")); break;
48
+ case 658: e[o] = 1; break;
49
+ default: throw new Error(\`Unknown chunk \${o}\`);
50
+ }
51
+ }
52
+ }
53
+
54
+ "
55
+ `);
56
+ });
57
+ });
@@ -1,4 +1,3 @@
1
1
  export * from "./copy-package-cli-files.js";
2
2
  export * from "./patch-cache.js";
3
3
  export * from "./patch-require.js";
4
- export * from "./update-webpack-chunks-file/index.js";
@@ -1,4 +1,3 @@
1
1
  export * from "./copy-package-cli-files.js";
2
2
  export * from "./patch-cache.js";
3
3
  export * from "./patch-require.js";
4
- export * from "./update-webpack-chunks-file/index.js";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content.
3
+ *
4
+ * The updater allows multiple plugins to update the content.
5
+ */
6
+ import { type OnLoadOptions, type Plugin, type PluginBuild } from "esbuild";
7
+ export type Callback = (args: {
8
+ contents: string;
9
+ path: string;
10
+ }) => string | undefined | Promise<string | undefined>;
11
+ export type Updater = OnLoadOptions & {
12
+ callback: Callback;
13
+ };
14
+ export declare class ContentUpdater {
15
+ updaters: Map<string, Updater>;
16
+ /**
17
+ * Register a callback to update the file content.
18
+ *
19
+ * The callbacks are called in order of registration.
20
+ *
21
+ * @param name The name of the plugin (must be unique).
22
+ * @param options Same options as the `onLoad` hook to restrict updates.
23
+ * @param callback The callback updating the content.
24
+ * @returns A noop ESBuild plugin.
25
+ */
26
+ updateContent(name: string, options: OnLoadOptions, callback: Callback): Plugin;
27
+ /**
28
+ * Returns an ESBuild plugin applying the registered updates.
29
+ */
30
+ get plugin(): {
31
+ name: string;
32
+ setup: (build: PluginBuild) => Promise<void>;
33
+ };
34
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ESBuild stops calling `onLoad` hooks after the first hook returns an updated content.
3
+ *
4
+ * The updater allows multiple plugins to update the content.
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ export class ContentUpdater {
8
+ updaters = new Map();
9
+ /**
10
+ * Register a callback to update the file content.
11
+ *
12
+ * The callbacks are called in order of registration.
13
+ *
14
+ * @param name The name of the plugin (must be unique).
15
+ * @param options Same options as the `onLoad` hook to restrict updates.
16
+ * @param callback The callback updating the content.
17
+ * @returns A noop ESBuild plugin.
18
+ */
19
+ updateContent(name, options, callback) {
20
+ if (this.updaters.has(name)) {
21
+ throw new Error(`Plugin "${name}" already registered`);
22
+ }
23
+ this.updaters.set(name, { ...options, callback });
24
+ return {
25
+ name,
26
+ setup() { },
27
+ };
28
+ }
29
+ /**
30
+ * Returns an ESBuild plugin applying the registered updates.
31
+ */
32
+ get plugin() {
33
+ return {
34
+ name: "aggregate-on-load",
35
+ setup: async (build) => {
36
+ build.onLoad({ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, async (args) => {
37
+ let contents = await readFile(args.path, "utf-8");
38
+ for (const { filter, namespace, callback } of this.updaters.values()) {
39
+ if (namespace !== undefined && args.namespace !== namespace) {
40
+ continue;
41
+ }
42
+ if (!filter.test(args.path)) {
43
+ continue;
44
+ }
45
+ contents = (await callback({ contents, path: args.path })) ?? contents;
46
+ }
47
+ return { contents };
48
+ });
49
+ },
50
+ };
51
+ }
52
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `loadInstrumentationModule` uses a dynamic require which is not supported.
3
+ */
4
+ import type { ContentUpdater } from "./content-updater.js";
5
+ export declare const instrumentationRule = "\nrule:\n kind: method_definition\n all:\n - has: {field: name, regex: ^loadInstrumentationModule$}\n - has: {pattern: dynamicRequire, stopBy: end}\n\nfix: async loadInstrumentationModule() { }\n";
6
+ export declare function patchLoadInstrumentation(updater: ContentUpdater): import("esbuild").Plugin;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `loadInstrumentationModule` uses a dynamic require which is not supported.
3
+ */
4
+ import { patchCode } from "../ast/util.js";
5
+ export const instrumentationRule = `
6
+ rule:
7
+ kind: method_definition
8
+ all:
9
+ - has: {field: name, regex: ^loadInstrumentationModule$}
10
+ - has: {pattern: dynamicRequire, stopBy: end}
11
+
12
+ fix: async loadInstrumentationModule() { }
13
+ `;
14
+ export function patchLoadInstrumentation(updater) {
15
+ return updater.updateContent("patch-load-instrumentation", { filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, ({ contents }) => {
16
+ if (/async loadInstrumentationModule\(/.test(contents)) {
17
+ return patchCode(contents, instrumentationRule);
18
+ }
19
+ });
20
+ }