66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import { join , normalize , strings } from '@angular-devkit/core' ;
9+ import { isJsonObject , join , normalize , strings } from '@angular-devkit/core' ;
1010import {
1111 Rule ,
12+ SchematicContext ,
1213 SchematicsException ,
1314 Tree ,
1415 apply ,
@@ -19,6 +20,7 @@ import {
1920 schematic ,
2021 url ,
2122} from '@angular-devkit/schematics' ;
23+ import { posix } from 'node:path' ;
2224import { Schema as ServerOptions } from '../server/schema' ;
2325import { DependencyType , addDependency , readWorkspace , updateWorkspace } from '../utility' ;
2426import { JSONFile } from '../utility/json-file' ;
@@ -33,21 +35,24 @@ import { Schema as SSROptions } from './schema';
3335
3436const SERVE_SSR_TARGET_NAME = 'serve-ssr' ;
3537const PRERENDER_TARGET_NAME = 'prerender' ;
38+ const DEFAULT_BROWSER_DIR = 'browser' ;
39+ const DEFAULT_MEDIA_DIR = 'media' ;
40+ const DEFAULT_SERVER_DIR = 'server' ;
3641
37- async function getOutputPath (
42+ async function getLegacyOutputPaths (
3843 host : Tree ,
3944 projectName : string ,
4045 target : 'server' | 'build' ,
4146) : Promise < string > {
4247 // Generate new output paths
4348 const workspace = await readWorkspace ( host ) ;
4449 const project = workspace . projects . get ( projectName ) ;
45- const serverTarget = project ?. targets . get ( target ) ;
46- if ( ! serverTarget || ! serverTarget . options ) {
50+ const architectTarget = project ?. targets . get ( target ) ;
51+ if ( ! architectTarget ? .options ) {
4752 throw new SchematicsException ( `Cannot find 'options' for ${ projectName } ${ target } target.` ) ;
4853 }
4954
50- const { outputPath } = serverTarget . options ;
55+ const { outputPath } = architectTarget . options ;
5156 if ( typeof outputPath !== 'string' ) {
5257 throw new SchematicsException (
5358 `outputPath for ${ projectName } ${ target } target is not a string.` ,
@@ -57,6 +62,52 @@ async function getOutputPath(
5762 return outputPath ;
5863}
5964
65+ async function getApplicationBuilderOutputPaths (
66+ host : Tree ,
67+ projectName : string ,
68+ ) : Promise < { browser : string ; server : string ; base : string } > {
69+ // Generate new output paths
70+ const target = 'build' ;
71+ const workspace = await readWorkspace ( host ) ;
72+ const project = workspace . projects . get ( projectName ) ;
73+ const architectTarget = project ?. targets . get ( target ) ;
74+
75+ if ( ! architectTarget ?. options ) {
76+ throw new SchematicsException ( `Cannot find 'options' for ${ projectName } ${ target } target.` ) ;
77+ }
78+
79+ const { outputPath } = architectTarget . options ;
80+ if ( outputPath === null || outputPath === undefined ) {
81+ throw new SchematicsException (
82+ `outputPath for ${ projectName } ${ target } target is undeined or null.` ,
83+ ) ;
84+ }
85+
86+ const defaultDirs = {
87+ server : DEFAULT_SERVER_DIR ,
88+ browser : DEFAULT_BROWSER_DIR ,
89+ } ;
90+
91+ if ( outputPath && isJsonObject ( outputPath ) ) {
92+ return {
93+ ...defaultDirs ,
94+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95+ ...( outputPath as any ) ,
96+ } ;
97+ }
98+
99+ if ( typeof outputPath !== 'string' ) {
100+ throw new SchematicsException (
101+ `outputPath for ${ projectName } ${ target } target is not a string.` ,
102+ ) ;
103+ }
104+
105+ return {
106+ base : outputPath ,
107+ ...defaultDirs ,
108+ } ;
109+ }
110+
60111function addScriptsRule ( { project } : SSROptions , isUsingApplicationBuilder : boolean ) : Rule {
61112 return async ( host ) => {
62113 const pkgPath = '/package.json' ;
@@ -66,11 +117,11 @@ function addScriptsRule({ project }: SSROptions, isUsingApplicationBuilder: bool
66117 }
67118
68119 if ( isUsingApplicationBuilder ) {
69- const distPath = await getOutputPath ( host , project , 'build' ) ;
120+ const { base , server } = await getApplicationBuilderOutputPaths ( host , project ) ;
70121 pkg . scripts ??= { } ;
71- pkg . scripts [ `serve:ssr:${ project } ` ] = `node ${ distPath } / server/server.mjs` ;
122+ pkg . scripts [ `serve:ssr:${ project } ` ] = `node ${ posix . join ( base , server ) } /server.mjs` ;
72123 } else {
73- const serverDist = await getOutputPath ( host , project , 'server' ) ;
124+ const serverDist = await getLegacyOutputPaths ( host , project , 'server' ) ;
74125 pkg . scripts = {
75126 ...pkg . scripts ,
76127 'dev:ssr' : `ng run ${ project } :${ SERVE_SSR_TARGET_NAME } ` ,
@@ -111,15 +162,40 @@ function updateApplicationBuilderTsConfigRule(options: SSROptions): Rule {
111162function updateApplicationBuilderWorkspaceConfigRule (
112163 projectRoot : string ,
113164 options : SSROptions ,
165+ { logger } : SchematicContext ,
114166) : Rule {
115167 return updateWorkspace ( ( workspace ) => {
116168 const buildTarget = workspace . projects . get ( options . project ) ?. targets . get ( 'build' ) ;
117169 if ( ! buildTarget ) {
118170 return ;
119171 }
120172
173+ let outputPath = buildTarget . options ?. outputPath ;
174+ if ( outputPath && isJsonObject ( outputPath ) ) {
175+ if ( outputPath . browser === '' ) {
176+ const base = outputPath . base as string ;
177+ logger . warn (
178+ `The output location of the browser build has been updated from "${ base } " to "${ posix . join (
179+ base ,
180+ DEFAULT_BROWSER_DIR ,
181+ ) } ".
182+ You might need to adjust your deployment pipeline.` ,
183+ ) ;
184+
185+ if (
186+ ( outputPath . media && outputPath . media !== DEFAULT_MEDIA_DIR ) ||
187+ ( outputPath . server && outputPath . server !== DEFAULT_SERVER_DIR )
188+ ) {
189+ delete outputPath . browser ;
190+ } else {
191+ outputPath = outputPath . base ;
192+ }
193+ }
194+ }
195+
121196 buildTarget . options = {
122197 ...buildTarget . options ,
198+ outputPath,
123199 prerender : true ,
124200 ssr : {
125201 entry : join ( normalize ( projectRoot ) , 'server.ts' ) ,
@@ -238,23 +314,22 @@ function addDependencies(isUsingApplicationBuilder: boolean): Rule {
238314
239315function addServerFile ( options : ServerOptions , isStandalone : boolean ) : Rule {
240316 return async ( host ) => {
317+ const projectName = options . project ;
241318 const workspace = await readWorkspace ( host ) ;
242- const project = workspace . projects . get ( options . project ) ;
319+ const project = workspace . projects . get ( projectName ) ;
243320 if ( ! project ) {
244- throw new SchematicsException ( `Invalid project name (${ options . project } )` ) ;
321+ throw new SchematicsException ( `Invalid project name (${ projectName } )` ) ;
245322 }
323+ const isUsingApplicationBuilder =
324+ project ?. targets ?. get ( 'build' ) ?. builder === Builders . Application ;
246325
247- const browserDistDirectory = await getOutputPath ( host , options . project , 'build' ) ;
326+ const browserDistDirectory = isUsingApplicationBuilder
327+ ? ( await getApplicationBuilderOutputPaths ( host , projectName ) ) . browser
328+ : await getLegacyOutputPaths ( host , projectName , 'build' ) ;
248329
249330 return mergeWith (
250331 apply (
251- url (
252- `./files/${
253- project ?. targets ?. get ( 'build' ) ?. builder === Builders . Application
254- ? 'application-builder'
255- : 'server-builder'
256- } `,
257- ) ,
332+ url ( `./files/${ isUsingApplicationBuilder ? 'application-builder' : 'server-builder' } ` ) ,
258333 [
259334 applyTemplates ( {
260335 ...strings ,
@@ -270,7 +345,7 @@ function addServerFile(options: ServerOptions, isStandalone: boolean): Rule {
270345}
271346
272347export default function ( options : SSROptions ) : Rule {
273- return async ( host ) => {
348+ return async ( host , context ) => {
274349 const browserEntryPoint = await getMainFilePath ( host , options . project ) ;
275350 const isStandalone = isStandaloneApp ( host , browserEntryPoint ) ;
276351
@@ -289,7 +364,7 @@ export default function (options: SSROptions): Rule {
289364 } ) ,
290365 ...( isUsingApplicationBuilder
291366 ? [
292- updateApplicationBuilderWorkspaceConfigRule ( clientProject . root , options ) ,
367+ updateApplicationBuilderWorkspaceConfigRule ( clientProject . root , options , context ) ,
293368 updateApplicationBuilderTsConfigRule ( options ) ,
294369 ]
295370 : [
0 commit comments