@@ -11,20 +11,14 @@ import {
1111 WebpackLoggingCallback ,
1212 runWebpack ,
1313} from '@angular-devkit/build-webpack' ;
14- import {
15- join ,
16- json ,
17- logging ,
18- normalize ,
19- tags ,
20- virtualFs ,
21- } from '@angular-devkit/core' ;
14+ import { join , json , logging , normalize , tags , virtualFs } from '@angular-devkit/core' ;
2215import { NodeJsSyncHost } from '@angular-devkit/core/node' ;
2316import * as findCacheDirectory from 'find-cache-dir' ;
2417import * as fs from 'fs' ;
18+ import * as os from 'os' ;
2519import * as path from 'path' ;
2620import { Observable , from , of } from 'rxjs' ;
27- import { catchError , concatMap , map , switchMap } from 'rxjs/operators' ;
21+ import { concatMap , map , switchMap } from 'rxjs/operators' ;
2822import { ScriptTarget } from 'typescript' ;
2923import * as webpack from 'webpack' ;
3024import { NgBuildAnalyticsPlugin } from '../../plugins/webpack/analytics' ;
@@ -62,6 +56,7 @@ import {
6256} from '../utils' ;
6357import { copyAssets } from '../utils/copy-assets' ;
6458import { I18nOptions , createI18nOptions } from '../utils/i18n-options' ;
59+ import { createTranslationLoader } from '../utils/load-translations' ;
6560import {
6661 ProcessBundleFile ,
6762 ProcessBundleOptions ,
@@ -167,17 +162,54 @@ async function initialize(
167162 projectSourceRoot ?: string ;
168163 i18n : I18nOptions ;
169164} > {
165+ if ( ! context . target ) {
166+ throw new Error ( 'The builder requires a target.' ) ;
167+ }
168+
169+ const metadata = await context . getProjectMetadata ( context . target ) ;
170+ const i18n = createI18nOptions ( metadata , options . localize ) ;
171+
172+ if ( i18n . inlineLocales . size > 0 ) {
173+ // Load locales
174+ const loader = await createTranslationLoader ( ) ;
175+
176+ const usedFormats = new Set < string > ( ) ;
177+ for ( const [ locale , desc ] of Object . entries ( i18n . locales ) ) {
178+ if ( i18n . inlineLocales . has ( locale ) ) {
179+ const result = loader ( desc . file ) ;
180+
181+ usedFormats . add ( result . format ) ;
182+ if ( usedFormats . size > 1 ) {
183+ // This limitation is technically only for legacy message id support
184+ throw new Error (
185+ 'Localization currently only supports using one type of translation file format for the entire application.' ,
186+ ) ;
187+ }
188+
189+ desc . format = result . format ;
190+ desc . translation = result . translation ;
191+ }
192+ }
193+
194+ // Legacy message id's require the format of the translations
195+ if ( usedFormats . size > 0 ) {
196+ options . i18nFormat = [ ...usedFormats ] [ 0 ] ;
197+ }
198+ }
199+
200+ const originalOutputPath = options . outputPath ;
201+
202+ // If inlining store the output in a temporary location to facilitate post-processing
203+ if ( i18n . shouldInline ) {
204+ options . outputPath = fs . mkdtempSync ( path . join ( fs . realpathSync ( os . tmpdir ( ) ) , 'angular-cli-' ) ) ;
205+ }
206+
170207 const { config, projectRoot, projectSourceRoot } = await buildBrowserWebpackConfigFromContext (
171208 options ,
172209 context ,
173210 host ,
174211 ) ;
175212
176- // target is verified in the above call
177- // tslint:disable-next-line: no-non-null-assertion
178- const metadata = await context . getProjectMetadata ( context . target ! ) ;
179- const i18n = createI18nOptions ( metadata ) ;
180-
181213 let transformedConfig ;
182214 if ( webpackConfigurationTransform ) {
183215 transformedConfig = await webpackConfigurationTransform ( config ) ;
@@ -186,7 +218,7 @@ async function initialize(
186218 if ( options . deleteOutputPath ) {
187219 await deleteOutputDir (
188220 normalize ( context . workspaceRoot ) ,
189- normalize ( options . outputPath ) ,
221+ normalize ( originalOutputPath ) ,
190222 host ,
191223 ) . toPromise ( ) ;
192224 }
@@ -254,6 +286,10 @@ export function buildWebpackBrowser(
254286
255287 return { success } ;
256288 } else if ( success ) {
289+ if ( ! fs . existsSync ( baseOutputPath ) ) {
290+ fs . mkdirSync ( baseOutputPath , { recursive : true } ) ;
291+ }
292+
257293 let noModuleFiles : EmittedFiles [ ] | undefined ;
258294 let moduleFiles : EmittedFiles [ ] | undefined ;
259295 let files : EmittedFiles [ ] | undefined ;
@@ -272,6 +308,10 @@ export function buildWebpackBrowser(
272308 moduleFiles = [ ] ;
273309 noModuleFiles = [ ] ;
274310
311+ if ( ! webpackStats ) {
312+ throw new Error ( 'Webpack stats build result is required.' ) ;
313+ }
314+
275315 // Common options for all bundle process actions
276316 const sourceMapOptions = normalizeSourceMaps ( options . sourceMap || false ) ;
277317 const actionOptions : Partial < ProcessBundleOptions > = {
@@ -324,7 +364,8 @@ export function buildWebpackBrowser(
324364
325365 // Retrieve the content/map for the file
326366 // NOTE: Additional future optimizations will read directly from memory
327- let filename = path . join ( baseOutputPath , file . file ) ;
367+ // tslint:disable-next-line: no-non-null-assertion
368+ let filename = path . join ( webpackStats . outputPath ! , file . file ) ;
328369 const code = fs . readFileSync ( filename , 'utf8' ) ;
329370 let map ;
330371 if ( actionOptions . sourceMaps ) {
@@ -368,9 +409,6 @@ export function buildWebpackBrowser(
368409 noModuleFiles . push ( { ...file , file : newFilename } ) ;
369410 }
370411
371- // Execute the bundle processing actions
372- context . logger . info ( 'Generating ES5 bundles for differential loading...' ) ;
373-
374412 const processActions : typeof actions = [ ] ;
375413 let processRuntimeAction : ProcessBundleOptions | undefined ;
376414 const processResults : ProcessBundleResult [ ] = [ ] ;
@@ -389,29 +427,118 @@ export function buildWebpackBrowser(
389427 options . subresourceIntegrity ? 'sha384' : undefined ,
390428 ) ;
391429
430+ // Execute the bundle processing actions
392431 try {
432+ context . logger . info ( 'Generating ES5 bundles for differential loading...' ) ;
433+
393434 for await ( const result of executor . processAll ( processActions ) ) {
394435 processResults . push ( result ) ;
395436 }
437+
438+ // Runtime must be processed after all other files
439+ if ( processRuntimeAction ) {
440+ const runtimeOptions = {
441+ ...processRuntimeAction ,
442+ runtimeData : processResults ,
443+ } ;
444+ processResults . push (
445+ await import ( '../utils/process-bundle' ) . then ( m => m . process ( runtimeOptions ) ) ,
446+ ) ;
447+ }
448+
449+ context . logger . info ( 'ES5 bundle generation complete.' ) ;
396450 } finally {
397451 executor . stop ( ) ;
398452 }
399453
400- // Runtime must be processed after all other files
401- if ( processRuntimeAction ) {
402- const runtimeOptions = {
403- ...processRuntimeAction ,
404- runtimeData : processResults ,
405- } ;
406- processResults . push (
407- await import ( '../utils/process-bundle' ) . then ( m => m . process ( runtimeOptions ) ) ,
454+ if ( i18n . shouldInline ) {
455+ context . logger . info ( 'Generating localized bundles...' ) ;
456+
457+ const localize = await import ( '@angular/localize/src/tools/src/translate/main' ) ;
458+ const localizeDiag = await import ( '@angular/localize/src/tools/src/diagnostics' ) ;
459+
460+ const diagnostics = new localizeDiag . Diagnostics ( ) ;
461+ const translationFilePaths = [ ] ;
462+ let copySourceLocale = false ;
463+ for ( const locale of i18n . inlineLocales ) {
464+ if ( locale === i18n . sourceLocale ) {
465+ copySourceLocale = true ;
466+ continue ;
467+ }
468+ translationFilePaths . push ( i18n . locales [ locale ] . file ) ;
469+ }
470+
471+ if ( copySourceLocale ) {
472+ await copyAssets (
473+ [
474+ {
475+ glob : '**/*' ,
476+ // tslint:disable-next-line: no-non-null-assertion
477+ input : webpackStats . outputPath ! ,
478+ output : i18n . sourceLocale ,
479+ } ,
480+ ] ,
481+ [ baseOutputPath ] ,
482+ '' ,
483+ ) ;
484+ }
485+
486+ if ( translationFilePaths . length > 0 ) {
487+ const sourceFilePaths = [ ] ;
488+ for ( const result of processResults ) {
489+ if ( result . original ) {
490+ sourceFilePaths . push ( result . original . filename ) ;
491+ }
492+ if ( result . downlevel ) {
493+ sourceFilePaths . push ( result . downlevel . filename ) ;
494+ }
495+ }
496+ try {
497+ localize . translateFiles ( {
498+ // tslint:disable-next-line: no-non-null-assertion
499+ sourceRootPath : webpackStats . outputPath ! ,
500+ sourceFilePaths,
501+ translationFilePaths,
502+ outputPathFn : ( locale , relativePath ) =>
503+ path . join ( baseOutputPath , locale , relativePath ) ,
504+ diagnostics,
505+ missingTranslation : options . i18nMissingTranslation || 'warning' ,
506+ } ) ;
507+ } catch ( err ) {
508+ context . logger . error ( 'Localized bundle generation failed: ' + err . message ) ;
509+
510+ return { success : false } ;
511+ } finally {
512+ try {
513+ // Remove temporary directory used for i18n processing
514+ // tslint:disable-next-line: no-non-null-assertion
515+ await host . delete ( normalize ( webpackStats . outputPath ! ) ) . toPromise ( ) ;
516+ } catch { }
517+ }
518+ }
519+
520+ context . logger . info (
521+ `Localized bundle generation ${ diagnostics . hasErrors ? 'failed' : 'complete' } .` ,
408522 ) ;
409- }
410523
411- context . logger . info ( 'ES5 bundle generation complete.' ) ;
524+ for ( const message of diagnostics . messages ) {
525+ if ( message . type === 'error' ) {
526+ context . logger . error ( message . message ) ;
527+ } else {
528+ context . logger . warn ( message . message ) ;
529+ }
530+ }
531+
532+ if ( diagnostics . hasErrors ) {
533+ return { success : false } ;
534+ }
535+ }
412536
413537 // Copy assets
414538 if ( options . assets ) {
539+ const outputPaths = i18n . shouldInline
540+ ? [ ...i18n . inlineLocales ] . map ( l => path . join ( baseOutputPath , l ) )
541+ : [ baseOutputPath ] ;
415542 try {
416543 await copyAssets (
417544 normalizeAssetPatterns (
@@ -421,7 +548,7 @@ export function buildWebpackBrowser(
421548 normalize ( projectRoot ) ,
422549 projectSourceRoot === undefined ? undefined : normalize ( projectSourceRoot ) ,
423550 ) ,
424- [ baseOutputPath ] ,
551+ outputPaths ,
425552 context . workspaceRoot ,
426553 ) ;
427554 } catch ( err ) {
@@ -503,33 +630,29 @@ export function buildWebpackBrowser(
503630 }
504631
505632 if ( options . index ) {
506- return writeIndexHtml ( {
507- host,
508- outputPath : join ( normalize ( baseOutputPath ) , getIndexOutputFile ( options ) ) ,
509- indexPath : join ( root , getIndexInputFile ( options ) ) ,
510- files,
511- noModuleFiles,
512- moduleFiles,
513- baseHref : options . baseHref ,
514- deployUrl : options . deployUrl ,
515- sri : options . subresourceIntegrity ,
516- scripts : options . scripts ,
517- styles : options . styles ,
518- postTransform : transforms . indexHtml ,
519- crossOrigin : options . crossOrigin ,
520- lang : options . i18nLocale ,
521- } )
522- . pipe (
523- map ( ( ) => ( { success : true } ) ) ,
524- catchError ( error => of ( { success : false , error : mapErrorToMessage ( error ) } ) ) ,
525- )
526- . toPromise ( ) ;
527- } else {
528- return { success } ;
633+ const outputPaths = i18n . shouldInline
634+ ? [ ...i18n . inlineLocales ] . map ( l => path . join ( baseOutputPath , l ) )
635+ : [ baseOutputPath ] ;
636+
637+ for ( const outputPath of outputPaths ) {
638+ try {
639+ await generateIndex (
640+ outputPath ,
641+ options ,
642+ root ,
643+ files ,
644+ noModuleFiles ,
645+ moduleFiles ,
646+ transforms . indexHtml ,
647+ ) ;
648+ } catch ( err ) {
649+ return { success : false , error : mapErrorToMessage ( err ) } ;
650+ }
651+ }
529652 }
530- } else {
531- return { success } ;
532653 }
654+
655+ return { success } ;
533656 } ) ,
534657 concatMap ( buildEvent => {
535658 if ( buildEvent . success && ! options . watch && options . serviceWorker ) {
@@ -563,6 +686,35 @@ export function buildWebpackBrowser(
563686 ) ;
564687}
565688
689+ function generateIndex (
690+ baseOutputPath : string ,
691+ options : BrowserBuilderSchema ,
692+ root : string ,
693+ files : EmittedFiles [ ] | undefined ,
694+ noModuleFiles : EmittedFiles [ ] | undefined ,
695+ moduleFiles : EmittedFiles [ ] | undefined ,
696+ transformer ?: IndexHtmlTransform ,
697+ ) : Promise < void > {
698+ const host = new NodeJsSyncHost ( ) ;
699+
700+ return writeIndexHtml ( {
701+ host,
702+ outputPath : join ( normalize ( baseOutputPath ) , getIndexOutputFile ( options ) ) ,
703+ indexPath : join ( normalize ( root ) , getIndexInputFile ( options ) ) ,
704+ files,
705+ noModuleFiles,
706+ moduleFiles,
707+ baseHref : options . baseHref ,
708+ deployUrl : options . deployUrl ,
709+ sri : options . subresourceIntegrity ,
710+ scripts : options . scripts ,
711+ styles : options . styles ,
712+ postTransform : transformer ,
713+ crossOrigin : options . crossOrigin ,
714+ lang : options . i18nLocale ,
715+ } ) . toPromise ( ) ;
716+ }
717+
566718function mapErrorToMessage ( error : unknown ) : string | undefined {
567719 if ( error instanceof Error ) {
568720 return error . message ;
0 commit comments