Skip to content

Commit e98e014

Browse files
authored
Improve custom config support (#416)
* fix missing fnName in the function * compile open-next to node by default * compile config for edge
1 parent f83d636 commit e98e014

File tree

8 files changed

+153
-45
lines changed

8 files changed

+153
-45
lines changed

.changeset/orange-bees-lie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"open-next": patch
3+
---
4+
5+
Improve custom config support

packages/open-next/src/build.ts

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import os from "node:os";
55
import path from "node:path";
66
import url from "node:url";
77

8-
import { buildSync } from "esbuild";
98
import { MiddlewareManifest } from "types/next-types.js";
109

1110
import { isBinaryContentType } from "./adapters/binary.js";
11+
import {
12+
compileOpenNextConfigEdge,
13+
compileOpenNextConfigNode,
14+
} from "./build/compileConfig.js";
1215
import { createServerBundle } from "./build/createServerBundle.js";
1316
import { buildEdgeBundle } from "./build/edge/createEdgeBundle.js";
1417
import { generateOutput } from "./build/generateOutput.js";
@@ -39,15 +42,24 @@ export type PublicFiles = {
3942
files: string[];
4043
};
4144

42-
export async function build(openNextConfigPath?: string) {
45+
export async function build(
46+
openNextConfigPath?: string,
47+
nodeExternals?: string,
48+
) {
4349
showWindowsWarning();
4450

4551
// Load open-next.config.ts
4652
const tempDir = initTempDir();
47-
const configPath = compileOpenNextConfig(tempDir, openNextConfigPath);
53+
const configPath = compileOpenNextConfigNode(
54+
tempDir,
55+
openNextConfigPath,
56+
nodeExternals,
57+
);
4858
config = (await import(configPath)).default as OpenNextConfig;
4959
validateConfig(config);
5060

61+
compileOpenNextConfigEdge(tempDir, config, openNextConfigPath);
62+
5163
const { root: monorepoRoot, packager } = findMonorepoRoot(
5264
path.join(process.cwd(), config.appPath || "."),
5365
);
@@ -107,40 +119,6 @@ function initTempDir() {
107119
return tempDir;
108120
}
109121

110-
function compileOpenNextConfig(tempDir: string, openNextConfigPath?: string) {
111-
const sourcePath = path.join(
112-
process.cwd(),
113-
openNextConfigPath ?? "open-next.config.ts",
114-
);
115-
const outputPath = path.join(tempDir, "open-next.config.mjs");
116-
117-
//Check if open-next.config.ts exists
118-
if (!fs.existsSync(sourcePath)) {
119-
//Create a simple open-next.config.mjs file
120-
logger.debug("Cannot find open-next.config.ts. Using default config.");
121-
fs.writeFileSync(
122-
outputPath,
123-
[
124-
"var config = { default: { } };",
125-
"var open_next_config_default = config;",
126-
"export { open_next_config_default as default };",
127-
].join("\n"),
128-
);
129-
} else {
130-
buildSync({
131-
entryPoints: [sourcePath],
132-
outfile: outputPath,
133-
bundle: true,
134-
format: "esm",
135-
target: ["node18"],
136-
external: ["node:*"],
137-
platform: "neutral",
138-
});
139-
}
140-
141-
return outputPath;
142-
}
143-
144122
function checkRunningInsideNextjsApp() {
145123
const { appPath } = options;
146124
const extension = ["js", "cjs", "mjs"].find((ext) =>
@@ -228,9 +206,22 @@ function initOutputDir() {
228206
path.join(tempDir, "open-next.config.mjs"),
229207
"utf8",
230208
);
209+
let openNextConfigEdge: string | null = null;
210+
if (fs.existsSync(path.join(tempDir, "open-next.config.edge.mjs"))) {
211+
openNextConfigEdge = readFileSync(
212+
path.join(tempDir, "open-next.config.edge.mjs"),
213+
"utf8",
214+
);
215+
}
231216
fs.rmSync(outputDir, { recursive: true, force: true });
232217
fs.mkdirSync(tempDir, { recursive: true });
233218
fs.writeFileSync(path.join(tempDir, "open-next.config.mjs"), openNextConfig);
219+
if (openNextConfigEdge) {
220+
fs.writeFileSync(
221+
path.join(tempDir, "open-next.config.edge.mjs"),
222+
openNextConfigEdge,
223+
);
224+
}
234225
}
235226

236227
async function createWarmerBundle(config: OpenNextConfig) {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
import { buildSync } from "esbuild";
5+
import { OpenNextConfig } from "types/open-next.js";
6+
7+
import logger from "../logger.js";
8+
9+
export function compileOpenNextConfigNode(
10+
tempDir: string,
11+
openNextConfigPath?: string,
12+
nodeExternals?: string,
13+
) {
14+
const sourcePath = path.join(
15+
process.cwd(),
16+
openNextConfigPath ?? "open-next.config.ts",
17+
);
18+
const outputPath = path.join(tempDir, "open-next.config.mjs");
19+
20+
//Check if open-next.config.ts exists
21+
if (!fs.existsSync(sourcePath)) {
22+
//Create a simple open-next.config.mjs file
23+
logger.debug("Cannot find open-next.config.ts. Using default config.");
24+
fs.writeFileSync(
25+
outputPath,
26+
[
27+
"var config = { default: { } };",
28+
"var open_next_config_default = config;",
29+
"export { open_next_config_default as default };",
30+
].join("\n"),
31+
);
32+
} else {
33+
buildSync({
34+
entryPoints: [sourcePath],
35+
outfile: outputPath,
36+
bundle: true,
37+
format: "esm",
38+
target: ["node18"],
39+
external: nodeExternals ? nodeExternals.split(",") : [],
40+
platform: "node",
41+
banner: {
42+
js: [
43+
"import { createRequire as topLevelCreateRequire } from 'module';",
44+
"const require = topLevelCreateRequire(import.meta.url);",
45+
"import bannerUrl from 'url';",
46+
"const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));",
47+
].join(""),
48+
},
49+
});
50+
}
51+
52+
return outputPath;
53+
}
54+
55+
export function compileOpenNextConfigEdge(
56+
tempDir: string,
57+
config: OpenNextConfig,
58+
openNextConfigPath?: string,
59+
) {
60+
const sourcePath = path.join(
61+
process.cwd(),
62+
openNextConfigPath ?? "open-next.config.ts",
63+
);
64+
const outputPath = path.join(tempDir, "open-next.config.edge.mjs");
65+
66+
// We need to check if the config uses the edge runtime at any point
67+
// If it does, we need to compile it with the edge runtime
68+
const usesEdgeRuntime =
69+
config.middleware?.external ||
70+
Object.values(config.functions || {}).some((fn) => fn.runtime === "edge");
71+
if (!usesEdgeRuntime) {
72+
logger.debug(
73+
"No edge runtime found in the open-next.config.ts. Using default config.",
74+
);
75+
//Nothing to do here
76+
} else {
77+
logger.info("Compiling open-next.config.ts for edge runtime.", outputPath);
78+
buildSync({
79+
entryPoints: [sourcePath],
80+
outfile: outputPath,
81+
bundle: true,
82+
format: "esm",
83+
target: ["es2020"],
84+
conditions: ["worker", "browser"],
85+
platform: "browser",
86+
external: config.edgeExternals ?? [],
87+
});
88+
logger.info("Compiled open-next.config.ts for edge runtime.");
89+
}
90+
}

packages/open-next/src/build/createServerBundle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ async function generateBundle(
257257
"const require = topLevelCreateRequire(import.meta.url);",
258258
"import bannerUrl from 'url';",
259259
"const __dirname = bannerUrl.fileURLToPath(new URL('.', import.meta.url));",
260+
name === "default" ? "" : `globalThis.fnName = "${name}";`,
260261
].join(""),
261262
},
262263
plugins,

packages/open-next/src/build/edge/createEdgeBundle.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export async function generateEdgeBundle(
114114
fs.mkdirSync(outputPath, { recursive: true });
115115

116116
// Copy open-next.config.mjs
117-
copyOpenNextConfig(path.join(outputDir, ".build"), outputPath);
117+
copyOpenNextConfig(path.join(outputDir, ".build"), outputPath, true);
118118

119119
// Load middleware manifest
120120
const middlewareManifest = JSON.parse(

packages/open-next/src/build/helper.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,17 @@ export function compareSemver(v1: string, v2: string): number {
240240
return patch1 - patch2;
241241
}
242242

243-
export function copyOpenNextConfig(tempDir: string, outputPath: string) {
243+
export function copyOpenNextConfig(
244+
tempDir: string,
245+
outputPath: string,
246+
isEdge = false,
247+
) {
244248
// Copy open-next.config.mjs
245249
fs.copyFileSync(
246-
path.join(tempDir, "open-next.config.mjs"),
250+
path.join(
251+
tempDir,
252+
isEdge ? "open-next.config.edge.mjs" : "open-next.config.mjs",
253+
),
247254
path.join(outputPath, "open-next.config.mjs"),
248255
);
249256
}

packages/open-next/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ if (command !== "build") printHelp();
88
const args = parseArgs();
99
if (Object.keys(args).includes("--help")) printHelp();
1010

11-
build(args["--config-path"]);
11+
build(args["--config-path"], args["--node-externals"]);
1212

1313
function parseArgs() {
1414
return process.argv.slice(2).reduce(
@@ -33,9 +33,14 @@ function printHelp() {
3333
console.log("");
3434
console.log("Usage:");
3535
console.log(" npx open-next build");
36+
console.log("You can use a custom config path here");
3637
console.log(
3738
" npx open-next build --config-path ./path/to/open-next.config.ts",
3839
);
40+
console.log(
41+
"You can configure externals for the esbuild compilation of the open-next.config.ts file",
42+
);
43+
console.log(" npx open-next build --node-externals aws-sdk,sharp,sqlite3");
3944
console.log("");
4045

4146
process.exit(1);

packages/open-next/src/types/open-next.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ export interface OpenNextConfig {
306306
tagCache?: "dynamodb" | LazyLoadedOverride<TagCache>;
307307
};
308308

309+
/**
310+
* Dangerous options. This break some functionnality but can be useful in some cases.
311+
*/
312+
dangerous?: DangerousOptions;
309313
/**
310314
* The command to build the Next.js app.
311315
* @default `npm run build`, `yarn build`, or `pnpm build` based on the lock file found in the app's directory or any of its parent directories.
@@ -316,10 +320,6 @@ export interface OpenNextConfig {
316320
* });
317321
* ```
318322
*/
319-
/**
320-
* Dangerous options. This break some functionnality but can be useful in some cases.
321-
*/
322-
dangerous?: DangerousOptions;
323323
buildCommand?: string;
324324
/**
325325
* The path to the target folder of build output from the `buildCommand` option (the path which will contain the `.next` and `.open-next` folders). This path is relative from the current process.cwd().
@@ -336,4 +336,13 @@ export interface OpenNextConfig {
336336
* @default "."
337337
*/
338338
packageJsonPath?: string;
339+
/**
340+
* **Advanced usage**
341+
* If you use the edge runtime somewhere (either in the middleware or in the functions), we compile 2 versions of the open-next.config.ts file.
342+
* One for the node runtime and one for the edge runtime.
343+
* This option allows you to specify the externals for the edge runtime used in esbuild for the compilation of open-next.config.ts
344+
* It is especially useful if you use some custom overrides only in node
345+
* @default []
346+
*/
347+
edgeExternals?: string[];
339348
}

0 commit comments

Comments
 (0)