Skip to content

Commit 3c2fb5c

Browse files
committed
refactor(@angular-devkit/build-angular): avoid loading Webpack for differential loading sourcemaps
The `@ampproject/remapping` package is now used for source map processing instead of Webpack for differential loading and i18n processing. This dependency is already used within the recently added JavaScript optimizer refactoring and reduces the amount of code that needs to be loaded into each worker to support differential loading sourcemaps.
1 parent 356f25a commit 3c2fb5c

File tree

3 files changed

+46
-187
lines changed

3 files changed

+46
-187
lines changed

packages/angular_devkit/build_angular/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ ts_library(
178178
"@npm//sass",
179179
"@npm//sass-loader",
180180
"@npm//semver",
181-
"@npm//source-map",
182181
"@npm//source-map-loader",
183182
"@npm//source-map-support",
184183
"@npm//style-loader",

packages/angular_devkit/build_angular/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
"sass": "1.35.1",
6262
"sass-loader": "12.1.0",
6363
"semver": "7.3.5",
64-
"source-map": "0.7.3",
6564
"source-map-loader": "3.0.0",
6665
"source-map-support": "0.5.19",
6766
"style-loader": "3.0.0",

packages/angular_devkit/build_angular/src/utils/process-bundle.ts

Lines changed: 46 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9+
import remapping from '@ampproject/remapping';
910
import {
1011
NodePath,
1112
ParseResult,
@@ -21,14 +22,16 @@ import * as cacache from 'cacache';
2122
import { createHash } from 'crypto';
2223
import * as fs from 'fs';
2324
import * as path from 'path';
24-
import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map';
2525
import { minify } from 'terser';
2626
import { workerData } from 'worker_threads';
2727
import { allowMangle, allowMinify, shouldBeautify } from './environment-options';
2828
import { I18nOptions } from './i18n-options';
2929

3030
type 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
3437
let webpackSources: typeof import('webpack').sources | undefined;
@@ -114,10 +117,7 @@ export async function process(options: ProcessBundleOptions): Promise<ProcessBun
114117
const downlevelFilename = filename.replace(/\-(es20\d{2}|esnext)/, '-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-
309187
async 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

376260
async 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

422292
function 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

Comments
 (0)