Skip to content

Commit b5f6d86

Browse files
victorporofclydin
authored andcommitted
feat(@angular-devkit/build-angular): Identify third-party sources in sourcemaps
This PR includes a webpack plugin which adds a field to source maps that identifies which sources are vendored or runtime-injected (aka third-party) sources. These will be consumed by Chrome DevTools to automatically ignore-list sources. When vendor source map processing is enabled, this is interpreted as the developer intending to debug third-party code; in this case, the feature is disabled. Signed-off-by: Victor Porof <victorporof@chromium.org> (cherry picked from commit f3087dc)
1 parent 163618e commit b5f6d86

File tree

3 files changed

+146
-1
lines changed

3 files changed

+146
-1
lines changed

‎packages/angular_devkit/build_angular/src/builders/browser/specs/vendor-source-map_spec.ts

+71
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { Architect } from '@angular-devkit/architect';
1010
import * as path from 'path';
1111
import { browserBuild, createArchitect, host } from '../../../testing/test-utils';
1212

13+
// Following the naming conventions from
14+
// https://sourcemaps.info/spec.html#h.ghqpj1ytqjbm
15+
const IGNORE_LIST = 'x_google_ignoreList';
16+
1317
describe('Browser Builder external source map', () => {
1418
const target = { project: 'app', target: 'build' };
1519
let architect: Architect;
@@ -50,3 +54,70 @@ describe('Browser Builder external source map', () => {
5054
expect(hasTsSourcePaths).toBe(false, `vendor.js.map not should have '.ts' extentions`);
5155
});
5256
});
57+
58+
describe('Identifying third-party code in source maps', () => {
59+
interface SourceMap {
60+
sources: string[];
61+
[IGNORE_LIST]: number[];
62+
}
63+
64+
const target = { project: 'app', target: 'build' };
65+
let architect: Architect;
66+
67+
beforeEach(async () => {
68+
await host.initialize().toPromise();
69+
architect = (await createArchitect(host.root())).architect;
70+
});
71+
afterEach(async () => host.restore().toPromise());
72+
73+
it('specifies which sources are third party when vendor processing is disabled', async () => {
74+
const overrides = {
75+
sourceMap: {
76+
scripts: true,
77+
vendor: false,
78+
},
79+
};
80+
81+
const { files } = await browserBuild(architect, host, target, overrides);
82+
const mainMap: SourceMap = JSON.parse(await files['main.js.map']);
83+
const polyfillsMap: SourceMap = JSON.parse(await files['polyfills.js.map']);
84+
const runtimeMap: SourceMap = JSON.parse(await files['runtime.js.map']);
85+
const vendorMap: SourceMap = JSON.parse(await files['vendor.js.map']);
86+
87+
expect(mainMap[IGNORE_LIST]).not.toBeUndefined();
88+
expect(polyfillsMap[IGNORE_LIST]).not.toBeUndefined();
89+
expect(runtimeMap[IGNORE_LIST]).not.toBeUndefined();
90+
expect(vendorMap[IGNORE_LIST]).not.toBeUndefined();
91+
92+
expect(mainMap[IGNORE_LIST].length).toEqual(0);
93+
expect(polyfillsMap[IGNORE_LIST].length).not.toEqual(0);
94+
expect(runtimeMap[IGNORE_LIST].length).not.toEqual(0);
95+
expect(vendorMap[IGNORE_LIST].length).not.toEqual(0);
96+
97+
const thirdPartyInMain = mainMap.sources.some((s) => s.includes('node_modules'));
98+
const thirdPartyInPolyfills = polyfillsMap.sources.some((s) => s.includes('node_modules'));
99+
const thirdPartyInRuntime = runtimeMap.sources.some((s) => s.includes('webpack'));
100+
const thirdPartyInVendor = vendorMap.sources.some((s) => s.includes('node_modules'));
101+
expect(thirdPartyInMain).toBe(false, `main.js.map should not include any node modules`);
102+
expect(thirdPartyInPolyfills).toBe(true, `polyfills.js.map should include some node modules`);
103+
expect(thirdPartyInRuntime).toBe(true, `runtime.js.map should include some webpack code`);
104+
expect(thirdPartyInVendor).toBe(true, `vendor.js.map should include some node modules`);
105+
106+
// All sources in the main map are first-party.
107+
expect(mainMap.sources.filter((_, i) => !mainMap[IGNORE_LIST].includes(i))).toEqual([
108+
'./src/app/app.component.ts',
109+
'./src/app/app.module.ts',
110+
'./src/environments/environment.ts',
111+
'./src/main.ts',
112+
]);
113+
114+
// Only some sources in the polyfills map are first-party.
115+
expect(polyfillsMap.sources.filter((_, i) => !polyfillsMap[IGNORE_LIST].includes(i))).toEqual([
116+
'./src/polyfills.ts',
117+
]);
118+
119+
// None of the sources in the runtime and vendor maps are first-party.
120+
expect(runtimeMap.sources.filter((_, i) => !runtimeMap[IGNORE_LIST].includes(i))).toEqual([]);
121+
expect(vendorMap.sources.filter((_, i) => !vendorMap[IGNORE_LIST].includes(i))).toEqual([]);
122+
});
123+
});

