1
+ import { spawnSync } from "child_process" ;
1
2
import fsExtra from "fs-extra" ;
2
- import { PHASE_PRODUCTION_BUILD } from "./constants.js " ;
3
- import { ROUTES_MANIFEST } from "./constants.js " ;
3
+ import { createRequire } from "node:module " ;
4
+ import { join , relative , normalize } from "path " ;
4
5
import { fileURLToPath } from "url" ;
5
- import { OutputBundleOptions } from "./interfaces.js" ;
6
6
import { stringify as yamlStringify } from "yaml" ;
7
- import { spawnSync } from "child_process" ;
8
7
9
- import { join , relative , normalize } from "path" ;
8
+ import { PHASE_PRODUCTION_BUILD } from "./constants.js" ;
9
+ import { ROUTES_MANIFEST } from "./constants.js" ;
10
+ import { OutputBundleOptions , RoutesManifest } from "./interfaces.js" ;
11
+ import { NextConfigComplete } from "next/dist/server/config-shared.js" ;
10
12
11
- import type { RoutesManifest } from "./interfaces.js" ;
12
13
// fs-extra is CJS, readJson can't be imported using shorthand
13
14
export const { move, exists, writeFile, readJson } = fsExtra ;
14
15
15
- export async function loadConfig ( cwd : string ) {
16
+ // The default fallback command prefix to run a build.
17
+ export const DEFAULT_COMMAND = "npm" ;
18
+
19
+ // Loads the user's next.config.js file.
20
+ export async function loadConfig ( root : string , projectRoot : string ) : Promise < NextConfigComplete > {
21
+ // createRequire() gives us access to Node's CommonJS implementation of require.resolve()
22
+ // (https://nodejs.org/api/module.html#modulecreaterequirefilename).
23
+ // We use the require.resolve() resolution algorithm to get the path to the next config module,
24
+ // which may reside in the node_modules folder at a higher level in the directory structure
25
+ // (e.g. for monorepo projects).
26
+ // Note that ESM has an equivalent (https://nodejs.org/api/esm.html#importmetaresolvespecifier),
27
+ // but the feature is still experimental.
28
+ const require = createRequire ( import . meta. url ) ;
29
+ const configPath = require . resolve ( "next/dist/server/config.js" , { paths : [ projectRoot ] } ) ;
16
30
// dynamically load NextJS so this can be used in an NPX context
17
31
const { default : nextServerConfig } : { default : typeof import ( "next/dist/server/config.js" ) } =
18
- await import ( `${ cwd } /node_modules/next/dist/server/config.js` ) ;
32
+ await import ( configPath ) ;
33
+
19
34
const loadConfig = nextServerConfig . default ;
20
- return await loadConfig ( PHASE_PRODUCTION_BUILD , cwd ) ;
35
+ return await loadConfig ( PHASE_PRODUCTION_BUILD , root ) ;
21
36
}
22
37
23
38
export async function readRoutesManifest ( distDir : string ) : Promise < RoutesManifest > {
@@ -30,42 +45,62 @@ export const isMain = (meta: ImportMeta) => {
30
45
return process . argv [ 1 ] === fileURLToPath ( meta . url ) ;
31
46
} ;
32
47
33
- export function populateOutputBundleOptions ( cwd : string ) : OutputBundleOptions {
34
- const outputBundleDir = join ( cwd , ".apphosting" ) ;
48
+ /**
49
+ * Provides the paths in the output bundle for the built artifacts.
50
+ * @param rootDir The root directory of the uploaded source code.
51
+ * @param appDir The path to the application source code, relative to the root.
52
+ * @return The output bundle paths.
53
+ */
54
+ export function populateOutputBundleOptions ( rootDir : string , appDir : string ) : OutputBundleOptions {
55
+ const outputBundleDir = join ( rootDir , ".apphosting" ) ;
56
+ // In monorepo setups, the standalone directory structure will mirror the structure of the monorepo.
57
+ // We find the relative path from the root to the app directory to correctly locate server.js.
58
+ const outputBundleAppDir = join (
59
+ outputBundleDir ,
60
+ process . env . MONOREPO_COMMAND ? relative ( rootDir , appDir ) : "" ,
61
+ ) ;
62
+
35
63
return {
36
64
bundleYamlPath : join ( outputBundleDir , "bundle.yaml" ) ,
37
65
outputDirectory : outputBundleDir ,
38
- serverFilePath : join ( outputBundleDir , "server.js" ) ,
39
- outputPublicDirectory : join ( outputBundleDir , "public" ) ,
40
- outputStaticDirectory : join ( outputBundleDir , ".next" , "static" ) ,
66
+ serverFilePath : join ( outputBundleAppDir , "server.js" ) ,
67
+ outputPublicDirectory : join ( outputBundleAppDir , "public" ) ,
68
+ outputStaticDirectory : join ( outputBundleAppDir , ".next" , "static" ) ,
41
69
} ;
42
70
}
43
71
44
72
// Run build command
45
- export function build ( cwd : string ) : void {
73
+ export function build ( cwd : string , cmd = DEFAULT_COMMAND ) : void {
46
74
// Set standalone mode
47
75
process . env . NEXT_PRIVATE_STANDALONE = "true" ;
48
76
// Opt-out sending telemetry to Vercel
49
77
process . env . NEXT_TELEMETRY_DISABLED = "1" ;
50
- spawnSync ( "npm" , [ "run" , "build" ] , { cwd, shell : true , stdio : "inherit" } ) ;
78
+ spawnSync ( cmd , [ "run" , "build" ] , { cwd, shell : true , stdio : "inherit" } ) ;
51
79
}
52
80
53
- // move the standalone directory, the static directory and the public directory to apphosting output directory
54
- // as well as generating bundle.yaml
81
+ /**
82
+ * Moves the standalone directory, the static directory and the public directory to apphosting output directory.
83
+ * Also generates the bundle.yaml file.
84
+ * @param rootDir The root directory of the uploaded source code.
85
+ * @param appDir The path to the application source code, relative to the root.
86
+ * @param outputBundleOptions The target location of built artifacts in the output bundle.
87
+ * @param nextBuildDirectory The location of the .next directory.
88
+ */
55
89
export async function generateOutputDirectory (
56
- cwd : string ,
90
+ rootDir : string ,
91
+ appDir : string ,
57
92
outputBundleOptions : OutputBundleOptions ,
58
93
nextBuildDirectory : string ,
59
94
) : Promise < void > {
60
95
const standaloneDirectory = join ( nextBuildDirectory , "standalone" ) ;
61
96
await move ( standaloneDirectory , outputBundleOptions . outputDirectory , { overwrite : true } ) ;
62
97
63
98
const staticDirectory = join ( nextBuildDirectory , "static" ) ;
64
- const publicDirectory = join ( cwd , "public" ) ;
99
+ const publicDirectory = join ( appDir , "public" ) ;
65
100
await Promise . all ( [
66
101
move ( staticDirectory , outputBundleOptions . outputStaticDirectory , { overwrite : true } ) ,
67
102
movePublicDirectory ( publicDirectory , outputBundleOptions . outputPublicDirectory ) ,
68
- generateBundleYaml ( outputBundleOptions , nextBuildDirectory , cwd ) ,
103
+ generateBundleYaml ( outputBundleOptions , nextBuildDirectory , rootDir ) ,
69
104
] ) ;
70
105
return ;
71
106
}
0 commit comments