@@ -11,20 +11,14 @@ import {
11
11
WebpackLoggingCallback ,
12
12
runWebpack ,
13
13
} 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' ;
22
15
import { NodeJsSyncHost } from '@angular-devkit/core/node' ;
23
16
import * as findCacheDirectory from 'find-cache-dir' ;
24
17
import * as fs from 'fs' ;
18
+ import * as os from 'os' ;
25
19
import * as path from 'path' ;
26
20
import { Observable , from , of } from 'rxjs' ;
27
- import { catchError , concatMap , map , switchMap } from 'rxjs/operators' ;
21
+ import { concatMap , map , switchMap } from 'rxjs/operators' ;
28
22
import { ScriptTarget } from 'typescript' ;
29
23
import * as webpack from 'webpack' ;
30
24
import { NgBuildAnalyticsPlugin } from '../../plugins/webpack/analytics' ;
@@ -62,6 +56,7 @@ import {
62
56
} from '../utils' ;
63
57
import { copyAssets } from '../utils/copy-assets' ;
64
58
import { I18nOptions , createI18nOptions } from '../utils/i18n-options' ;
59
+ import { createTranslationLoader } from '../utils/load-translations' ;
65
60
import {
66
61
ProcessBundleFile ,
67
62
ProcessBundleOptions ,
@@ -167,17 +162,54 @@ async function initialize(
167
162
projectSourceRoot ?: string ;
168
163
i18n : I18nOptions ;
169
164
} > {
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
+
170
207
const { config, projectRoot, projectSourceRoot } = await buildBrowserWebpackConfigFromContext (
171
208
options ,
172
209
context ,
173
210
host ,
174
211
) ;
175
212
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
-
181
213
let transformedConfig ;
182
214
if ( webpackConfigurationTransform ) {
183
215
transformedConfig = await webpackConfigurationTransform ( config ) ;
@@ -186,7 +218,7 @@ async function initialize(
186
218
if ( options . deleteOutputPath ) {
187
219
await deleteOutputDir (
188
220
normalize ( context . workspaceRoot ) ,
189
- normalize ( options . outputPath ) ,
221
+ normalize ( originalOutputPath ) ,
190
222
host ,
191
223
) . toPromise ( ) ;
192
224
}
@@ -254,6 +286,10 @@ export function buildWebpackBrowser(
254
286
255
287
return { success } ;
256
288
} else if ( success ) {
289
+ if ( ! fs . existsSync ( baseOutputPath ) ) {
290
+ fs . mkdirSync ( baseOutputPath , { recursive : true } ) ;
291
+ }
292
+
257
293
let noModuleFiles : EmittedFiles [ ] | undefined ;
258
294
let moduleFiles : EmittedFiles [ ] | undefined ;
259
295
let files : EmittedFiles [ ] | undefined ;
@@ -272,6 +308,10 @@ export function buildWebpackBrowser(
272
308
moduleFiles = [ ] ;
273
309
noModuleFiles = [ ] ;
274
310
311
+ if ( ! webpackStats ) {
312
+ throw new Error ( 'Webpack stats build result is required.' ) ;
313
+ }
314
+
275
315
// Common options for all bundle process actions
276
316
const sourceMapOptions = normalizeSourceMaps ( options . sourceMap || false ) ;
277
317
const actionOptions : Partial < ProcessBundleOptions > = {
@@ -324,7 +364,8 @@ export function buildWebpackBrowser(
324
364
325
365
// Retrieve the content/map for the file
326
366
// 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 ) ;
328
369
const code = fs . readFileSync ( filename , 'utf8' ) ;
329
370
let map ;
330
371
if ( actionOptions . sourceMaps ) {
@@ -368,9 +409,6 @@ export function buildWebpackBrowser(
368
409
noModuleFiles . push ( { ...file , file : newFilename } ) ;
369
410
}
370
411
371
- // Execute the bundle processing actions
372
- context . logger . info ( 'Generating ES5 bundles for differential loading...' ) ;
373
-
374
412
const processActions : typeof actions = [ ] ;
375
413
let processRuntimeAction : ProcessBundleOptions | undefined ;
376
414
const processResults : ProcessBundleResult [ ] = [ ] ;
@@ -389,29 +427,118 @@ export function buildWebpackBrowser(
389
427
options . subresourceIntegrity ? 'sha384' : undefined ,
390
428
) ;
391
429
430
+ // Execute the bundle processing actions
392
431
try {
432
+ context . logger . info ( 'Generating ES5 bundles for differential loading...' ) ;
433
+
393
434
for await ( const result of executor . processAll ( processActions ) ) {
394
435
processResults . push ( result ) ;
395
436
}
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.' ) ;
396
450
} finally {
397
451
executor . stop ( ) ;
398
452
}
399
453
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' } .` ,
408
522
) ;
409
- }
410
523
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
+ }
412
536
413
537
// Copy assets
414
538
if ( options . assets ) {
539
+ const outputPaths = i18n . shouldInline
540
+ ? [ ...i18n . inlineLocales ] . map ( l => path . join ( baseOutputPath , l ) )
541
+ : [ baseOutputPath ] ;
415
542
try {
416
543
await copyAssets (
417
544
normalizeAssetPatterns (
@@ -421,7 +548,7 @@ export function buildWebpackBrowser(
421
548
normalize ( projectRoot ) ,
422
549
projectSourceRoot === undefined ? undefined : normalize ( projectSourceRoot ) ,
423
550
) ,
424
- [ baseOutputPath ] ,
551
+ outputPaths ,
425
552
context . workspaceRoot ,
426
553
) ;
427
554
} catch ( err ) {
@@ -503,33 +630,29 @@ export function buildWebpackBrowser(
503
630
}
504
631
505
632
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
+ }
529
652
}
530
- } else {
531
- return { success } ;
532
653
}
654
+
655
+ return { success } ;
533
656
} ) ,
534
657
concatMap ( buildEvent => {
535
658
if ( buildEvent . success && ! options . watch && options . serviceWorker ) {
@@ -563,6 +686,35 @@ export function buildWebpackBrowser(
563
686
) ;
564
687
}
565
688
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
+
566
718
function mapErrorToMessage ( error : unknown ) : string | undefined {
567
719
if ( error instanceof Error ) {
568
720
return error . message ;
0 commit comments