@@ -15,6 +15,7 @@ import {
1515 BuildOutputFileType ,
1616 createOutputFile ,
1717} from '../../tools/esbuild/bundler-files' ;
18+ import { calculateHash } from '../hash' ;
1819
1920export const SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs' ;
2021export const SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs' ;
@@ -60,6 +61,37 @@ function escapeUnsafeChars(str: string): string {
6061 return str . replace ( / [ $ ` \\ ] / g, ( c ) => UNSAFE_CHAR_MAP [ c ] ) ;
6162}
6263
64+ /**
65+ * Matches every character which is not safe in the name of a generated server asset chunk.
66+ */
67+ const UNSAFE_CHUNK_NAME_CHARACTER_REGEXP = / [ ^ a - z A - Z 0 - 9 _ - ] / g;
68+
69+ /**
70+ * The maximum number of characters of an asset path kept in the name of its generated chunk.
71+ * The appended digest is what makes the name unique, so the readable part can be truncated to
72+ * stay well within the file name length limits of all supported platforms.
73+ */
74+ const MAX_CHUNK_NAME_LENGTH = 128 ;
75+
76+ /**
77+ * Builds the path of the generated chunk which holds the content of a server asset.
78+ *
79+ * Asset paths are derived from route paths and can therefore contain characters which are unusable
80+ * in a file name (`?`, `:` and `*` are invalid on Windows) or which change how the generated
81+ * dynamic import is resolved (`?`, `#` and `%` are URL syntax). Those characters are replaced, and
82+ * a digest of the asset path is appended so that two asset paths never share a chunk.
83+ *
84+ * @param assetPath - The path of the asset, for example `store/summer sale/index.html`.
85+ * @returns The path of the chunk to generate for the asset.
86+ */
87+ function generateServerAssetChunkPath ( assetPath : string ) : string {
88+ const name = assetPath
89+ . replace ( UNSAFE_CHUNK_NAME_CHARACTER_REGEXP , '_' )
90+ . slice ( 0 , MAX_CHUNK_NAME_LENGTH ) ;
91+
92+ return `assets-chunks/${ name } -${ calculateHash ( assetPath ) } .mjs` ;
93+ }
94+
6395/**
6496 * Generates the server manifest for the App Engine environment.
6597 *
@@ -85,7 +117,7 @@ export function generateAngularServerAppEngineManifest(
85117 for ( const locale of i18nOptions . inlineLocales ) {
86118 const { subPath } = i18nOptions . locales [ locale ] ;
87119 const importPath = `${ subPath ? `${ subPath } /` : '' } ${ MAIN_SERVER_OUTPUT_FILENAME } ` ;
88- entryPoints [ subPath ] = `() => import('. /${ importPath } ' )` ;
120+ entryPoints [ subPath ] = `() => import(${ JSON . stringify ( `. /${ importPath } ` ) } )` ;
89121 supportedLocales [ locale ] = subPath ;
90122 }
91123 } else {
@@ -101,12 +133,12 @@ export function generateAngularServerAppEngineManifest(
101133
102134 const manifestContent = `
103135export default {
104- basePath: ' ${ basePath } ' ,
136+ basePath: ${ JSON . stringify ( basePath ) } ,
105137 allowedHosts: ${ JSON . stringify ( allowedHosts , undefined , 2 ) } ,
106138 supportedLocales: ${ JSON . stringify ( supportedLocales , undefined , 2 ) } ,
107139 entryPoints: {
108140 ${ Object . entries ( entryPoints )
109- . map ( ( [ key , value ] ) => `' ${ key } ' : ${ value } ` )
141+ . map ( ( [ key , value ] ) => `${ JSON . stringify ( key ) } : ${ value } ` )
110142 . join ( ',\n ' ) }
111143 },
112144};
@@ -163,7 +195,7 @@ export function generateAngularServerAppManifest(
163195 for ( const file of [ ...additionalHtmlOutputFiles . values ( ) , ...outputFiles ] ) {
164196 const extension = extname ( file . path ) ;
165197 if ( extension === '.html' || ( inlineCriticalCss && extension === '.css' ) ) {
166- const jsChunkFilePath = `assets-chunks/ ${ file . path . replace ( / [ . / ] / g , '_' ) } .mjs` ;
198+ const jsChunkFilePath = generateServerAssetChunkPath ( file . path ) ;
167199 const escapedContent = escapeUnsafeChars ( file . text ) ;
168200
169201 serverAssetsChunks . push (
@@ -183,8 +215,11 @@ export function generateAngularServerAppManifest(
183215 pos = file . text . indexOf ( '\r\n' , pos + 2 ) ;
184216 }
185217
218+ // Asset paths are derived from route paths and can contain arbitrary characters, so they are
219+ // serialized rather than interpolated into the generated executable manifest.
186220 serverAssets [ file . path ] =
187- `{size: ${ size } , hash: '${ file . hash } ', text: () => import('./${ jsChunkFilePath } ').then(m => m.default)}` ;
221+ `{size: ${ size } , hash: ${ JSON . stringify ( file . hash ) } , ` +
222+ `text: () => import(${ JSON . stringify ( `./${ jsChunkFilePath } ` ) } ).then(m => m.default)}` ;
188223 }
189224 }
190225
@@ -197,13 +232,13 @@ export function generateAngularServerAppManifest(
197232export default {
198233 bootstrap: () => import('./main.server.mjs').then(m => m.default),
199234 inlineCriticalCss: ${ inlineCriticalCss } ,
200- baseHref: ' ${ baseHref } ' ,
235+ baseHref: ${ JSON . stringify ( baseHref ) } ,
201236 locale: ${ JSON . stringify ( locale ) } ,
202237 routes: ${ JSON . stringify ( routes , undefined , 2 ) } ,
203238 entryPointToBrowserMapping: ${ JSON . stringify ( entryPointToBrowserMapping , undefined , 2 ) } ,
204239 assets: {
205240 ${ Object . entries ( serverAssets )
206- . map ( ( [ key , value ] ) => `' ${ key } ' : ${ value } ` )
241+ . map ( ( [ key , value ] ) => `${ JSON . stringify ( key ) } : ${ value } ` )
207242 . join ( ',\n ' ) }
208243 },
209244};
0 commit comments