66 * found in the LICENSE file at https://angular.io/license
77 */
88
9+ import remapping from '@ampproject/remapping' ;
910import {
1011 NodePath ,
1112 ParseResult ,
@@ -21,14 +22,16 @@ import * as cacache from 'cacache';
2122import { createHash } from 'crypto' ;
2223import * as fs from 'fs' ;
2324import * as path from 'path' ;
24- import { RawSourceMap , SourceMapConsumer , SourceMapGenerator } from 'source-map' ;
2525import { minify } from 'terser' ;
2626import { workerData } from 'worker_threads' ;
2727import { allowMangle , allowMinify , shouldBeautify } from './environment-options' ;
2828import { I18nOptions } from './i18n-options' ;
2929
3030type LocalizeUtilities = typeof import ( '@angular/localize/src/tools/src/source_file_utils' ) ;
3131
32+ // Extract Sourcemap input type from the remapping function since it is not currently exported
33+ type SourceMapInput = Exclude < Parameters < typeof remapping > [ 0 ] , unknown [ ] > ;
34+
3235// Lazy loaded webpack-sources object
3336// Webpack is only imported if needed during the processing
3437let webpackSources : typeof import ( 'webpack' ) . sources | undefined ;
@@ -114,10 +117,7 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
114117 const downlevelFilename = filename . replace ( / \- ( e s 2 0 \d { 2 } | e s n e x t ) / , '-es5' ) ;
115118 const downlevel = ! options . optimizeOnly ;
116119 const sourceCode = options . code ;
117- const sourceMap = options . map ? JSON . parse ( options . map ) : undefined ;
118120
119- let downlevelCode ;
120- let downlevelMap ;
121121 if ( downlevel ) {
122122 const { supportedBrowsers : targets = [ ] } = options ;
123123
@@ -158,36 +158,17 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
158158 ] ,
159159 minified : allowMinify && ! ! options . optimize ,
160160 compact : ! shouldBeautify && ! ! options . optimize ,
161- sourceMaps : ! ! sourceMap ,
161+ sourceMaps : ! ! options . map ,
162162 } ) ;
163163
164164 if ( ! transformResult || ! transformResult . code ) {
165165 throw new Error ( `Unknown error occurred processing bundle for "${ options . filename } ".` ) ;
166166 }
167- downlevelCode = transformResult . code ;
168-
169- if ( sourceMap && transformResult . map ) {
170- // String length is used as an estimate for byte length
171- const fastSourceMaps = sourceCode . length > FAST_SOURCEMAP_THRESHOLD ;
172-
173- downlevelMap = await mergeSourceMaps (
174- sourceCode ,
175- sourceMap ,
176- downlevelCode ,
177- transformResult . map ,
178- filename ,
179- // When not optimizing, the sourcemaps are significantly less complex
180- // and can use the higher fidelity merge
181- ! ! options . optimize && fastSourceMaps ,
182- ) ;
183- }
184- }
185167
186- if ( downlevelCode ) {
187168 result . downlevel = await processBundle ( {
188169 ...options ,
189- code : downlevelCode ,
190- map : downlevelMap ,
170+ code : transformResult . code ,
171+ downlevelMap : ( transformResult . map as SourceMapInput ) ?? undefined ,
191172 filename : path . join ( basePath , downlevelFilename ) ,
192173 isOriginal : false ,
193174 } ) ;
@@ -203,156 +184,59 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
203184 return result ;
204185}
205186
206- async function mergeSourceMaps (
207- inputCode : string ,
208- inputSourceMap : RawSourceMap ,
209- resultCode : string ,
210- resultSourceMap : RawSourceMap ,
211- filename : string ,
212- fast = false ,
213- ) : Promise < RawSourceMap > {
214- // Webpack 5 terser sourcemaps currently fail merging with the high-quality method
215- if ( fast ) {
216- return mergeSourceMapsFast ( inputSourceMap , resultSourceMap ) ;
217- }
218-
219- // Load Webpack only when needed
220- if ( webpackSources === undefined ) {
221- webpackSources = ( await import ( 'webpack' ) ) . sources ;
222- }
223-
224- // SourceMapSource produces high-quality sourcemaps
225- // Final sourcemap will always be available when providing the input sourcemaps
226- const finalSourceMap = new webpackSources . SourceMapSource (
227- resultCode ,
228- filename ,
229- resultSourceMap ,
230- inputCode ,
231- inputSourceMap ,
232- true ,
233- ) . map ( ) ;
234-
235- return finalSourceMap as RawSourceMap ;
236- }
237-
238- async function mergeSourceMapsFast ( first : RawSourceMap , second : RawSourceMap ) {
239- const sourceRoot = first . sourceRoot ;
240- const generator = new SourceMapGenerator ( ) ;
241-
242- // sourcemap package adds the sourceRoot to all position source paths if not removed
243- delete first . sourceRoot ;
244-
245- await SourceMapConsumer . with ( first , null , ( originalConsumer ) => {
246- return SourceMapConsumer . with ( second , null , ( newConsumer ) => {
247- newConsumer . eachMapping ( ( mapping ) => {
248- if ( mapping . originalLine === null ) {
249- return ;
250- }
251- const originalPosition = originalConsumer . originalPositionFor ( {
252- line : mapping . originalLine ,
253- column : mapping . originalColumn ,
254- } ) ;
255- if (
256- originalPosition . line === null ||
257- originalPosition . column === null ||
258- originalPosition . source === null
259- ) {
260- return ;
261- }
262- generator . addMapping ( {
263- generated : {
264- line : mapping . generatedLine ,
265- column : mapping . generatedColumn ,
266- } ,
267- name : originalPosition . name || undefined ,
268- original : {
269- line : originalPosition . line ,
270- column : originalPosition . column ,
271- } ,
272- source : originalPosition . source ,
273- } ) ;
274- } ) ;
275- } ) ;
276- } ) ;
277-
278- const map = generator . toJSON ( ) ;
279- map . file = second . file ;
280- map . sourceRoot = sourceRoot ;
281-
282- // Add source content if present
283- if ( first . sourcesContent ) {
284- // Source content array is based on index of sources
285- const sourceContentMap = new Map < string , number > ( ) ;
286- for ( let i = 0 ; i < first . sources . length ; i ++ ) {
287- // make paths "absolute" so they can be compared (`./a.js` and `a.js` are equivalent)
288- sourceContentMap . set ( path . resolve ( '/' , first . sources [ i ] ) , i ) ;
289- }
290- map . sourcesContent = [ ] ;
291- for ( let i = 0 ; i < map . sources . length ; i ++ ) {
292- const contentIndex = sourceContentMap . get ( path . resolve ( '/' , map . sources [ i ] ) ) ;
293- if ( contentIndex === undefined ) {
294- map . sourcesContent . push ( '' ) ;
295- } else {
296- map . sourcesContent . push ( first . sourcesContent [ contentIndex ] ) ;
297- }
298- }
299- }
300-
301- // Put the sourceRoot back
302- if ( sourceRoot ) {
303- first . sourceRoot = sourceRoot ;
304- }
305-
306- return map ;
307- }
308-
309187async function processBundle (
310- options : Omit < ProcessBundleOptions , 'map' > & { isOriginal : boolean ; map ?: string | RawSourceMap } ,
188+ options : ProcessBundleOptions & {
189+ isOriginal : boolean ;
190+ downlevelMap ?: SourceMapInput ;
191+ } ,
311192) : Promise < ProcessBundleFile > {
312193 const {
313194 optimize,
314195 isOriginal,
315196 code,
316197 map,
198+ downlevelMap,
317199 filename : filepath ,
318200 hiddenSourceMaps,
319201 cacheKeys = [ ] ,
320202 integrityAlgorithm,
321203 } = options ;
322204
323- const rawMap = typeof map === 'string' ? ( JSON . parse ( map ) as RawSourceMap ) : map ;
324205 const filename = path . basename ( filepath ) ;
206+ let resultCode = code ;
325207
326- let result : {
327- code : string ;
328- map : RawSourceMap | undefined ;
329- } ;
330-
331- if ( rawMap ) {
332- rawMap . file = filename ;
333- }
334-
208+ let optimizeResult ;
335209 if ( optimize ) {
336- result = await terserMangle ( code , {
210+ optimizeResult = await terserMangle ( code , {
337211 filename,
338- map : rawMap ,
212+ sourcemap : ! ! map ,
339213 compress : ! isOriginal , // We only compress bundles which are downlevelled.
340214 ecma : isOriginal ? 2015 : 5 ,
341215 } ) ;
342- } else {
343- result = {
344- map : rawMap ,
345- code,
346- } ;
216+ resultCode = optimizeResult . code ;
347217 }
348218
349219 let mapContent : string | undefined ;
350- if ( result . map ) {
220+ if ( map ) {
351221 if ( ! hiddenSourceMaps ) {
352- result . code += `\n//# sourceMappingURL=${ filename } .map` ;
222+ resultCode += `\n//# sourceMappingURL=${ filename } .map` ;
223+ }
224+
225+ const partialSourcemaps : SourceMapInput [ ] = [ ] ;
226+ if ( optimizeResult && optimizeResult . map ) {
227+ partialSourcemaps . push ( optimizeResult . map ) ;
228+ }
229+ if ( downlevelMap ) {
230+ partialSourcemaps . push ( downlevelMap ) ;
353231 }
354232
355- mapContent = JSON . stringify ( result . map ) ;
233+ if ( partialSourcemaps . length > 0 ) {
234+ partialSourcemaps . push ( map ) ;
235+ const fullSourcemap = remapping ( partialSourcemaps , ( ) => null ) ;
236+ mapContent = JSON . stringify ( fullSourcemap ) ;
237+ } else {
238+ mapContent = map ;
239+ }
356240
357241 await cachePut (
358242 mapContent ,
@@ -361,21 +245,21 @@ async function processBundle(
361245 fs . writeFileSync ( filepath + '.map' , mapContent ) ;
362246 }
363247
364- const fileResult = createFileEntry ( filepath , result . code , mapContent , integrityAlgorithm ) ;
248+ const fileResult = createFileEntry ( filepath , resultCode , mapContent , integrityAlgorithm ) ;
365249
366250 await cachePut (
367- result . code ,
251+ resultCode ,
368252 cacheKeys [ isOriginal ? CacheKey . OriginalCode : CacheKey . DownlevelCode ] ,
369253 fileResult . integrity ,
370254 ) ;
371- fs . writeFileSync ( filepath , result . code ) ;
255+ fs . writeFileSync ( filepath , resultCode ) ;
372256
373257 return fileResult ;
374258}
375259
376260async function terserMangle (
377261 code : string ,
378- options : { filename ?: string ; map ?: RawSourceMap ; compress ?: boolean ; ecma ?: 5 | 2015 } = { } ,
262+ options : { filename ?: string ; sourcemap ?: boolean ; compress ?: boolean ; ecma ?: 5 | 2015 } = { } ,
379263) {
380264 // Note: Investigate converting the AST instead of re-parsing
381265 // estree -> terser is already supported; need babel -> estree/terser
@@ -393,7 +277,7 @@ async function terserMangle(
393277 wrap_func_args : false ,
394278 } ,
395279 sourceMap :
396- ! ! options . map &&
280+ ! ! options . sourcemap &&
397281 ( {
398282 asObject : true ,
399283 // typings don't include asObject option
@@ -402,21 +286,7 @@ async function terserMangle(
402286 } ) ;
403287
404288 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
405- const outputCode = minifyOutput . code ! ;
406-
407- let outputMap ;
408- if ( options . map && minifyOutput . map ) {
409- outputMap = await mergeSourceMaps (
410- code ,
411- options . map ,
412- outputCode ,
413- minifyOutput . map as unknown as RawSourceMap ,
414- options . filename || '0' ,
415- code . length > FAST_SOURCEMAP_THRESHOLD ,
416- ) ;
417- }
418-
419- return { code : outputCode , map : outputMap } ;
289+ return { code : minifyOutput . code ! , map : minifyOutput . map as SourceMapInput | undefined } ;
420290}
421291
422292function createFileEntry (
@@ -644,7 +514,6 @@ export async function inlineLocales(options: InlineOptions) {
644514 }
645515
646516 const diagnostics = [ ] ;
647- const inputMap = options . map && ( JSON . parse ( options . map ) as RawSourceMap ) ;
648517 for ( const locale of i18n . inlineLocales ) {
649518 const isSourceLocale = locale === i18n . sourceLocale ;
650519 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -675,7 +544,7 @@ export async function inlineLocales(options: InlineOptions) {
675544 configFile : false ,
676545 plugins,
677546 compact : ! shouldBeautify ,
678- sourceMaps : ! ! inputMap ,
547+ sourceMaps : ! ! options . map ,
679548 } ) ;
680549
681550 diagnostics . push ( ...localeDiagnostics . messages ) ;
@@ -691,15 +560,8 @@ export async function inlineLocales(options: InlineOptions) {
691560 ) ;
692561 fs . writeFileSync ( outputPath , transformResult . code ) ;
693562
694- if ( inputMap && transformResult . map ) {
695- const outputMap = await mergeSourceMaps (
696- options . code ,
697- inputMap ,
698- transformResult . code ,
699- transformResult . map ,
700- options . filename ,
701- options . code . length > FAST_SOURCEMAP_THRESHOLD ,
702- ) ;
563+ if ( options . map && transformResult . map ) {
564+ const outputMap = remapping ( [ transformResult . map as SourceMapInput , options . map ] , ( ) => null ) ;
703565
704566 fs . writeFileSync ( outputPath + '.map' , JSON . stringify ( outputMap ) ) ;
705567 }
@@ -725,7 +587,7 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
725587 return inlineCopyOnly ( options ) ;
726588 }
727589
728- const inputMap = options . map && ( JSON . parse ( options . map ) as RawSourceMap ) ;
590+ const inputMap = ! ! options . map && ( JSON . parse ( options . map ) as { sourceRoot ?: string } ) ;
729591 // Cleanup source root otherwise it will be added to each source entry
730592 const mapSourceRoot = inputMap && inputMap . sourceRoot ;
731593 if ( inputMap ) {
@@ -741,8 +603,7 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
741603 for ( const locale of i18n . inlineLocales ) {
742604 const content = new ReplaceSource (
743605 inputMap
744- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
745- new SourceMapSource ( options . code , options . filename , inputMap as any )
606+ ? new SourceMapSource ( options . code , options . filename , inputMap )
746607 : new OriginalSource ( options . code , options . filename ) ,
747608 ) ;
748609
@@ -784,7 +645,7 @@ async function inlineLocalesDirect(ast: ParseResult, options: InlineOptions) {
784645
785646 const { source : outputCode , map : outputMap } = outputSource . sourceAndMap ( ) as {
786647 source : string ;
787- map : RawSourceMap ;
648+ map : { file : string ; sourceRoot ?: string } ;
788649 } ;
789650 const outputPath = path . join (
790651 options . outputPath ,
0 commit comments