@@ -19,8 +19,10 @@ import { existsSync, readFileSync } from 'fs';
1919import { parse , stringify } from 'comment-json' ;
2020import { formatFiles } from '../../utils/format-files.rule.js' ;
2121import {
22+ findNestCliConfigPath ,
2223 inPlaceSortByKeys ,
2324 normalizeToKebabOrSnakeCase ,
25+ readJsonFile ,
2426} from '../../utils/index.js' ;
2527import {
2628 DEFAULT_APPS_PATH ,
@@ -35,6 +37,12 @@ import {
3537import type { SubAppOptions } from './sub-app.schema.js' ;
3638import { isEsmProject } from '../../utils/source-root.helpers.js' ;
3739
40+ /** The builder `nest g app` writes when it converts a workspace to a monorepo. */
41+ const DEFAULT_BUILDER = 'rspack' ;
42+
43+ /** What the CLI itself assumes when `nest-cli.json` names no builder. */
44+ const CLI_DEFAULT_BUILDER = 'tsc' ;
45+
3846type UpdateJsonFn < T > = ( obj : T ) => T | void ;
3947interface TsConfigPartialType {
4048 compilerOptions ?: Record < string , any > ;
@@ -59,6 +67,7 @@ export function main(options: SubAppOptions): Rule {
5967 ] ) ( tree , context ) ,
6068 addAppsToCliOptions ( options . path ! , options . name , appName ) ,
6169 addTsConfigReference ( options . path ! , options . name ) ,
70+ applyStartProdScript ( options , appName ) ,
6271 ( tree ) => {
6372 ( options as any ) . isEsm = isEsmProject ( tree ) ;
6473 return tree ;
@@ -228,6 +237,34 @@ function updatePackageJson(options: SubAppOptions, defaultAppName: string) {
228237 } ;
229238}
230239
240+ type BuilderConfig = { compilerOptions ?: { builder ?: unknown } } ;
241+
242+ /**
243+ * Reads the builder in use. `builder` is either a name or a `{ type }` object,
244+ * and the CLI falls back to `tsc` when it is absent - which only happens in a
245+ * workspace that was already a monorepo, since the conversion writes one.
246+ */
247+ function readBuilder ( host : Tree ) : string {
248+ const path = findNestCliConfigPath ( host ) ;
249+ const config = path ? readJsonFile < BuilderConfig > ( host , path ) : null ;
250+ const builder = config ?. compilerOptions ?. builder ;
251+ const name =
252+ typeof builder === 'string'
253+ ? builder
254+ : ( builder as { type ?: unknown } | undefined ) ?. type ;
255+ return typeof name === 'string' && name ? name : CLI_DEFAULT_BUILDER ;
256+ }
257+
258+ /**
259+ * Builders that emit one bundle per project rather than mirroring the source
260+ * tree into `outDir`. Everything else - `tsc`, and `swc`, whose
261+ * `stripLeadingPaths` is off whenever `rootDir` sits above the source root -
262+ * writes the nested `dist/<root>/<root>/src/main.js` entry.
263+ */
264+ function isBundler ( builder : string ) : boolean {
265+ return builder === 'rspack' || builder === 'webpack' ;
266+ }
267+
231268function updateNpmScripts (
232269 scripts : Record < string , any > ,
233270 options : SubAppOptions ,
@@ -237,13 +274,8 @@ function updateNpmScripts(
237274 return ;
238275 }
239276 const defaultFormatScriptName = 'format' ;
240- const defaultStartScriptName = 'start:prod' ;
241277 const defaultTestScriptName = 'test:e2e' ;
242- if (
243- ! scripts [ defaultTestScriptName ] &&
244- ! scripts [ defaultFormatScriptName ] &&
245- ! scripts [ defaultStartScriptName ]
246- ) {
278+ if ( ! scripts [ defaultTestScriptName ] && ! scripts [ defaultFormatScriptName ] ) {
247279 return ;
248280 }
249281 if (
@@ -269,15 +301,49 @@ function updateNpmScripts(
269301 scripts [ defaultFormatScriptName ] =
270302 `prettier --write "${ defaultSourceRoot } /**/*.ts" "${ DEFAULT_LIB_PATH } /**/*.ts"` ;
271303 }
272- if (
273- scripts [ defaultStartScriptName ] &&
274- scripts [ defaultStartScriptName ] . indexOf ( 'dist/main' ) >= 0
275- ) {
276- const defaultSourceRoot =
277- options . rootDir !== undefined ? options . rootDir : DEFAULT_APPS_PATH ;
278- scripts [ defaultStartScriptName ] =
279- `node dist/${ defaultSourceRoot } /${ defaultAppName } /main` ;
280- }
304+ }
305+
306+ /**
307+ * Writes `start:prod` for the builder the workspace ends up using. This has to
308+ * run after the CLI options are final: `nest g app` forces `rspack` when it
309+ * converts a single app into a monorepo, so reading the builder earlier would
310+ * write a script that does not match the final config.
311+ */
312+ function applyStartProdScript (
313+ options : SubAppOptions ,
314+ defaultAppName : string ,
315+ ) : Rule {
316+ return ( host : Tree ) => {
317+ if ( ! host . exists ( 'package.json' ) ) {
318+ return host ;
319+ }
320+ const builder = readBuilder ( host ) ;
321+ return updateJsonFile (
322+ host ,
323+ 'package.json' ,
324+ ( packageJson : Record < string , Record < string , any > > ) => {
325+ const scripts = packageJson . scripts ;
326+ const startScript = scripts ?. [ 'start:prod' ] ;
327+ if (
328+ typeof startScript !== 'string' ||
329+ ! / ^ n o d e d i s t \/ ( .+ \/ ) ? m a i n $ / . test ( startScript )
330+ ) {
331+ return ;
332+ }
333+ const defaultSourceRoot =
334+ options . rootDir !== undefined ? options . rootDir : DEFAULT_APPS_PATH ;
335+ // A bundler ignores `rootDir` and writes `dist/<root>/main.js`, which
336+ // is also where `nest start` falls back to looking. A per-file
337+ // compiler mirrors the source tree under `rootDir` (the workspace
338+ // root) into the project's `outDir`, so the project path appears
339+ // twice: dist/apps/<app>/apps/<app>/src/main.
340+ const projectRoot = `${ defaultSourceRoot } /${ defaultAppName } ` ;
341+ scripts [ 'start:prod' ] = isBundler ( builder )
342+ ? `node dist/${ projectRoot } /main`
343+ : `node dist/${ projectRoot } /${ projectRoot } /src/main` ;
344+ } ,
345+ ) ;
346+ } ;
281347}
282348
283349function updateJestOptions (
@@ -411,7 +477,7 @@ function updateMainAppOptions(
411477 if ( ! optionsFile . compilerOptions ) {
412478 optionsFile . compilerOptions = { } ;
413479 }
414- optionsFile . compilerOptions . builder = 'rspack' ;
480+ optionsFile . compilerOptions . builder = DEFAULT_BUILDER ;
415481 optionsFile . compilerOptions . tsConfigPath = tsConfigPath ;
416482
417483 if ( ! optionsFile . projects ) {
0 commit comments