Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): Identify third-party sources in …
Browse files Browse the repository at this point in the history
…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)
  • Loading branch information
victorporof authored and clydin committed Jul 18, 2022
1 parent 163618e commit b5f6d86
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { Architect } from '@angular-devkit/architect';
import * as path from 'path';
import { browserBuild, createArchitect, host } from '../../../testing/test-utils';

// Following the naming conventions from
// https://sourcemaps.info/spec.html#h.ghqpj1ytqjbm
const IGNORE_LIST = 'x_google_ignoreList';

describe('Browser Builder external source map', () => {
const target = { project: 'app', target: 'build' };
let architect: Architect;
Expand Down Expand Up @@ -50,3 +54,70 @@ describe('Browser Builder external source map', () => {
expect(hasTsSourcePaths).toBe(false, `vendor.js.map not should have '.ts' extentions`);
});
});

describe('Identifying third-party code in source maps', () => {
interface SourceMap {
sources: string[];
[IGNORE_LIST]: number[];
}

const target = { project: 'app', target: 'build' };
let architect: Architect;

beforeEach(async () => {
await host.initialize().toPromise();
architect = (await createArchitect(host.root())).architect;
});
afterEach(async () => host.restore().toPromise());

it('specifies which sources are third party when vendor processing is disabled', async () => {
const overrides = {
sourceMap: {
scripts: true,
vendor: false,
},
};

const { files } = await browserBuild(architect, host, target, overrides);
const mainMap: SourceMap = JSON.parse(await files['main.js.map']);
const polyfillsMap: SourceMap = JSON.parse(await files['polyfills.js.map']);
const runtimeMap: SourceMap = JSON.parse(await files['runtime.js.map']);
const vendorMap: SourceMap = JSON.parse(await files['vendor.js.map']);

expect(mainMap[IGNORE_LIST]).not.toBeUndefined();
expect(polyfillsMap[IGNORE_LIST]).not.toBeUndefined();
expect(runtimeMap[IGNORE_LIST]).not.toBeUndefined();
expect(vendorMap[IGNORE_LIST]).not.toBeUndefined();

expect(mainMap[IGNORE_LIST].length).toEqual(0);
expect(polyfillsMap[IGNORE_LIST].length).not.toEqual(0);
expect(runtimeMap[IGNORE_LIST].length).not.toEqual(0);
expect(vendorMap[IGNORE_LIST].length).not.toEqual(0);

const thirdPartyInMain = mainMap.sources.some((s) => s.includes('node_modules'));
const thirdPartyInPolyfills = polyfillsMap.sources.some((s) => s.includes('node_modules'));
const thirdPartyInRuntime = runtimeMap.sources.some((s) => s.includes('webpack'));
const thirdPartyInVendor = vendorMap.sources.some((s) => s.includes('node_modules'));
expect(thirdPartyInMain).toBe(false, `main.js.map should not include any node modules`);
expect(thirdPartyInPolyfills).toBe(true, `polyfills.js.map should include some node modules`);
expect(thirdPartyInRuntime).toBe(true, `runtime.js.map should include some webpack code`);
expect(thirdPartyInVendor).toBe(true, `vendor.js.map should include some node modules`);

// All sources in the main map are first-party.
expect(mainMap.sources.filter((_, i) => !mainMap[IGNORE_LIST].includes(i))).toEqual([
'./src/app/app.component.ts',
'./src/app/app.module.ts',
'./src/environments/environment.ts',
'./src/main.ts',
]);

// Only some sources in the polyfills map are first-party.
expect(polyfillsMap.sources.filter((_, i) => !polyfillsMap[IGNORE_LIST].includes(i))).toEqual([
'./src/polyfills.ts',
]);

// None of the sources in the runtime and vendor maps are first-party.
expect(runtimeMap.sources.filter((_, i) => !runtimeMap[IGNORE_LIST].includes(i))).toEqual([]);
expect(vendorMap.sources.filter((_, i) => !vendorMap[IGNORE_LIST].includes(i))).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
JsonStatsPlugin,
ScriptsWebpackPlugin,
} from '../plugins';
import { DevToolsIgnorePlugin } from '../plugins/devtools-ignore-plugin';
import { NamedChunksPlugin } from '../plugins/named-chunks-plugin';
import { ProgressPlugin } from '../plugins/progress-plugin';
import { TransferSizePlugin } from '../plugins/transfer-size-plugin';
Expand All @@ -45,6 +46,8 @@ import {
globalScriptsByBundleName,
} from '../utils/helpers';

const VENDORS_TEST = /[\\/]node_modules[\\/]/;

// eslint-disable-next-line max-lines-per-function
export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Configuration> {
const {
Expand Down Expand Up @@ -190,6 +193,8 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
include.push(/css$/);
}

extraPlugins.push(new DevToolsIgnorePlugin());

extraPlugins.push(
new SourceMapDevToolPlugin({
filename: '[file].map',
Expand Down Expand Up @@ -434,7 +439,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
name: 'vendor',
chunks: (chunk) => chunk.name === 'main',
enforce: true,
test: /[\\/]node_modules[\\/]/,
test: VENDORS_TEST,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import { Compilation, Compiler } from 'webpack';

// Following the naming conventions from
// https://sourcemaps.info/spec.html#h.ghqpj1ytqjbm
const IGNORE_LIST = 'x_google_ignoreList';

const PLUGIN_NAME = 'devtools-ignore-plugin';

interface SourceMap {
sources: string[];
[IGNORE_LIST]: number[];
}

/**
* This plugin adds a field to source maps that identifies which sources are
* vendored or runtime-injected (aka third-party) sources. These are consumed by
* Chrome DevTools to automatically ignore-list sources.
*/
export class DevToolsIgnorePlugin {
apply(compiler: Compiler) {
const { RawSource } = compiler.webpack.sources;

compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_DEV_TOOLING,
additionalAssets: true,
},
(assets) => {
for (const [name, asset] of Object.entries(assets)) {
// Instead of using `asset.map()` to fetch the source maps from
// SourceMapSource assets, process them directly as a RawSource.
// This is because `.map()` is slow and can take several seconds.
if (!name.endsWith('.map')) {
// Ignore non source map files.
continue;
}

const mapContent = asset.source().toString();
if (!mapContent) {
continue;
}

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
ignoreList.push(index);
}
}

map[IGNORE_LIST] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));
}
},
);
});
}
}

0 comments on commit b5f6d86

Please sign in to comment.