@opennextjs/cloudflare 0.3.8 → 0.3.10
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/cli/build/{index.d.ts → build.d.ts} +0 -1
- package/dist/cli/build/build.js +89 -0
- package/dist/cli/build/bundle-server.js +9 -27
- package/dist/cli/build/open-next/createServerBundle.js +3 -2
- package/dist/cli/build/patches/ast/optional-deps.d.ts +10 -0
- package/dist/cli/build/patches/ast/optional-deps.js +31 -0
- package/dist/cli/build/patches/ast/optional-deps.spec.d.ts +1 -0
- package/dist/cli/build/patches/ast/optional-deps.spec.js +89 -0
- package/dist/cli/build/patches/ast/util.d.ts +39 -0
- package/dist/cli/build/patches/ast/util.js +54 -0
- package/dist/cli/build/patches/ast/util.spec.d.ts +1 -0
- package/dist/cli/build/patches/ast/util.spec.js +43 -0
- package/dist/cli/build/patches/to-investigate/inline-eval-manifest.js +1 -1
- package/dist/cli/build/patches/to-investigate/patch-read-file.js +1 -1
- package/dist/cli/build/patches/to-investigate/wrangler-deps.js +4 -4
- package/dist/cli/build/utils/apply-patches.d.ts +12 -0
- package/dist/cli/build/utils/apply-patches.js +23 -0
- package/dist/cli/build/utils/create-config-files.d.ts +24 -0
- package/dist/cli/build/utils/create-config-files.js +85 -0
- package/dist/cli/build/utils/ensure-cf-config.d.ts +7 -0
- package/dist/cli/build/utils/ensure-cf-config.js +46 -0
- package/dist/cli/build/utils/extract-project-env-vars.spec.js +1 -1
- package/dist/cli/build/utils/index.d.ts +3 -0
- package/dist/cli/build/utils/index.js +3 -0
- package/dist/cli/config.js +2 -2
- package/dist/cli/index.js +1 -1
- package/package.json +9 -7
- package/templates/defaults/{wrangler.jsonc → wrangler.json} +1 -0
- package/dist/cli/build/index.js +0 -218
- package/dist/cli/build/utils/read-paths-recursively.d.ts +0 -7
- package/dist/cli/build/utils/read-paths-recursively.js +0 -20
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { cpSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js";
|
|
5
|
+
import { compileCache } from "@opennextjs/aws/build/compileCache.js";
|
|
6
|
+
import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
|
|
7
|
+
import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js";
|
|
8
|
+
import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js";
|
|
9
|
+
import * as buildHelper from "@opennextjs/aws/build/helper.js";
|
|
10
|
+
import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
|
|
11
|
+
import logger from "@opennextjs/aws/logger.js";
|
|
12
|
+
import { containsDotNextDir, getConfig } from "../config.js";
|
|
13
|
+
import { bundleServer } from "./bundle-server.js";
|
|
14
|
+
import { compileEnvFiles } from "./open-next/compile-env-files.js";
|
|
15
|
+
import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
|
|
16
|
+
import { createServerBundle } from "./open-next/createServerBundle.js";
|
|
17
|
+
import { createOpenNextConfigIfNotExistent, createWranglerConfigIfNotExistent, ensureCloudflareConfig, } from "./utils/index.js";
|
|
18
|
+
/**
|
|
19
|
+
* Builds the application in a format that can be passed to workerd
|
|
20
|
+
*
|
|
21
|
+
* It saves the output in a `.worker-next` directory
|
|
22
|
+
*
|
|
23
|
+
* @param projectOpts The options for the project
|
|
24
|
+
*/
|
|
25
|
+
export async function build(projectOpts) {
|
|
26
|
+
printHeader("Cloudflare build");
|
|
27
|
+
showWarningOnWindows();
|
|
28
|
+
const baseDir = projectOpts.sourceDir;
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const openNextDistDir = dirname(require.resolve("@opennextjs/aws/index.js"));
|
|
31
|
+
await createOpenNextConfigIfNotExistent(projectOpts);
|
|
32
|
+
const { config, buildDir } = await compileOpenNextConfig(baseDir);
|
|
33
|
+
ensureCloudflareConfig(config);
|
|
34
|
+
// Initialize options
|
|
35
|
+
const options = buildHelper.normalizeOptions(config, openNextDistDir, buildDir);
|
|
36
|
+
logger.setLevel(options.debug ? "debug" : "info");
|
|
37
|
+
// Do not minify the code so that we can apply string replacement patch.
|
|
38
|
+
// Note that wrangler will still minify the bundle.
|
|
39
|
+
options.minify = false;
|
|
40
|
+
// Pre-build validation
|
|
41
|
+
buildHelper.checkRunningInsideNextjsApp(options);
|
|
42
|
+
logger.info(`App directory: ${options.appPath}`);
|
|
43
|
+
buildHelper.printNextjsVersion(options);
|
|
44
|
+
ensureNextjsVersionSupported(options);
|
|
45
|
+
buildHelper.printOpenNextVersion(options);
|
|
46
|
+
if (projectOpts.skipNextBuild) {
|
|
47
|
+
logger.warn("Skipping Next.js build");
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Build the next app
|
|
51
|
+
printHeader("Building Next.js app");
|
|
52
|
+
setStandaloneBuildMode(options);
|
|
53
|
+
buildNextjsApp(options);
|
|
54
|
+
}
|
|
55
|
+
if (!containsDotNextDir(projectOpts.sourceDir)) {
|
|
56
|
+
throw new Error(`.next folder not found in ${projectOpts.sourceDir}`);
|
|
57
|
+
}
|
|
58
|
+
// Generate deployable bundle
|
|
59
|
+
printHeader("Generating bundle");
|
|
60
|
+
buildHelper.initOutputDir(options);
|
|
61
|
+
// Compile cache.ts
|
|
62
|
+
compileCache(options);
|
|
63
|
+
// Compile .env files
|
|
64
|
+
compileEnvFiles(options);
|
|
65
|
+
// Compile middleware
|
|
66
|
+
await createMiddleware(options, { forceOnlyBuildOnce: true });
|
|
67
|
+
createStaticAssets(options);
|
|
68
|
+
if (config.dangerous?.disableIncrementalCache !== true) {
|
|
69
|
+
createCacheAssets(options);
|
|
70
|
+
copyCacheAssets(options);
|
|
71
|
+
}
|
|
72
|
+
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);
|
|
79
|
+
if (!projectOpts.skipWranglerConfigCheck) {
|
|
80
|
+
await createWranglerConfigIfNotExistent(projectOpts);
|
|
81
|
+
}
|
|
82
|
+
logger.info("OpenNext build complete.");
|
|
83
|
+
}
|
|
84
|
+
function ensureNextjsVersionSupported(options) {
|
|
85
|
+
if (buildHelper.compareSemver(options.nextVersion, "14.0.0") < 0) {
|
|
86
|
+
logger.error("Next.js version unsupported, please upgrade to version 14 or greater.");
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -2,9 +2,12 @@ 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 { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
|
|
5
7
|
import { build } from "esbuild";
|
|
8
|
+
import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
|
|
6
9
|
import * as patches from "./patches/index.js";
|
|
7
|
-
import { normalizePath } from "./utils/index.js";
|
|
10
|
+
import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
|
|
8
11
|
/** The dist directory of the Cloudflare adapter package */
|
|
9
12
|
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10
13
|
/**
|
|
@@ -31,7 +34,7 @@ export async function bundleServer(config, openNextOptions) {
|
|
|
31
34
|
target: "esnext",
|
|
32
35
|
minify: false,
|
|
33
36
|
plugins: [createFixRequiresESBuildPlugin(config)],
|
|
34
|
-
external: ["./middleware/handler.mjs"],
|
|
37
|
+
external: ["./middleware/handler.mjs", "caniuse-lite"],
|
|
35
38
|
alias: {
|
|
36
39
|
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
|
|
37
40
|
// eval("require")("bufferutil");
|
|
@@ -149,42 +152,21 @@ async function updateWorkerBundledCode(workerOutputFile, config, openNextOptions
|
|
|
149
152
|
(code) => code.replace('require.resolve("./cache.cjs")', '"unused"'),
|
|
150
153
|
],
|
|
151
154
|
]);
|
|
152
|
-
|
|
155
|
+
const bundle = parse(Lang.TypeScript, patchedCode).root();
|
|
156
|
+
const edits = patchOptionalDependencies(bundle);
|
|
157
|
+
await writeFile(workerOutputFile, bundle.commitEdits(edits));
|
|
153
158
|
}
|
|
154
159
|
function createFixRequiresESBuildPlugin(config) {
|
|
155
160
|
return {
|
|
156
161
|
name: "replaceRelative",
|
|
157
162
|
setup(build) {
|
|
158
163
|
// Note: we (empty) shim require-hook modules as they generate problematic code that uses requires
|
|
159
|
-
build.onResolve({ filter:
|
|
164
|
+
build.onResolve({ filter: getCrossPlatformPathRegex(String.raw `^\./require-hook$`, { escape: false }) }, () => ({
|
|
160
165
|
path: path.join(config.paths.internal.templates, "shims", "empty.js"),
|
|
161
166
|
}));
|
|
162
167
|
},
|
|
163
168
|
};
|
|
164
169
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Applies multiple code patches in order to a given piece of code, at each step it validates that the code
|
|
167
|
-
* has actually been patched/changed, if not an error is thrown
|
|
168
|
-
*
|
|
169
|
-
* @param code the code to apply the patches to
|
|
170
|
-
* @param patches array of tuples, containing a string indicating the target of the patching (for logging) and
|
|
171
|
-
* a patching function that takes a string (pre-patch code) and returns a string (post-patch code)
|
|
172
|
-
* @returns the patched code
|
|
173
|
-
*/
|
|
174
|
-
async function patchCodeWithValidations(code, patches) {
|
|
175
|
-
console.log(`Applying code patches:`);
|
|
176
|
-
let patchedCode = code;
|
|
177
|
-
for (const [target, patchFunction, opts] of patches) {
|
|
178
|
-
console.log(` - patching ${target}`);
|
|
179
|
-
const prePatchCode = patchedCode;
|
|
180
|
-
patchedCode = await patchFunction(patchedCode);
|
|
181
|
-
if (!opts?.isOptional && prePatchCode === patchedCode) {
|
|
182
|
-
throw new Error(`Failed to patch ${target}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
console.log(`All ${patches.length} patches applied\n`);
|
|
186
|
-
return patchedCode;
|
|
187
|
-
}
|
|
188
170
|
/**
|
|
189
171
|
* Gets the path of the worker.js file generated by the build process
|
|
190
172
|
*
|
|
@@ -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: /
|
|
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: /
|
|
129
|
+
target: getCrossPlatformPathRegex("core/util.js"),
|
|
129
130
|
deletes: [
|
|
130
131
|
...(disableNextPrebundledReact ? ["requireHooks"] : []),
|
|
131
132
|
...(isBefore13413 ? ["trustHostHeader"] : ["requestHandlerHost"]),
|
|
@@ -0,0 +1,10 @@
|
|
|
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): import("@ast-grep/napi").Edit[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getRuleEdits } 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 getRuleEdits(optionalDepRule, root);
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { optionalDepRule } from "./optional-deps.js";
|
|
3
|
+
import { patchCode } from "./util.js";
|
|
4
|
+
describe("optional dependecy", () => {
|
|
5
|
+
it('should wrap a top-level require("caniuse-lite") in a try-catch', () => {
|
|
6
|
+
const code = `t = require("caniuse-lite");`;
|
|
7
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
|
|
8
|
+
"try {
|
|
9
|
+
t = require("caniuse-lite");
|
|
10
|
+
} catch {
|
|
11
|
+
throw new Error('The optional dependency "caniuse-lite" is not installed');
|
|
12
|
+
};"
|
|
13
|
+
`);
|
|
14
|
+
});
|
|
15
|
+
it('should wrap a top-level require("caniuse-lite/data") in a try-catch', () => {
|
|
16
|
+
const code = `t = require("caniuse-lite/data");`;
|
|
17
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
|
|
18
|
+
"try {
|
|
19
|
+
t = require("caniuse-lite/data");
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error('The optional dependency "caniuse-lite/data" is not installed');
|
|
22
|
+
};"
|
|
23
|
+
`);
|
|
24
|
+
});
|
|
25
|
+
it('should wrap e.exports = require("caniuse-lite") in a try-catch', () => {
|
|
26
|
+
const code = 'e.exports = require("caniuse-lite");';
|
|
27
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
|
|
28
|
+
"try {
|
|
29
|
+
e.exports = require("caniuse-lite");
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error('The optional dependency "caniuse-lite" is not installed');
|
|
32
|
+
};"
|
|
33
|
+
`);
|
|
34
|
+
});
|
|
35
|
+
it('should wrap module.exports = require("caniuse-lite") in a try-catch', () => {
|
|
36
|
+
const code = 'module.exports = require("caniuse-lite");';
|
|
37
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
|
|
38
|
+
"try {
|
|
39
|
+
module.exports = require("caniuse-lite");
|
|
40
|
+
} catch {
|
|
41
|
+
throw new Error('The optional dependency "caniuse-lite" is not installed');
|
|
42
|
+
};"
|
|
43
|
+
`);
|
|
44
|
+
});
|
|
45
|
+
it('should wrap exports.foo = require("caniuse-lite") in a try-catch', () => {
|
|
46
|
+
const code = 'exports.foo = require("caniuse-lite");';
|
|
47
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
|
|
48
|
+
"try {
|
|
49
|
+
exports.foo = require("caniuse-lite");
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error('The optional dependency "caniuse-lite" is not installed');
|
|
52
|
+
};"
|
|
53
|
+
`);
|
|
54
|
+
});
|
|
55
|
+
it('should not wrap require("lodash") in a try-catch', () => {
|
|
56
|
+
const code = 't = require("lodash");';
|
|
57
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`"t = require("lodash");"`);
|
|
58
|
+
});
|
|
59
|
+
it('should not wrap require("other-module") if it does not match caniuse-lite regex', () => {
|
|
60
|
+
const code = 't = require("other-module");';
|
|
61
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`"t = require("other-module");"`);
|
|
62
|
+
});
|
|
63
|
+
it("should not wrap a require() call already inside a try-catch", () => {
|
|
64
|
+
const code = `
|
|
65
|
+
try {
|
|
66
|
+
const t = require("caniuse-lite");
|
|
67
|
+
} catch {}
|
|
68
|
+
`;
|
|
69
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
|
|
70
|
+
"try {
|
|
71
|
+
const t = require("caniuse-lite");
|
|
72
|
+
} catch {}
|
|
73
|
+
"
|
|
74
|
+
`);
|
|
75
|
+
});
|
|
76
|
+
it("should handle require with subpath and not wrap if already in try-catch", () => {
|
|
77
|
+
const code = `
|
|
78
|
+
try {
|
|
79
|
+
const t = require("caniuse-lite/path");
|
|
80
|
+
} catch {}
|
|
81
|
+
`;
|
|
82
|
+
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
|
|
83
|
+
"try {
|
|
84
|
+
const t = require("caniuse-lite/path");
|
|
85
|
+
} catch {}
|
|
86
|
+
"
|
|
87
|
+
`);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Edit, Lang, type NapiConfig, type SgNode } from "@ast-grep/napi";
|
|
2
|
+
/**
|
|
3
|
+
* fix has the same meaning as in yaml rules
|
|
4
|
+
* see https://ast-grep.github.io/guide/rewrite-code.html#using-fix-in-yaml-rule
|
|
5
|
+
*/
|
|
6
|
+
export type RuleConfig = NapiConfig & {
|
|
7
|
+
fix?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Returns the `Edit`s for an ast-grep rule in yaml format
|
|
11
|
+
*
|
|
12
|
+
* The rule must have a `fix` to rewrite the matched node.
|
|
13
|
+
*
|
|
14
|
+
* Tip: use https://ast-grep.github.io/playground.html to create rules.
|
|
15
|
+
*
|
|
16
|
+
* @param rule The rule. Either a yaml string or an instance of `RuleConfig`
|
|
17
|
+
* @param root The root node
|
|
18
|
+
* @param once only apply once
|
|
19
|
+
* @returns A list of edits.
|
|
20
|
+
*/
|
|
21
|
+
export declare function getRuleEdits(rule: string | RuleConfig, root: SgNode, { once }?: {
|
|
22
|
+
once?: boolean | undefined;
|
|
23
|
+
}): Edit[];
|
|
24
|
+
/**
|
|
25
|
+
* Patches the code from by applying the rule.
|
|
26
|
+
*
|
|
27
|
+
* This function is mainly for on off edits and tests,
|
|
28
|
+
* use `getRuleEdits` to apply multiple rules.
|
|
29
|
+
*
|
|
30
|
+
* @param code The source code
|
|
31
|
+
* @param rule The astgrep rule (yaml or NapiConfig)
|
|
32
|
+
* @param lang The language used by the source code
|
|
33
|
+
* @param lang Whether to apply the rule only once
|
|
34
|
+
* @returns The patched code
|
|
35
|
+
*/
|
|
36
|
+
export declare function patchCode(code: string, rule: string | RuleConfig, { lang, once }?: {
|
|
37
|
+
lang?: Lang | undefined;
|
|
38
|
+
once?: boolean | undefined;
|
|
39
|
+
}): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Lang, parse } from "@ast-grep/napi";
|
|
2
|
+
import yaml from "yaml";
|
|
3
|
+
/**
|
|
4
|
+
* Returns the `Edit`s for an ast-grep rule in yaml format
|
|
5
|
+
*
|
|
6
|
+
* The rule must have a `fix` to rewrite the matched node.
|
|
7
|
+
*
|
|
8
|
+
* Tip: use https://ast-grep.github.io/playground.html to create rules.
|
|
9
|
+
*
|
|
10
|
+
* @param rule The rule. Either a yaml string or an instance of `RuleConfig`
|
|
11
|
+
* @param root The root node
|
|
12
|
+
* @param once only apply once
|
|
13
|
+
* @returns A list of edits.
|
|
14
|
+
*/
|
|
15
|
+
export function getRuleEdits(rule, root, { once = false } = {}) {
|
|
16
|
+
const ruleConfig = typeof rule === "string" ? yaml.parse(rule) : rule;
|
|
17
|
+
if (ruleConfig.transform) {
|
|
18
|
+
throw new Error("transform is not supported");
|
|
19
|
+
}
|
|
20
|
+
if (!ruleConfig.fix) {
|
|
21
|
+
throw new Error("no fix to apply");
|
|
22
|
+
}
|
|
23
|
+
const fix = ruleConfig.fix;
|
|
24
|
+
const matches = once ? [root.find(ruleConfig)].filter((m) => m !== null) : root.findAll(ruleConfig);
|
|
25
|
+
const edits = [];
|
|
26
|
+
matches.forEach((match) => {
|
|
27
|
+
edits.push(match.replace(
|
|
28
|
+
// Replace known placeholders by their value
|
|
29
|
+
fix
|
|
30
|
+
.replace(/\$\$\$([A-Z0-9_]+)/g, (_m, name) => match
|
|
31
|
+
.getMultipleMatches(name)
|
|
32
|
+
.map((n) => n.text())
|
|
33
|
+
.join(""))
|
|
34
|
+
.replace(/\$([A-Z0-9_]+)/g, (m, name) => match.getMatch(name)?.text() ?? m)));
|
|
35
|
+
});
|
|
36
|
+
return edits;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Patches the code from by applying the rule.
|
|
40
|
+
*
|
|
41
|
+
* This function is mainly for on off edits and tests,
|
|
42
|
+
* use `getRuleEdits` to apply multiple rules.
|
|
43
|
+
*
|
|
44
|
+
* @param code The source code
|
|
45
|
+
* @param rule The astgrep rule (yaml or NapiConfig)
|
|
46
|
+
* @param lang The language used by the source code
|
|
47
|
+
* @param lang Whether to apply the rule only once
|
|
48
|
+
* @returns The patched code
|
|
49
|
+
*/
|
|
50
|
+
export function patchCode(code, rule, { lang = Lang.TypeScript, once = false } = {}) {
|
|
51
|
+
const node = parse(lang, code).root();
|
|
52
|
+
const edits = getRuleEdits(rule, node, { once });
|
|
53
|
+
return node.commitEdits(edits);
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { patchCode } from "./util.js";
|
|
3
|
+
describe("patchCode", () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.clearAllMocks();
|
|
6
|
+
});
|
|
7
|
+
it("should throw an error if rule has a transform", () => {
|
|
8
|
+
expect(() => patchCode(`console.log("hi")`, { rule: { pattern: "console.log($MSG)" }, transform: "not supported" })).toThrow(/not supported/);
|
|
9
|
+
});
|
|
10
|
+
it("should throw an error if rule has no fix", () => {
|
|
11
|
+
expect(() => patchCode(`console.log("hi")`, { rule: { pattern: "console.log($MSG)" } })).toThrow(/no fix/);
|
|
12
|
+
});
|
|
13
|
+
it("should accept yaml rules", () => {
|
|
14
|
+
const yamlRule = `
|
|
15
|
+
rule:
|
|
16
|
+
pattern: a
|
|
17
|
+
fix: b
|
|
18
|
+
`;
|
|
19
|
+
expect(patchCode(`a`, yamlRule)).toEqual("b");
|
|
20
|
+
});
|
|
21
|
+
it("should apply fix to a single match when once is true", () => {
|
|
22
|
+
expect(patchCode(`a+a`, { rule: { pattern: "a" }, fix: "b" }, { once: true })).toEqual("b+a");
|
|
23
|
+
});
|
|
24
|
+
it("should apply fix to all matches when once is false (default)", () => {
|
|
25
|
+
expect(patchCode(`a+a`, { rule: { pattern: "a" }, fix: "b" })).toEqual("b+b");
|
|
26
|
+
expect(patchCode(`a+a`, { rule: { pattern: "a" }, fix: "b" }, { once: false })).toEqual("b+b");
|
|
27
|
+
});
|
|
28
|
+
it("should handle no matches", () => {
|
|
29
|
+
expect(patchCode(`a`, { rule: { pattern: "b" }, fix: "c" })).toEqual("a");
|
|
30
|
+
});
|
|
31
|
+
it("should replace $PLACEHOLDER with match text", () => {
|
|
32
|
+
expect(patchCode(`console.log(message)`, { rule: { pattern: "console.log($MSG)" }, fix: "$MSG" })).toEqual("message");
|
|
33
|
+
});
|
|
34
|
+
it("should handle $PLACEHODLERS that are not found in matches", () => {
|
|
35
|
+
expect(patchCode(`console.log(message)`, { rule: { pattern: "console.log($MSG)" }, fix: "$WHAT$$$WHAT" })).toEqual("$WHAT");
|
|
36
|
+
});
|
|
37
|
+
it("should replace $$$PLACEHOLDER with match text", () => {
|
|
38
|
+
expect(patchCode(`console.log("hello" + world, "!")`, {
|
|
39
|
+
rule: { pattern: "console.log($$$ARGS)" },
|
|
40
|
+
fix: "$$$ARGS",
|
|
41
|
+
})).toEqual(`"hello" + world,"!"`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -9,7 +9,7 @@ import { normalizePath } from "../../utils/index.js";
|
|
|
9
9
|
* there is a vm `runInNewContext` call which we also don't support (source: https://github.com/vercel/next.js/blob/b1e32c5d1f/packages/next/src/server/load-manifest.ts#L88)
|
|
10
10
|
*/
|
|
11
11
|
export function inlineEvalManifest(code, config) {
|
|
12
|
-
const manifestJss = globSync(normalizePath(join(config.paths.output.standaloneAppDotNext, "
|
|
12
|
+
const manifestJss = globSync(normalizePath(join(config.paths.output.standaloneAppDotNext, "**/*_client-reference-manifest.js"))).map((file) => normalizePath(file).replace(normalizePath(config.paths.output.standaloneApp) + posix.sep, ""));
|
|
13
13
|
return code.replace(/function evalManifest\((.+?), .+?\) {/, `$&
|
|
14
14
|
${manifestJss
|
|
15
15
|
.map((manifestJs) => `
|
|
@@ -15,7 +15,7 @@ export function patchLoadManifest(code, config) {
|
|
|
15
15
|
// Same as patchBuildId, the next-server code loads the manifests with `readFileSync` and we want to avoid that
|
|
16
16
|
// (source: https://github.com/vercel/next.js/blob/15aeb92e/packages/next/src/server/load-manifest.ts#L34-L56)
|
|
17
17
|
// Note: we could/should probably just patch readFileSync here or something!
|
|
18
|
-
const manifestJsons = globSync(normalizePath(join(config.paths.output.standaloneAppDotNext, "
|
|
18
|
+
const manifestJsons = globSync(normalizePath(join(config.paths.output.standaloneAppDotNext, "**/*-manifest.json"))).map((file) => normalizePath(file).replace(normalizePath(config.paths.output.standaloneApp) + posix.sep, ""));
|
|
19
19
|
return code.replace(/function loadManifest\((.+?), .+?\) {/, `$&
|
|
20
20
|
${manifestJsons
|
|
21
21
|
.map((manifestJson) => `
|
|
@@ -12,7 +12,7 @@ export function patchWranglerDeps(config) {
|
|
|
12
12
|
// [alias]
|
|
13
13
|
// # critters is `require`d from `pages.runtime.prod.js` when running wrangler dev, so we need to stub it out
|
|
14
14
|
// "critters" = "./.next/standalone/node_modules/cf/templates/shims/empty.ts"
|
|
15
|
-
const pagesRuntimeFile = join(distPath, "compiled
|
|
15
|
+
const pagesRuntimeFile = join(distPath, "compiled/next-server/pages.runtime.prod.js");
|
|
16
16
|
const patchedPagesRuntime = readFileSync(pagesRuntimeFile, "utf-8").replace(`e.exports=require("critters")`, `e.exports={}`);
|
|
17
17
|
writeFileSync(pagesRuntimeFile, patchedPagesRuntime);
|
|
18
18
|
patchRequireReactDomServerEdge(config);
|
|
@@ -26,7 +26,7 @@ export function patchWranglerDeps(config) {
|
|
|
26
26
|
// # try block here: https://github.com/vercel/next.js/blob/9e8266a7/packages/next/src/server/lib/trace/tracer.ts#L27-L31
|
|
27
27
|
// # causing the code to require the 'next/dist/compiled/@opentelemetry/api' module instead (which properly works)
|
|
28
28
|
// #"@opentelemetry/api" = "./.next/standalone/node_modules/cf/templates/shims/throw.ts"
|
|
29
|
-
const tracerFile = join(distPath, "server
|
|
29
|
+
const tracerFile = join(distPath, "server/lib/trace/tracer.js");
|
|
30
30
|
const patchedTracer = readFileSync(tracerFile, "utf-8").replaceAll(/\w+\s*=\s*require\([^/]*opentelemetry.*\)/g, `throw new Error("@opentelemetry/api")`);
|
|
31
31
|
writeFileSync(tracerFile, patchedTracer);
|
|
32
32
|
}
|
|
@@ -45,7 +45,7 @@ export function patchWranglerDeps(config) {
|
|
|
45
45
|
function getDistPath(config) {
|
|
46
46
|
for (const root of [config.paths.output.standaloneApp, config.paths.output.standaloneRoot]) {
|
|
47
47
|
try {
|
|
48
|
-
const distPath = join(root, "node_modules
|
|
48
|
+
const distPath = join(root, "node_modules/next/dist");
|
|
49
49
|
if (statSync(distPath).isDirectory())
|
|
50
50
|
return distPath;
|
|
51
51
|
}
|
|
@@ -73,7 +73,7 @@ function getDistPath(config) {
|
|
|
73
73
|
function patchRequireReactDomServerEdge(config) {
|
|
74
74
|
const distPath = getDistPath(config);
|
|
75
75
|
// Patch .next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js
|
|
76
|
-
const pagesRuntimeFile = join(distPath, "compiled
|
|
76
|
+
const pagesRuntimeFile = join(distPath, "compiled/next-server/pages.runtime.prod.js");
|
|
77
77
|
const code = readFileSync(pagesRuntimeFile, "utf-8");
|
|
78
78
|
const file = tsParseFile(code);
|
|
79
79
|
// we need to update this function: `e=>{"use strict";e.exports=require("react-dom/server.edge")}`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies multiple code patches in order to a given piece of code, at each step it validates that the code
|
|
3
|
+
* has actually been patched/changed, if not an error is thrown
|
|
4
|
+
*
|
|
5
|
+
* @param code the code to apply the patches to
|
|
6
|
+
* @param patches array of tuples, containing a string indicating the target of the patching (for logging) and
|
|
7
|
+
* a patching function that takes a string (pre-patch code) and returns a string (post-patch code)
|
|
8
|
+
* @returns the patched code
|
|
9
|
+
*/
|
|
10
|
+
export declare function patchCodeWithValidations(code: string, patches: [string, (code: string) => string | Promise<string>, opts?: {
|
|
11
|
+
isOptional?: boolean;
|
|
12
|
+
}][]): Promise<string>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies multiple code patches in order to a given piece of code, at each step it validates that the code
|
|
3
|
+
* has actually been patched/changed, if not an error is thrown
|
|
4
|
+
*
|
|
5
|
+
* @param code the code to apply the patches to
|
|
6
|
+
* @param patches array of tuples, containing a string indicating the target of the patching (for logging) and
|
|
7
|
+
* a patching function that takes a string (pre-patch code) and returns a string (post-patch code)
|
|
8
|
+
* @returns the patched code
|
|
9
|
+
*/
|
|
10
|
+
export async function patchCodeWithValidations(code, patches) {
|
|
11
|
+
console.log(`Applying code patches:`);
|
|
12
|
+
let patchedCode = code;
|
|
13
|
+
for (const [target, patchFunction, opts] of patches) {
|
|
14
|
+
console.log(` - patching ${target}`);
|
|
15
|
+
const prePatchCode = patchedCode;
|
|
16
|
+
patchedCode = await patchFunction(patchedCode);
|
|
17
|
+
if (!opts?.isOptional && prePatchCode === patchedCode) {
|
|
18
|
+
throw new Error(`Failed to patch ${target}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
console.log(`All ${patches.length} patches applied\n`);
|
|
22
|
+
return patchedCode;
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProjectOptions } from "../../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Creates a `wrangler.json` file for the user if a wrangler config file doesn't already exist,
|
|
4
|
+
* but only after asking for the user's confirmation.
|
|
5
|
+
*
|
|
6
|
+
* If the user refuses a warning is shown (which offers ways to opt out of this check to the user).
|
|
7
|
+
*
|
|
8
|
+
* Note: we generate a wrangler.json file with comments instead of using the jsonc extension,
|
|
9
|
+
* we decided to do that since json is more common than jsonc, wrangler also parses
|
|
10
|
+
* them in the same way and we also expect developers to associate `wrangler.json`
|
|
11
|
+
* files to the jsonc language
|
|
12
|
+
*
|
|
13
|
+
* @param projectOpts The options for the project
|
|
14
|
+
*/
|
|
15
|
+
export declare function createWranglerConfigIfNotExistent(projectOpts: ProjectOptions): Promise<void>;
|
|
16
|
+
export declare function getLatestCompatDate(): Promise<string | undefined>;
|
|
17
|
+
/**
|
|
18
|
+
* Creates a `open-next.config.ts` file for the user if it doesn't exist, but only after asking for the user's confirmation.
|
|
19
|
+
*
|
|
20
|
+
* If the user refuses an error is thrown (since the file is mandatory).
|
|
21
|
+
*
|
|
22
|
+
* @param projectOpts The options for the project
|
|
23
|
+
*/
|
|
24
|
+
export declare function createOpenNextConfigIfNotExistent(projectOpts: ProjectOptions): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getPackageTemplatesDirPath } from "../../../utils/get-package-templates-dir-path.js";
|
|
4
|
+
import { askConfirmation } from "../../utils/ask-confirmation.js";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a `wrangler.json` file for the user if a wrangler config file doesn't already exist,
|
|
7
|
+
* but only after asking for the user's confirmation.
|
|
8
|
+
*
|
|
9
|
+
* If the user refuses a warning is shown (which offers ways to opt out of this check to the user).
|
|
10
|
+
*
|
|
11
|
+
* Note: we generate a wrangler.json file with comments instead of using the jsonc extension,
|
|
12
|
+
* we decided to do that since json is more common than jsonc, wrangler also parses
|
|
13
|
+
* them in the same way and we also expect developers to associate `wrangler.json`
|
|
14
|
+
* files to the jsonc language
|
|
15
|
+
*
|
|
16
|
+
* @param projectOpts The options for the project
|
|
17
|
+
*/
|
|
18
|
+
export async function createWranglerConfigIfNotExistent(projectOpts) {
|
|
19
|
+
const possibleExts = ["toml", "json", "jsonc"];
|
|
20
|
+
const wranglerConfigFileExists = possibleExts.some((ext) => existsSync(join(projectOpts.sourceDir, `wrangler.${ext}`)));
|
|
21
|
+
if (wranglerConfigFileExists) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const answer = await askConfirmation("No `wrangler.(toml|json|jsonc)` config file found, do you want to create one?");
|
|
25
|
+
if (!answer) {
|
|
26
|
+
console.warn("No Wrangler config file created" +
|
|
27
|
+
"\n" +
|
|
28
|
+
"(to avoid this check use the `--skipWranglerConfigCheck` flag or set a `SKIP_WRANGLER_CONFIG_CHECK` environment variable to `yes`)");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
let wranglerConfig = readFileSync(join(getPackageTemplatesDirPath(), "defaults/wrangler.json"), "utf8");
|
|
32
|
+
const appName = getAppNameFromPackageJson(projectOpts.sourceDir) ?? "app-name";
|
|
33
|
+
if (appName) {
|
|
34
|
+
wranglerConfig = wranglerConfig.replace('"app-name"', JSON.stringify(appName.replaceAll("_", "-")));
|
|
35
|
+
}
|
|
36
|
+
const compatDate = await getLatestCompatDate();
|
|
37
|
+
if (compatDate) {
|
|
38
|
+
wranglerConfig = wranglerConfig.replace(/"compatibility_date": "\d{4}-\d{2}-\d{2}"/, `"compatibility_date": ${JSON.stringify(compatDate)}`);
|
|
39
|
+
}
|
|
40
|
+
writeFileSync(join(projectOpts.sourceDir, "wrangler.json"), wranglerConfig);
|
|
41
|
+
}
|
|
42
|
+
function getAppNameFromPackageJson(sourceDir) {
|
|
43
|
+
try {
|
|
44
|
+
const packageJsonStr = readFileSync(join(sourceDir, "package.json"), "utf8");
|
|
45
|
+
const packageJson = JSON.parse(packageJsonStr);
|
|
46
|
+
if (typeof packageJson.name === "string")
|
|
47
|
+
return packageJson.name;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* empty */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function getLatestCompatDate() {
|
|
54
|
+
try {
|
|
55
|
+
const resp = await fetch(`https://registry.npmjs.org/workerd`);
|
|
56
|
+
const latestWorkerdVersion = (await resp.json())["dist-tags"].latest;
|
|
57
|
+
// The format of the workerd version is `major.yyyymmdd.patch`.
|
|
58
|
+
const match = latestWorkerdVersion.match(/\d+\.(\d{4})(\d{2})(\d{2})\.\d+/);
|
|
59
|
+
if (match) {
|
|
60
|
+
const [, year, month, date] = match;
|
|
61
|
+
const compatDate = `${year}-${month}-${date}`;
|
|
62
|
+
return compatDate;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* empty */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Creates a `open-next.config.ts` file for the user if it doesn't exist, but only after asking for the user's confirmation.
|
|
71
|
+
*
|
|
72
|
+
* If the user refuses an error is thrown (since the file is mandatory).
|
|
73
|
+
*
|
|
74
|
+
* @param projectOpts The options for the project
|
|
75
|
+
*/
|
|
76
|
+
export async function createOpenNextConfigIfNotExistent(projectOpts) {
|
|
77
|
+
const openNextConfigPath = join(projectOpts.sourceDir, "open-next.config.ts");
|
|
78
|
+
if (!existsSync(openNextConfigPath)) {
|
|
79
|
+
const answer = await askConfirmation("Missing required `open-next.config.ts` file, do you want to create one?");
|
|
80
|
+
if (!answer) {
|
|
81
|
+
throw new Error("The `open-next.config.ts` file is required, aborting!");
|
|
82
|
+
}
|
|
83
|
+
cpSync(join(getPackageTemplatesDirPath(), "defaults/open-next.config.ts"), openNextConfigPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures open next is configured for cloudflare.
|
|
3
|
+
*
|
|
4
|
+
* @param config OpenNext configuration.
|
|
5
|
+
*/
|
|
6
|
+
export function ensureCloudflareConfig(config) {
|
|
7
|
+
const requirements = {
|
|
8
|
+
dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node",
|
|
9
|
+
dftUseEdgeConverter: config.default?.override?.converter === "edge",
|
|
10
|
+
dftMaybeUseCache: config.default?.override?.incrementalCache === "dummy" ||
|
|
11
|
+
typeof config.default?.override?.incrementalCache === "function",
|
|
12
|
+
dftUseDummyTagCacheAndQueue: config.default?.override?.tagCache === "dummy" && config.default?.override?.queue === "dummy",
|
|
13
|
+
disableCacheInterception: config.dangerous?.enableCacheInterception !== true,
|
|
14
|
+
mwIsMiddlewareExternal: config.middleware?.external == true,
|
|
15
|
+
mwUseCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare-edge",
|
|
16
|
+
mwUseEdgeConverter: config.middleware?.override?.converter === "edge",
|
|
17
|
+
mwUseFetchProxy: config.middleware?.override?.proxyExternalRequest === "fetch",
|
|
18
|
+
};
|
|
19
|
+
if (Object.values(requirements).some((satisfied) => !satisfied)) {
|
|
20
|
+
throw new Error("The `open-next.config.ts` should have a default export like this:\n\n" +
|
|
21
|
+
`{
|
|
22
|
+
default: {
|
|
23
|
+
override: {
|
|
24
|
+
wrapper: "cloudflare-node",
|
|
25
|
+
converter: "edge",
|
|
26
|
+
incrementalCache: "dummy" | function,
|
|
27
|
+
tagCache: "dummy",
|
|
28
|
+
queue: "dummy",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
middleware: {
|
|
33
|
+
external: true,
|
|
34
|
+
override: {
|
|
35
|
+
wrapper: "cloudflare-edge",
|
|
36
|
+
converter: "edge",
|
|
37
|
+
proxyExternalRequest: "fetch",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"dangerous": {
|
|
42
|
+
"enableCacheInterception": false
|
|
43
|
+
},
|
|
44
|
+
}\n\n`.replace(/^ {8}/gm, ""));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import mockFs from "mock-fs";
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
-
import { extractProjectEnvVars } from "./extract-project-env-vars";
|
|
4
|
+
import { extractProjectEnvVars } from "./extract-project-env-vars.js";
|
|
5
5
|
const options = { monorepoRoot: "", appPath: "" };
|
|
6
6
|
describe("extractProjectEnvVars", () => {
|
|
7
7
|
beforeEach(() => {
|
package/dist/cli/config.js
CHANGED
|
@@ -17,7 +17,7 @@ export function getConfig(projectOpts) {
|
|
|
17
17
|
const standaloneAppServer = join(standaloneAppDotNext, "server");
|
|
18
18
|
const nodeModules = join(standaloneApp, "node_modules");
|
|
19
19
|
const internalPackage = join(nodeModules, ...PACKAGE_NAME.split("/"));
|
|
20
|
-
const internalTemplates = join(internalPackage, "cli
|
|
20
|
+
const internalTemplates = join(internalPackage, "cli/templates");
|
|
21
21
|
return {
|
|
22
22
|
build: {
|
|
23
23
|
skipNextBuild: projectOpts.skipNextBuild,
|
|
@@ -72,7 +72,7 @@ function getNextjsApplicationPath(dotNextDir) {
|
|
|
72
72
|
}
|
|
73
73
|
function findServerParentPath(parentPath) {
|
|
74
74
|
try {
|
|
75
|
-
if (statSync(join(parentPath, ".next
|
|
75
|
+
if (statSync(join(parentPath, ".next/server")).isDirectory()) {
|
|
76
76
|
return parentPath;
|
|
77
77
|
}
|
|
78
78
|
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { getArgs } from "./args.js";
|
|
4
|
-
import { build } from "./build/
|
|
4
|
+
import { build } from "./build/build.js";
|
|
5
5
|
const nextAppDir = process.cwd();
|
|
6
6
|
const { skipNextBuild, skipWranglerConfigCheck, outputDir, minify } = getArgs();
|
|
7
7
|
await build({
|
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.3.
|
|
4
|
+
"version": "0.3.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -42,33 +42,35 @@
|
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@cloudflare/workers-types": "^4.
|
|
45
|
+
"@cloudflare/workers-types": "^4.20241230.0",
|
|
46
46
|
"@eslint/js": "^9.11.1",
|
|
47
47
|
"@tsconfig/strictest": "^2.0.5",
|
|
48
48
|
"@types/mock-fs": "^4.13.4",
|
|
49
49
|
"@types/node": "^22.2.0",
|
|
50
50
|
"esbuild": "^0.23.0",
|
|
51
|
+
"eslint": "^9.11.1",
|
|
51
52
|
"eslint-plugin-import": "^2.31.0",
|
|
52
53
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
53
54
|
"eslint-plugin-unicorn": "^55.0.0",
|
|
54
|
-
"eslint": "^9.11.1",
|
|
55
55
|
"globals": "^15.9.0",
|
|
56
56
|
"mock-fs": "^5.4.1",
|
|
57
57
|
"next": "14.2.11",
|
|
58
58
|
"rimraf": "^6.0.1",
|
|
59
|
+
"typescript": "^5.7.3",
|
|
59
60
|
"typescript-eslint": "^8.7.0",
|
|
60
|
-
"typescript": "^5.5.4",
|
|
61
61
|
"vitest": "^2.1.1"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
+
"@ast-grep/napi": "^0.33.1",
|
|
64
65
|
"@dotenvx/dotenvx": "1.31.0",
|
|
65
|
-
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@
|
|
66
|
+
"@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@704",
|
|
67
|
+
"enquirer": "^2.4.1",
|
|
66
68
|
"glob": "^11.0.0",
|
|
67
69
|
"ts-morph": "^23.0.0",
|
|
68
|
-
"
|
|
70
|
+
"yaml": "^2.7.0"
|
|
69
71
|
},
|
|
70
72
|
"peerDependencies": {
|
|
71
|
-
"wrangler": "^3.
|
|
73
|
+
"wrangler": "^3.103.0"
|
|
72
74
|
},
|
|
73
75
|
"scripts": {
|
|
74
76
|
"clean": "rimraf dist",
|
package/dist/cli/build/index.js
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { cpSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { buildNextjsApp, setStandaloneBuildMode } from "@opennextjs/aws/build/buildNextApp.js";
|
|
5
|
-
import { compileCache } from "@opennextjs/aws/build/compileCache.js";
|
|
6
|
-
import { compileOpenNextConfig } from "@opennextjs/aws/build/compileConfig.js";
|
|
7
|
-
import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js";
|
|
8
|
-
import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js";
|
|
9
|
-
import * as buildHelper from "@opennextjs/aws/build/helper.js";
|
|
10
|
-
import { printHeader, showWarningOnWindows } from "@opennextjs/aws/build/utils.js";
|
|
11
|
-
import logger from "@opennextjs/aws/logger.js";
|
|
12
|
-
import { getPackageTemplatesDirPath } from "../../utils/get-package-templates-dir-path.js";
|
|
13
|
-
import { containsDotNextDir, getConfig } from "../config.js";
|
|
14
|
-
import { askConfirmation } from "../utils/ask-confirmation.js";
|
|
15
|
-
import { bundleServer } from "./bundle-server.js";
|
|
16
|
-
import { compileEnvFiles } from "./open-next/compile-env-files.js";
|
|
17
|
-
import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
|
|
18
|
-
import { createServerBundle } from "./open-next/createServerBundle.js";
|
|
19
|
-
/**
|
|
20
|
-
* Builds the application in a format that can be passed to workerd
|
|
21
|
-
*
|
|
22
|
-
* It saves the output in a `.worker-next` directory
|
|
23
|
-
*
|
|
24
|
-
* @param projectOpts The options for the project
|
|
25
|
-
*/
|
|
26
|
-
export async function build(projectOpts) {
|
|
27
|
-
printHeader("Cloudflare build");
|
|
28
|
-
showWarningOnWindows();
|
|
29
|
-
const baseDir = projectOpts.sourceDir;
|
|
30
|
-
const require = createRequire(import.meta.url);
|
|
31
|
-
const openNextDistDir = dirname(require.resolve("@opennextjs/aws/index.js"));
|
|
32
|
-
await createOpenNextConfigIfNotExistent(projectOpts);
|
|
33
|
-
const { config, buildDir } = await compileOpenNextConfig(baseDir);
|
|
34
|
-
ensureCloudflareConfig(config);
|
|
35
|
-
// Initialize options
|
|
36
|
-
const options = buildHelper.normalizeOptions(config, openNextDistDir, buildDir);
|
|
37
|
-
logger.setLevel(options.debug ? "debug" : "info");
|
|
38
|
-
// Do not minify the code so that we can apply string replacement patch.
|
|
39
|
-
// Note that wrangler will still minify the bundle.
|
|
40
|
-
options.minify = false;
|
|
41
|
-
// Pre-build validation
|
|
42
|
-
buildHelper.checkRunningInsideNextjsApp(options);
|
|
43
|
-
logger.info(`App directory: ${options.appPath}`);
|
|
44
|
-
buildHelper.printNextjsVersion(options);
|
|
45
|
-
ensureNextjsVersionSupported(options);
|
|
46
|
-
buildHelper.printOpenNextVersion(options);
|
|
47
|
-
if (projectOpts.skipNextBuild) {
|
|
48
|
-
logger.warn("Skipping Next.js build");
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
// Build the next app
|
|
52
|
-
printHeader("Building Next.js app");
|
|
53
|
-
setStandaloneBuildMode(options);
|
|
54
|
-
buildNextjsApp(options);
|
|
55
|
-
}
|
|
56
|
-
if (!containsDotNextDir(projectOpts.sourceDir)) {
|
|
57
|
-
throw new Error(`.next folder not found in ${projectOpts.sourceDir}`);
|
|
58
|
-
}
|
|
59
|
-
// Generate deployable bundle
|
|
60
|
-
printHeader("Generating bundle");
|
|
61
|
-
buildHelper.initOutputDir(options);
|
|
62
|
-
// Compile cache.ts
|
|
63
|
-
compileCache(options);
|
|
64
|
-
// Compile .env files
|
|
65
|
-
compileEnvFiles(options);
|
|
66
|
-
// Compile middleware
|
|
67
|
-
await createMiddleware(options, { forceOnlyBuildOnce: true });
|
|
68
|
-
createStaticAssets(options);
|
|
69
|
-
if (config.dangerous?.disableIncrementalCache !== true) {
|
|
70
|
-
createCacheAssets(options);
|
|
71
|
-
copyCacheAssets(options);
|
|
72
|
-
}
|
|
73
|
-
await createServerBundle(options);
|
|
74
|
-
// TODO: drop this copy.
|
|
75
|
-
// Copy the .next directory to the output directory so it can be mutated.
|
|
76
|
-
cpSync(join(projectOpts.sourceDir, ".next"), join(projectOpts.outputDir, ".next"), { recursive: true });
|
|
77
|
-
const projConfig = getConfig(projectOpts);
|
|
78
|
-
// TODO: rely on options only.
|
|
79
|
-
await bundleServer(projConfig, options);
|
|
80
|
-
if (!projectOpts.skipWranglerConfigCheck) {
|
|
81
|
-
await createWranglerConfigIfNotExistent(projectOpts);
|
|
82
|
-
}
|
|
83
|
-
logger.info("OpenNext build complete.");
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Creates a `open-next.config.ts` file for the user if it doesn't exist, but only after asking for the user's confirmation.
|
|
87
|
-
*
|
|
88
|
-
* If the user refuses an error is thrown (since the file is mandatory).
|
|
89
|
-
*
|
|
90
|
-
* @param projectOpts The options for the project
|
|
91
|
-
*/
|
|
92
|
-
async function createOpenNextConfigIfNotExistent(projectOpts) {
|
|
93
|
-
const openNextConfigPath = join(projectOpts.sourceDir, "open-next.config.ts");
|
|
94
|
-
if (!existsSync(openNextConfigPath)) {
|
|
95
|
-
const answer = await askConfirmation("Missing required `open-next.config.ts` file, do you want to create one?");
|
|
96
|
-
if (!answer) {
|
|
97
|
-
throw new Error("The `open-next.config.ts` file is required, aborting!");
|
|
98
|
-
}
|
|
99
|
-
cpSync(join(getPackageTemplatesDirPath(), "defaults", "open-next.config.ts"), openNextConfigPath);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* Ensures open next is configured for cloudflare.
|
|
104
|
-
*
|
|
105
|
-
* @param config OpenNext configuration.
|
|
106
|
-
*/
|
|
107
|
-
function ensureCloudflareConfig(config) {
|
|
108
|
-
const requirements = {
|
|
109
|
-
dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node",
|
|
110
|
-
dftUseEdgeConverter: config.default?.override?.converter === "edge",
|
|
111
|
-
dftMaybeUseCache: config.default?.override?.incrementalCache === "dummy" ||
|
|
112
|
-
typeof config.default?.override?.incrementalCache === "function",
|
|
113
|
-
dftUseDummyTagCacheAndQueue: config.default?.override?.tagCache === "dummy" && config.default?.override?.queue === "dummy",
|
|
114
|
-
disableCacheInterception: config.dangerous?.enableCacheInterception !== true,
|
|
115
|
-
mwIsMiddlewareExternal: config.middleware?.external == true,
|
|
116
|
-
mwUseCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare-edge",
|
|
117
|
-
mwUseEdgeConverter: config.middleware?.override?.converter === "edge",
|
|
118
|
-
mwUseFetchProxy: config.middleware?.override?.proxyExternalRequest === "fetch",
|
|
119
|
-
};
|
|
120
|
-
if (Object.values(requirements).some((satisfied) => !satisfied)) {
|
|
121
|
-
throw new Error("The `open-next.config.ts` should have a default export like this:\n\n" +
|
|
122
|
-
`{
|
|
123
|
-
default: {
|
|
124
|
-
override: {
|
|
125
|
-
wrapper: "cloudflare-node",
|
|
126
|
-
converter: "edge",
|
|
127
|
-
incrementalCache: "dummy" | function,
|
|
128
|
-
tagCache: "dummy",
|
|
129
|
-
queue: "dummy",
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
middleware: {
|
|
134
|
-
external: true,
|
|
135
|
-
override: {
|
|
136
|
-
wrapper: "cloudflare-edge",
|
|
137
|
-
converter: "edge",
|
|
138
|
-
proxyExternalRequest: "fetch",
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
|
|
142
|
-
"dangerous": {
|
|
143
|
-
"enableCacheInterception": false
|
|
144
|
-
},
|
|
145
|
-
}\n\n`.replace(/^ {8}/gm, ""));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Creates a `wrangler.json` file for the user if a wrangler config file doesn't already exist,
|
|
150
|
-
* but only after asking for the user's confirmation.
|
|
151
|
-
*
|
|
152
|
-
* If the user refuses a warning is shown (which offers ways to opt out of this check to the user).
|
|
153
|
-
*
|
|
154
|
-
* Note: we generate a wrangler.json file with comments instead of using the jsonc extension,
|
|
155
|
-
* we decided to do that since json is more common than jsonc, wrangler also parses
|
|
156
|
-
* them in the same way and we also expect developers to associate `wrangler.json`
|
|
157
|
-
* files to the jsonc language
|
|
158
|
-
*
|
|
159
|
-
* @param projectOpts The options for the project
|
|
160
|
-
*/
|
|
161
|
-
async function createWranglerConfigIfNotExistent(projectOpts) {
|
|
162
|
-
const possibleExts = ["toml", "json", "jsonc"];
|
|
163
|
-
const wranglerConfigFileExists = possibleExts.some((ext) => existsSync(join(projectOpts.sourceDir, `wrangler.${ext}`)));
|
|
164
|
-
if (wranglerConfigFileExists) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const answer = await askConfirmation("No `wrangler.(toml|json|jsonc)` config file found, do you want to create one?");
|
|
168
|
-
if (!answer) {
|
|
169
|
-
console.warn("No Wrangler config file created" +
|
|
170
|
-
"\n" +
|
|
171
|
-
"(to avoid this check use the `--skipWranglerConfigCheck` flag or set a `SKIP_WRANGLER_CONFIG_CHECK` environment variable to `yes`)");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const wranglerConfigTemplate = readFileSync(join(getPackageTemplatesDirPath(), "defaults", "wrangler.jsonc"), "utf8");
|
|
175
|
-
let wranglerConfigContent = wranglerConfigTemplate;
|
|
176
|
-
const appName = getAppNameFromPackageJson(projectOpts.sourceDir) ?? "app-name";
|
|
177
|
-
if (appName) {
|
|
178
|
-
wranglerConfigContent = wranglerConfigContent.replace('"app-name"', JSON.stringify(appName.replaceAll("_", "-")));
|
|
179
|
-
}
|
|
180
|
-
const compatDate = await getLatestCompatDate();
|
|
181
|
-
if (compatDate) {
|
|
182
|
-
wranglerConfigContent = wranglerConfigContent.replace(/"compatibility_date": "\d{4}-\d{2}-\d{2}"/, `"compatibility_date": ${JSON.stringify(compatDate)}`);
|
|
183
|
-
}
|
|
184
|
-
writeFileSync(join(projectOpts.sourceDir, "wrangler.json"), wranglerConfigContent);
|
|
185
|
-
}
|
|
186
|
-
function getAppNameFromPackageJson(sourceDir) {
|
|
187
|
-
try {
|
|
188
|
-
const packageJsonStr = readFileSync(join(sourceDir, "package.json"), "utf8");
|
|
189
|
-
const packageJson = JSON.parse(packageJsonStr);
|
|
190
|
-
if (typeof packageJson.name === "string")
|
|
191
|
-
return packageJson.name;
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
/* empty */
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
export async function getLatestCompatDate() {
|
|
198
|
-
try {
|
|
199
|
-
const resp = await fetch(`https://registry.npmjs.org/workerd`);
|
|
200
|
-
const latestWorkerdVersion = (await resp.json())["dist-tags"].latest;
|
|
201
|
-
// The format of the workerd version is `major.yyyymmdd.patch`.
|
|
202
|
-
const match = latestWorkerdVersion.match(/\d+\.(\d{4})(\d{2})(\d{2})\.\d+/);
|
|
203
|
-
if (match) {
|
|
204
|
-
const [, year, month, date] = match;
|
|
205
|
-
const compatDate = `${year}-${month}-${date}`;
|
|
206
|
-
return compatDate;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
/* empty */
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function ensureNextjsVersionSupported(options) {
|
|
214
|
-
if (buildHelper.compareSemver(options.nextVersion, "14.0.0") < 0) {
|
|
215
|
-
logger.error("Next.js version unsupported, please upgrade to version 14 or greater.");
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { readdirSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
/**
|
|
4
|
-
* Recursively reads all file paths in a directory.
|
|
5
|
-
*
|
|
6
|
-
* @param dir Directory to recursively read from.
|
|
7
|
-
* @returns Array of all paths for all files in a directory.
|
|
8
|
-
*/
|
|
9
|
-
export function readPathsRecursively(dir) {
|
|
10
|
-
try {
|
|
11
|
-
const files = readdirSync(dir, { withFileTypes: true });
|
|
12
|
-
return files.flatMap((file) => {
|
|
13
|
-
const filePath = join(dir, file.name);
|
|
14
|
-
return file.isDirectory() ? readPathsRecursively(filePath) : filePath;
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
}
|