‎packages/angular_devkit/build_angular/src/webpack/configs/common.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
JsonStatsPlugin,
3030
ScriptsWebpackPlugin,
3131
} from '../plugins';
32+
import { DevToolsIgnorePlugin } from '../plugins/devtools-ignore-plugin';
3233
import { NamedChunksPlugin } from '../plugins/named-chunks-plugin';
3334
import { ProgressPlugin } from '../plugins/progress-plugin';
3435
import { TransferSizePlugin } from '../plugins/transfer-size-plugin';
@@ -45,6 +46,8 @@ import {
4546
globalScriptsByBundleName,
4647
} from '../utils/helpers';
4748

49+
const VENDORS_TEST = /[\\/]node_modules[\\/]/;
50+
4851
// eslint-disable-next-line max-lines-per-function
4952
export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Configuration> {
5053
const {
@@ -190,6 +193,8 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
190193
include.push(/css$/);
191194
}
192195

196+
extraPlugins.push(new DevToolsIgnorePlugin());
197+
193198
extraPlugins.push(
194199
new SourceMapDevToolPlugin({
195200
filename: '[file].map',
@@ -434,7 +439,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
434439
name: 'vendor',
435440
chunks: (chunk) => chunk.name === 'main',
436441
enforce: true,
437-
test: /[\\/]node_modules[\\/]/,
442+
test: VENDORS_TEST,
438443
},
439444
},
440445
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { Compilation, Compiler } from 'webpack';
10+
11+
// Following the naming conventions from
12+
// https://sourcemaps.info/spec.html#h.ghqpj1ytqjbm
13+
const IGNORE_LIST = 'x_google_ignoreList';
14+
15+
const PLUGIN_NAME = 'devtools-ignore-plugin';
16+
17+
interface SourceMap {
18+
sources: string[];
19+
[IGNORE_LIST]: number[];
20+
}
21+
22+
/**
23+
* This plugin adds a field to source maps that identifies which sources are
24+
* vendored or runtime-injected (aka third-party) sources. These are consumed by
25+
* Chrome DevTools to automatically ignore-list sources.
26+
*/
27+
export class DevToolsIgnorePlugin {
28+
apply(compiler: Compiler) {
29+
const { RawSource } = compiler.webpack.sources;
30+
31+
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
32+
compilation.hooks.processAssets.tap(
33+
{
34+
name: PLUGIN_NAME,
35+
stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
36+
additionalAssets: true,
37+
},
38+
(assets) => {
39+
for (const [name, asset] of Object.entries(assets)) {
40+
// Instead of using `asset.map()` to fetch the source maps from
41+
// SourceMapSource assets, process them directly as a RawSource.
42+
// This is because `.map()` is slow and can take several seconds.
43+
if (!name.endsWith('.map')) {
44+
// Ignore non source map files.
45+
continue;
46+
}
47+
48+
const mapContent = asset.source().toString();
49+
if (!mapContent) {
50+
continue;
51+
}
52+
53+
const map = JSON.parse(mapContent) as SourceMap;
54+
const ignoreList = [];
55+
56+
for (const [index, path] of map.sources.entries()) {
57+
if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
58+
ignoreList.push(index);
59+
}
60+
}
61+
62+
map[IGNORE_LIST] = ignoreList;
63+
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));
64+
}
65+
},
66+
);
67+
});
68+
}
69+
}

0 commit comments

Comments
 (0)