@opennextjs/cloudflare 0.4.1 → 0.4.3
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.
- package/dist/api/cloudflare-context.js +8 -11
- package/dist/cli/build/bundle-server.d.ts +4 -0
- package/dist/cli/build/bundle-server.js +28 -19
- package/dist/cli/build/patches/ast/optional-deps.d.ts +9 -2
- package/dist/cli/build/patches/ast/optional-deps.js +33 -20
- package/dist/cli/build/patches/ast/optional-deps.spec.js +60 -14
- package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.js +2 -2
- package/dist/cli/build/patches/investigated/index.d.ts +0 -1
- package/dist/cli/build/patches/investigated/index.js +0 -1
- package/dist/cli/build/patches/plugins/require.d.ts +5 -0
- package/dist/cli/build/patches/plugins/require.js +44 -0
- package/dist/cli/build/patches/to-investigate/index.d.ts +0 -1
- package/dist/cli/build/patches/to-investigate/index.js +0 -1
- package/package.json +4 -4
- package/dist/cli/build/patches/to-investigate/wrangler-deps.d.ts +0 -2
- package/dist/cli/build/patches/to-investigate/wrangler-deps.js +0 -115
- /package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.d.ts +0 -0
- /package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.spec.d.ts +0 -0
- /package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.spec.js +0 -0
|
@@ -18,19 +18,16 @@ export function getCloudflareContext() {
|
|
|
18
18
|
// the cloudflare context is initialized by the worker and is always present in production/preview
|
|
19
19
|
// during local development (`next dev`) it might be missing only if the developers hasn't called
|
|
20
20
|
// the `initOpenNextCloudflareForDev` function in their Next.js config file
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
` ${initFunctionName}();\n\n` +
|
|
29
|
-
" /** @type {import('next').NextConfig} */\n" +
|
|
30
|
-
" const nextConfig = {};\n" +
|
|
21
|
+
throw new Error(`\n\nERROR: \`getCloudflareContext\` has been called without having called` +
|
|
22
|
+
` \`initOpenNextCloudflareForDev\` from the Next.js config file.\n` +
|
|
23
|
+
`You should update your Next.js config file as shown below:\n\n` +
|
|
24
|
+
" ```\n // next.config.mjs\n\n" +
|
|
25
|
+
` import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";\n\n` +
|
|
26
|
+
` initOpenNextCloudflareForDev();\n\n` +
|
|
27
|
+
" const nextConfig = { ... };\n" +
|
|
31
28
|
" export default nextConfig;\n" +
|
|
32
29
|
" ```\n" +
|
|
33
|
-
"\n
|
|
30
|
+
"\n");
|
|
34
31
|
}
|
|
35
32
|
return cloudflareContext;
|
|
36
33
|
}
|
|
@@ -3,6 +3,10 @@ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
|
|
|
3
3
|
* Bundle the Open Next server.
|
|
4
4
|
*/
|
|
5
5
|
export declare function bundleServer(buildOpts: BuildOptions): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* This function applies patches required for the code to run on workers.
|
|
8
|
+
*/
|
|
9
|
+
export declare function updateWorkerBundledCode(workerOutputFile: string, buildOpts: BuildOptions): Promise<void>;
|
|
6
10
|
/**
|
|
7
11
|
* Gets the path of the worker.js file generated by the build process
|
|
8
12
|
*
|
|
@@ -7,12 +7,27 @@ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
|
|
|
7
7
|
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
|
|
8
8
|
import { build } from "esbuild";
|
|
9
9
|
import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
|
|
10
|
+
import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
|
|
10
11
|
import * as patches from "./patches/index.js";
|
|
12
|
+
import fixRequire from "./patches/plugins/require.js";
|
|
11
13
|
import inlineRequirePagePlugin from "./patches/plugins/require-page.js";
|
|
12
14
|
import setWranglerExternal from "./patches/plugins/wrangler-external.js";
|
|
13
15
|
import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
|
|
14
16
|
/** The dist directory of the Cloudflare adapter package */
|
|
15
17
|
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
18
|
+
/**
|
|
19
|
+
* List of optional Next.js dependencies.
|
|
20
|
+
* They are not required for Next.js to run but only needed to enabled specific features.
|
|
21
|
+
* When one of those dependency is required, it should be installed by the application.
|
|
22
|
+
*/
|
|
23
|
+
const optionalDependencies = [
|
|
24
|
+
"caniuse-lite",
|
|
25
|
+
"critters",
|
|
26
|
+
"jimp",
|
|
27
|
+
"probe-image-size",
|
|
28
|
+
// `server.edge` is not available in react-dom@18
|
|
29
|
+
"react-dom/server.edge",
|
|
30
|
+
];
|
|
16
31
|
/**
|
|
17
32
|
* Bundle the Open Next server.
|
|
18
33
|
*/
|
|
@@ -22,32 +37,27 @@ export async function bundleServer(buildOpts) {
|
|
|
22
37
|
const serverFiles = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/required-server-files.json");
|
|
23
38
|
const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
|
|
24
39
|
console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
|
|
25
|
-
patches.patchWranglerDeps(buildOpts);
|
|
26
40
|
await patches.updateWebpackChunksFile(buildOpts);
|
|
27
|
-
|
|
41
|
+
patchVercelOgLibrary(buildOpts);
|
|
28
42
|
const outputPath = path.join(outputDir, "server-functions", "default");
|
|
29
43
|
const packagePath = getPackagePath(buildOpts);
|
|
30
44
|
const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
|
|
31
45
|
const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
|
|
32
|
-
await build({
|
|
46
|
+
const result = await build({
|
|
33
47
|
entryPoints: [openNextServer],
|
|
34
48
|
bundle: true,
|
|
35
49
|
outfile: openNextServerBundle,
|
|
36
50
|
format: "esm",
|
|
37
51
|
target: "esnext",
|
|
38
52
|
minify: false,
|
|
53
|
+
metafile: true,
|
|
39
54
|
plugins: [
|
|
40
55
|
createFixRequiresESBuildPlugin(buildOpts),
|
|
41
56
|
inlineRequirePagePlugin(buildOpts),
|
|
42
57
|
setWranglerExternal(),
|
|
58
|
+
fixRequire(),
|
|
43
59
|
],
|
|
44
|
-
external: [
|
|
45
|
-
"./middleware/handler.mjs",
|
|
46
|
-
// Next optional dependencies.
|
|
47
|
-
"caniuse-lite",
|
|
48
|
-
"jimp",
|
|
49
|
-
"probe-image-size",
|
|
50
|
-
],
|
|
60
|
+
external: ["./middleware/handler.mjs", ...optionalDependencies],
|
|
51
61
|
alias: {
|
|
52
62
|
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
|
|
53
63
|
// eval("require")("bufferutil");
|
|
@@ -75,7 +85,6 @@ export async function bundleServer(buildOpts) {
|
|
|
75
85
|
"process.env.NODE_ENV": '"production"',
|
|
76
86
|
"process.env.NEXT_MINIMAL": "true",
|
|
77
87
|
},
|
|
78
|
-
// We need to set platform to node so that esbuild doesn't complain about the node imports
|
|
79
88
|
platform: "node",
|
|
80
89
|
banner: {
|
|
81
90
|
js: `
|
|
@@ -103,7 +112,7 @@ const CustomRequest = class extends globalThis.Request {
|
|
|
103
112
|
// https://github.com/cloudflare/workerd/issues/2746
|
|
104
113
|
// https://github.com/cloudflare/workerd/issues/3245
|
|
105
114
|
Object.defineProperty(init, "body", {
|
|
106
|
-
value: init.body instanceof __cf_stream.Readable ? ReadableStream.from(init.body) : init.body
|
|
115
|
+
value: init.body instanceof __cf_stream.Readable ? ReadableStream.from(init.body) : init.body
|
|
107
116
|
});
|
|
108
117
|
}
|
|
109
118
|
super(input, init);
|
|
@@ -117,6 +126,11 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
|
|
|
117
126
|
`,
|
|
118
127
|
},
|
|
119
128
|
});
|
|
129
|
+
if (result.errors.length > 0) {
|
|
130
|
+
result.errors.forEach((error) => console.error(error));
|
|
131
|
+
throw new Error(`There was a problem bundling the server.`);
|
|
132
|
+
}
|
|
133
|
+
fs.writeFileSync(openNextServerBundle + ".meta.json", JSON.stringify(result.metafile, null, 2));
|
|
120
134
|
await updateWorkerBundledCode(openNextServerBundle, buildOpts);
|
|
121
135
|
const isMonorepo = monorepoRoot !== appPath;
|
|
122
136
|
if (isMonorepo) {
|
|
@@ -127,7 +141,7 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
|
|
|
127
141
|
/**
|
|
128
142
|
* This function applies patches required for the code to run on workers.
|
|
129
143
|
*/
|
|
130
|
-
async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
|
|
144
|
+
export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
|
|
131
145
|
const code = await readFile(workerOutputFile, "utf8");
|
|
132
146
|
const patchedCode = await patchCodeWithValidations(code, [
|
|
133
147
|
["require", patches.patchRequire],
|
|
@@ -148,11 +162,6 @@ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
|
|
|
148
162
|
// TODO: implement for cf (possibly in @opennextjs/aws)
|
|
149
163
|
.replace("patchAsyncStorage();", "//patchAsyncStorage();"),
|
|
150
164
|
],
|
|
151
|
-
[
|
|
152
|
-
'`eval("require")` calls',
|
|
153
|
-
(code) => code.replaceAll('eval("require")', "require"),
|
|
154
|
-
{ isOptional: true },
|
|
155
|
-
],
|
|
156
165
|
[
|
|
157
166
|
"`require.resolve` call",
|
|
158
167
|
// workers do not support dynamic require nor require.resolve
|
|
@@ -160,7 +169,7 @@ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
|
|
|
160
169
|
],
|
|
161
170
|
]);
|
|
162
171
|
const bundle = parse(Lang.TypeScript, patchedCode).root();
|
|
163
|
-
const { edits } = patchOptionalDependencies(bundle);
|
|
172
|
+
const { edits } = patchOptionalDependencies(bundle, optionalDependencies);
|
|
164
173
|
await writeFile(workerOutputFile, bundle.commitEdits(edits));
|
|
165
174
|
}
|
|
166
175
|
function createFixRequiresESBuildPlugin(options) {
|
|
@@ -6,8 +6,15 @@ import { type SgNode } from "@ast-grep/napi";
|
|
|
6
6
|
*
|
|
7
7
|
* So we wrap `require(optionalDep)` in a try/catch (if not already present).
|
|
8
8
|
*/
|
|
9
|
-
export declare
|
|
10
|
-
|
|
9
|
+
export declare function buildOptionalDepRule(dependencies: string[]): string;
|
|
10
|
+
/**
|
|
11
|
+
* Wraps requires for passed dependencies in a `try ... catch`.
|
|
12
|
+
*
|
|
13
|
+
* @param root AST root node
|
|
14
|
+
* @param dependencies List of dependencies to wrap
|
|
15
|
+
* @returns matches and edits, see `applyRule`
|
|
16
|
+
*/
|
|
17
|
+
export declare function patchOptionalDependencies(root: SgNode, dependencies: string[]): {
|
|
11
18
|
edits: import("@ast-grep/napi").Edit[];
|
|
12
19
|
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
20
|
};
|
|
@@ -6,26 +6,39 @@ import { applyRule } from "./util.js";
|
|
|
6
6
|
*
|
|
7
7
|
* So we wrap `require(optionalDep)` in a try/catch (if not already present).
|
|
8
8
|
*/
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
kind:
|
|
9
|
+
export function buildOptionalDepRule(dependencies) {
|
|
10
|
+
// Build a regexp matching either
|
|
11
|
+
// - the full packages names, i.e. `package`
|
|
12
|
+
// - subpaths in the package, i.e. `package/...`
|
|
13
|
+
const regex = `^(${dependencies.join("|")})(/|$)`;
|
|
14
|
+
return `
|
|
15
|
+
rule:
|
|
16
|
+
pattern: $$$LHS = require($$$REQ)
|
|
17
|
+
has:
|
|
18
|
+
pattern: $MOD
|
|
19
|
+
kind: string_fragment
|
|
20
20
|
stopBy: end
|
|
21
|
+
regex: ${regex}
|
|
22
|
+
not:
|
|
23
|
+
inside:
|
|
24
|
+
kind: try_statement
|
|
25
|
+
stopBy: end
|
|
21
26
|
|
|
22
|
-
fix: |-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`;
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
fix: |-
|
|
28
|
+
try {
|
|
29
|
+
$$$LHS = require($$$REQ);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error('The optional dependency "$MOD" is not installed');
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wraps requires for passed dependencies in a `try ... catch`.
|
|
37
|
+
*
|
|
38
|
+
* @param root AST root node
|
|
39
|
+
* @param dependencies List of dependencies to wrap
|
|
40
|
+
* @returns matches and edits, see `applyRule`
|
|
41
|
+
*/
|
|
42
|
+
export function patchOptionalDependencies(root, dependencies) {
|
|
43
|
+
return applyRule(buildOptionalDepRule(dependencies), root);
|
|
31
44
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { buildOptionalDepRule } from "./optional-deps.js";
|
|
3
3
|
import { patchCode } from "./util.js";
|
|
4
4
|
describe("optional dependecy", () => {
|
|
5
5
|
it('should wrap a top-level require("caniuse-lite") in a try-catch', () => {
|
|
6
6
|
const code = `t = require("caniuse-lite");`;
|
|
7
|
-
expect(patchCode(code,
|
|
7
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
8
8
|
"try {
|
|
9
9
|
t = require("caniuse-lite");
|
|
10
10
|
} catch {
|
|
@@ -14,7 +14,7 @@ describe("optional dependecy", () => {
|
|
|
14
14
|
});
|
|
15
15
|
it('should wrap a top-level require("caniuse-lite/data") in a try-catch', () => {
|
|
16
16
|
const code = `t = require("caniuse-lite/data");`;
|
|
17
|
-
expect(patchCode(code,
|
|
17
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
18
18
|
"try {
|
|
19
19
|
t = require("caniuse-lite/data");
|
|
20
20
|
} catch {
|
|
@@ -24,7 +24,7 @@ describe("optional dependecy", () => {
|
|
|
24
24
|
});
|
|
25
25
|
it('should wrap e.exports = require("caniuse-lite") in a try-catch', () => {
|
|
26
26
|
const code = 'e.exports = require("caniuse-lite");';
|
|
27
|
-
expect(patchCode(code,
|
|
27
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
28
28
|
"try {
|
|
29
29
|
e.exports = require("caniuse-lite");
|
|
30
30
|
} catch {
|
|
@@ -34,7 +34,7 @@ describe("optional dependecy", () => {
|
|
|
34
34
|
});
|
|
35
35
|
it('should wrap module.exports = require("caniuse-lite") in a try-catch', () => {
|
|
36
36
|
const code = 'module.exports = require("caniuse-lite");';
|
|
37
|
-
expect(patchCode(code,
|
|
37
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
38
38
|
"try {
|
|
39
39
|
module.exports = require("caniuse-lite");
|
|
40
40
|
} catch {
|
|
@@ -44,7 +44,7 @@ describe("optional dependecy", () => {
|
|
|
44
44
|
});
|
|
45
45
|
it('should wrap exports.foo = require("caniuse-lite") in a try-catch', () => {
|
|
46
46
|
const code = 'exports.foo = require("caniuse-lite");';
|
|
47
|
-
expect(patchCode(code,
|
|
47
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
48
48
|
"try {
|
|
49
49
|
exports.foo = require("caniuse-lite");
|
|
50
50
|
} catch {
|
|
@@ -54,21 +54,21 @@ describe("optional dependecy", () => {
|
|
|
54
54
|
});
|
|
55
55
|
it('should not wrap require("lodash") in a try-catch', () => {
|
|
56
56
|
const code = 't = require("lodash");';
|
|
57
|
-
expect(patchCode(code,
|
|
57
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`"t = require("lodash");"`);
|
|
58
58
|
});
|
|
59
59
|
it('should not wrap require("other-module") if it does not match caniuse-lite regex', () => {
|
|
60
60
|
const code = 't = require("other-module");';
|
|
61
|
-
expect(patchCode(code,
|
|
61
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`"t = require("other-module");"`);
|
|
62
62
|
});
|
|
63
63
|
it("should not wrap a require() call already inside a try-catch", () => {
|
|
64
64
|
const code = `
|
|
65
65
|
try {
|
|
66
|
-
|
|
66
|
+
t = require("caniuse-lite");
|
|
67
67
|
} catch {}
|
|
68
68
|
`;
|
|
69
|
-
expect(patchCode(code,
|
|
69
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
70
70
|
"try {
|
|
71
|
-
|
|
71
|
+
t = require("caniuse-lite");
|
|
72
72
|
} catch {}
|
|
73
73
|
"
|
|
74
74
|
`);
|
|
@@ -76,14 +76,60 @@ try {
|
|
|
76
76
|
it("should handle require with subpath and not wrap if already in try-catch", () => {
|
|
77
77
|
const code = `
|
|
78
78
|
try {
|
|
79
|
-
|
|
79
|
+
t = require("caniuse-lite/path");
|
|
80
80
|
} catch {}
|
|
81
81
|
`;
|
|
82
|
-
expect(patchCode(code,
|
|
82
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
83
83
|
"try {
|
|
84
|
-
|
|
84
|
+
t = require("caniuse-lite/path");
|
|
85
85
|
} catch {}
|
|
86
86
|
"
|
|
87
87
|
`);
|
|
88
88
|
});
|
|
89
|
+
it("should handle multiple dependencies", () => {
|
|
90
|
+
const code = `
|
|
91
|
+
t1 = require("caniuse-lite");
|
|
92
|
+
t2 = require("caniuse-lite/path");
|
|
93
|
+
t3 = require("jimp");
|
|
94
|
+
t4 = require("jimp/path");
|
|
95
|
+
`;
|
|
96
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite", "jimp"]))).toMatchInlineSnapshot(`
|
|
97
|
+
"try {
|
|
98
|
+
t1 = require("caniuse-lite");
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error('The optional dependency "caniuse-lite" is not installed');
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
t2 = require("caniuse-lite/path");
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error('The optional dependency "caniuse-lite/path" is not installed');
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
t3 = require("jimp");
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error('The optional dependency "jimp" is not installed');
|
|
111
|
+
};
|
|
112
|
+
try {
|
|
113
|
+
t4 = require("jimp/path");
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error('The optional dependency "jimp/path" is not installed');
|
|
116
|
+
};
|
|
117
|
+
"
|
|
118
|
+
`);
|
|
119
|
+
});
|
|
120
|
+
it("should not update partial matches", () => {
|
|
121
|
+
const code = `
|
|
122
|
+
t1 = require("before-caniuse-lite");
|
|
123
|
+
t2 = require("before-caniuse-lite/path");
|
|
124
|
+
t3 = require("caniuse-lite-after");
|
|
125
|
+
t4 = require("caniuse-lite-after/path");
|
|
126
|
+
`;
|
|
127
|
+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
|
|
128
|
+
"t1 = require("before-caniuse-lite");
|
|
129
|
+
t2 = require("before-caniuse-lite/path");
|
|
130
|
+
t3 = require("caniuse-lite-after");
|
|
131
|
+
t4 = require("caniuse-lite-after/path");
|
|
132
|
+
"
|
|
133
|
+
`);
|
|
134
|
+
});
|
|
89
135
|
});
|
|
@@ -2,8 +2,8 @@ import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } fro
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { getPackagePath } from "@opennextjs/aws/build/helper.js";
|
|
4
4
|
import { globSync } from "glob";
|
|
5
|
-
import { parseFile } from "
|
|
6
|
-
import { patchVercelOgFallbackFont, patchVercelOgImport } from "
|
|
5
|
+
import { parseFile } from "./util.js";
|
|
6
|
+
import { patchVercelOgFallbackFont, patchVercelOgImport } from "./vercel-og.js";
|
|
7
7
|
/**
|
|
8
8
|
* Patches the usage of @vercel/og to be compatible with Cloudflare Workers.
|
|
9
9
|
*
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
export default function fixRequire() {
|
|
3
|
+
return {
|
|
4
|
+
name: "fix-require",
|
|
5
|
+
setup: async (build) => {
|
|
6
|
+
build.onLoad({ filter: /.*/ }, async ({ path }) => {
|
|
7
|
+
let contents = await fs.readFile(path, "utf-8");
|
|
8
|
+
// `eval(...)` is not supported by workerd.
|
|
9
|
+
contents = contents.replaceAll(`eval("require")`, "require");
|
|
10
|
+
// `@opentelemetry` has a few issues.
|
|
11
|
+
//
|
|
12
|
+
// Next.js has the following code in `next/dist/server/lib/trace/tracer.js`:
|
|
13
|
+
//
|
|
14
|
+
// try {
|
|
15
|
+
// api = require('@opentelemetry/api');
|
|
16
|
+
// } catch (err) {
|
|
17
|
+
// api = require('next/dist/compiled/@opentelemetry/api');
|
|
18
|
+
// }
|
|
19
|
+
//
|
|
20
|
+
// The intent is to allow users to install their own version of `@opentelemetry/api`.
|
|
21
|
+
//
|
|
22
|
+
// The problem is that even when users do not explicitely install `@opentelemetry/api`,
|
|
23
|
+
// `require('@opentelemetry/api')` resolves to the package which is a dependency
|
|
24
|
+
// of Next.
|
|
25
|
+
//
|
|
26
|
+
// The second problem is that when Next traces files, it would not copy the `api/build/esm`
|
|
27
|
+
// folder (used by the `module` conditions in package.json) it would only copy `api/build/src`.
|
|
28
|
+
// This could be solved by updating the next config:
|
|
29
|
+
//
|
|
30
|
+
// const nextConfig: NextConfig = {
|
|
31
|
+
// // ...
|
|
32
|
+
// outputFileTracingIncludes: {
|
|
33
|
+
// "*": ["./node_modules/@opentelemetry/api/build/**/*"],
|
|
34
|
+
// },
|
|
35
|
+
// };
|
|
36
|
+
//
|
|
37
|
+
// We can consider doing that when we want to enable users to install their own version
|
|
38
|
+
// of `@opentelemetry/api`. For now we simply use the pre-compiled version.
|
|
39
|
+
contents = contents.replace(/require\(.@opentelemetry\/api.\)/g, `require("next/dist/compiled/@opentelemetry/api")`);
|
|
40
|
+
return { contents };
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennextjs/cloudflare",
|
|
3
3
|
"description": "Cloudflare builder for next apps",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@tsconfig/strictest": "^2.0.5",
|
|
48
48
|
"@types/mock-fs": "^4.13.4",
|
|
49
49
|
"@types/node": "^22.2.0",
|
|
50
|
-
"esbuild": "^0.
|
|
50
|
+
"esbuild": "^0.24.2",
|
|
51
51
|
"eslint": "^9.11.1",
|
|
52
52
|
"eslint-plugin-import": "^2.31.0",
|
|
53
53
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
@@ -63,14 +63,14 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@ast-grep/napi": "^0.34.1",
|
|
65
65
|
"@dotenvx/dotenvx": "1.31.0",
|
|
66
|
-
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@
|
|
66
|
+
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@715",
|
|
67
67
|
"enquirer": "^2.4.1",
|
|
68
68
|
"glob": "^11.0.0",
|
|
69
69
|
"ts-morph": "^23.0.0",
|
|
70
70
|
"yaml": "^2.7.0"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"wrangler": "^3.
|
|
73
|
+
"wrangler": "^3.107.0"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"clean": "rimraf dist",
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { getPackagePath } from "@opennextjs/aws/build/helper.js";
|
|
4
|
-
import * as ts from "ts-morph";
|
|
5
|
-
import { tsParseFile } from "../../utils/index.js";
|
|
6
|
-
export function patchWranglerDeps(buildOpts) {
|
|
7
|
-
console.log("# patchWranglerDeps");
|
|
8
|
-
const { outputDir } = buildOpts;
|
|
9
|
-
const nextDistDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), "node_modules/next/dist");
|
|
10
|
-
const pagesRuntimeFile = join(nextDistDir, "compiled/next-server/pages.runtime.prod.js");
|
|
11
|
-
const patchedPagesRuntime = readFileSync(pagesRuntimeFile, "utf-8").replace(`e.exports=require("critters")`, `
|
|
12
|
-
try {
|
|
13
|
-
e.exports=require("critters");
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
console.error('critters is not installed');
|
|
17
|
-
}
|
|
18
|
-
`);
|
|
19
|
-
writeFileSync(pagesRuntimeFile, patchedPagesRuntime);
|
|
20
|
-
patchRequireReactDomServerEdge(nextDistDir);
|
|
21
|
-
// we shim @opentelemetry/api to the throwing shim so that it will throw right away, this is so that we throw inside the
|
|
22
|
-
// try block here: https://github.com/vercel/next.js/blob/9e8266a7/packages/next/src/server/lib/trace/tracer.ts#L27-L31
|
|
23
|
-
// causing the code to require the 'next/dist/compiled/@opentelemetry/api' module instead (which properly works)
|
|
24
|
-
const tracerFile = join(nextDistDir, "server/lib/trace/tracer.js");
|
|
25
|
-
const patchedTracer = readFileSync(tracerFile, "utf-8").replaceAll(/\w+\s*=\s*require\([^/]*opentelemetry.*\)/g, `throw new Error("@opentelemetry/api")`);
|
|
26
|
-
writeFileSync(tracerFile, patchedTracer);
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* `react-dom` v>=19 has a `server.edge` export: https://github.com/facebook/react/blob/a160102f3/packages/react-dom/package.json#L79
|
|
30
|
-
* but version of `react-dom` <= 18 do not have this export but have a `server.browser` export instead: https://github.com/facebook/react/blob/8a015b68/packages/react-dom/package.json#L49
|
|
31
|
-
*
|
|
32
|
-
* Next.js also try-catches importing the `server.edge` export:
|
|
33
|
-
* https://github.com/vercel/next.js/blob/6784575/packages/next/src/server/ReactDOMServerPages.js
|
|
34
|
-
*
|
|
35
|
-
* The issue here is that in the `.next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js`
|
|
36
|
-
* file for whatever reason there is a non `try-catch`ed require for the `server.edge` export
|
|
37
|
-
*
|
|
38
|
-
* This functions fixes this issue by wrapping the require in a try-catch block in the same way Next.js does it
|
|
39
|
-
* (note: this will make the build succeed but doesn't guarantee that everything will necessarily work at runtime since
|
|
40
|
-
* it's not clear what code and how might be rely on this require call)
|
|
41
|
-
*
|
|
42
|
-
*/
|
|
43
|
-
function patchRequireReactDomServerEdge(nextDistDir) {
|
|
44
|
-
// Patch .next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js
|
|
45
|
-
const pagesRuntimeFile = join(nextDistDir, "compiled/next-server/pages.runtime.prod.js");
|
|
46
|
-
const code = readFileSync(pagesRuntimeFile, "utf-8");
|
|
47
|
-
const file = tsParseFile(code);
|
|
48
|
-
// we need to update this function: `e=>{"use strict";e.exports=require("react-dom/server.edge")}`
|
|
49
|
-
file.getDescendantsOfKind(ts.SyntaxKind.ArrowFunction).forEach((arrowFunction) => {
|
|
50
|
-
// the function has a single parameter
|
|
51
|
-
const p = arrowFunction.getParameters();
|
|
52
|
-
if (p.length !== 1) {
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const parameterName = p[0].getName();
|
|
56
|
-
const bodyChildren = arrowFunction.getBody().getChildren();
|
|
57
|
-
if (!(bodyChildren.length === 3 &&
|
|
58
|
-
bodyChildren[0].getFullText() === "{" &&
|
|
59
|
-
bodyChildren[2].getFullText() === "}")) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const bodyStatements = bodyChildren[1]?.getChildren();
|
|
63
|
-
// the function has only two statements: "use strict" and e.exports=require("react-dom/server.edge")
|
|
64
|
-
if (!(bodyStatements?.length === 2 &&
|
|
65
|
-
bodyStatements.every((statement) => statement.isKind(ts.SyntaxKind.ExpressionStatement)))) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
const bodyExpressionStatements = bodyStatements;
|
|
69
|
-
const stringLiteralExpression = bodyExpressionStatements[0].getExpressionIfKind(ts.SyntaxKind.StringLiteral);
|
|
70
|
-
// the first statement needs to be "use strict"
|
|
71
|
-
if (stringLiteralExpression?.getText() !== '"use strict"') {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
// the second statement (e.exports=require("react-dom/server.edge")) needs to be a binary expression
|
|
75
|
-
const binaryExpression = bodyExpressionStatements[1].getExpressionIfKind(ts.SyntaxKind.BinaryExpression);
|
|
76
|
-
if (!binaryExpression?.getOperatorToken().isKind(ts.SyntaxKind.EqualsToken)) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
// on the left we have `${parameterName}.exports`
|
|
80
|
-
const binaryLeft = binaryExpression.getLeft();
|
|
81
|
-
if (!binaryLeft.isKind(ts.SyntaxKind.PropertyAccessExpression) ||
|
|
82
|
-
binaryLeft.getExpressionIfKind(ts.SyntaxKind.Identifier)?.getText() !== parameterName ||
|
|
83
|
-
binaryLeft.getName() !== "exports") {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// on the right we have `require("react-dom/server.edge")`
|
|
87
|
-
const binaryRight = binaryExpression.getRight();
|
|
88
|
-
if (!binaryRight.isKind(ts.SyntaxKind.CallExpression) ||
|
|
89
|
-
binaryRight.getExpressionIfKind(ts.SyntaxKind.Identifier)?.getText() !== "require") {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const requireArgs = binaryRight.getArguments();
|
|
93
|
-
if (requireArgs.length !== 1 || requireArgs[0].getText() !== '"react-dom/server.edge"') {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
arrowFunction.setBodyText(`
|
|
97
|
-
// OpenNext patch
|
|
98
|
-
let ReactDOMServer;
|
|
99
|
-
try {
|
|
100
|
-
ReactDOMServer = require('react-dom/server.edge');
|
|
101
|
-
} catch (error) {
|
|
102
|
-
if (
|
|
103
|
-
error.code !== 'MODULE_NOT_FOUND' &&
|
|
104
|
-
error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED'
|
|
105
|
-
) {
|
|
106
|
-
throw error;
|
|
107
|
-
}
|
|
108
|
-
ReactDOMServer = require('react-dom/server.browser');
|
|
109
|
-
}
|
|
110
|
-
${parameterName}.exports = ReactDOMServer;
|
|
111
|
-
`);
|
|
112
|
-
});
|
|
113
|
-
const updatedCode = file.print();
|
|
114
|
-
writeFileSync(pagesRuntimeFile, updatedCode);
|
|
115
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|