Skip to content

Commit

Permalink
feat(@angular/cli): Identify third-party sources in sourcemaps
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
victorporof committed Jul 12, 2022
1 parent 3d6ed0c commit 5632024
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 2 deletions.
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_devtoolsIgnore';

describe('Browser Builder external source map', () => {
const target = { project: 'app', target: 'build' };
let architect: Architect;
Expand Down Expand Up @@ -50,3 +54,90 @@ 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('is a noop when vendoring processing is enabled', async () => {
const overrides = {
sourceMap: {
scripts: true,
vendor: true,
},
};

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]).toBeUndefined();
expect(polyfillsMap[IGNORE_LIST]).toBeUndefined();
expect(runtimeMap[IGNORE_LIST]).toBeUndefined();
expect(vendorMap[IGNORE_LIST]).toBeUndefined();
});

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,9 @@ import {
globalScriptsByBundleName,
} from '../utils/helpers';

const VENDORS_TEST = /[\\/]node_modules[\\/]/;
const RUNTIME_TEST = /^webpack\//;

// eslint-disable-next-line max-lines-per-function
export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Configuration> {
const {
Expand Down Expand Up @@ -434,12 +438,26 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
name: 'vendor',
chunks: (chunk) => chunk.name === 'main',
enforce: true,
test: /[\\/]node_modules[\\/]/,
test: VENDORS_TEST,
},
},
},
},
plugins: [new NamedChunksPlugin(), new DedupeModuleResolvePlugin({ verbose }), ...extraPlugins],
plugins: [
new NamedChunksPlugin(),
new DedupeModuleResolvePlugin({ verbose }),
new DevToolsIgnorePlugin({
vendors: !vendorSourceMap &&
!!vendorChunk && {
test: VENDORS_TEST,
},
runtime: !vendorSourceMap &&
!isPlatformServer && {
test: RUNTIME_TEST,
},
}),
...extraPlugins,
],
node: false,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* @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_devtoolsIgnore';

const PLUGIN_NAME = 'devtools-ignore-plugin';

interface Options {
vendors?: false | { test: RegExp };
runtime?: false | { test: RegExp };
}
/**
* 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 {
options: Options;

constructor(options: Options) {
this.options = options;
}

apply(compiler: Compiler) {
const { SourceMapSource } = 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) => {
if (!this.options.vendors || !this.options.runtime) {
return;
}
const vendorsTest = this.options.vendors.test;
const runtimeTest = this.options.runtime.test;

for (const [name, asset] of Object.entries(assets)) {
const source = asset.source();
const map = asset.map() as Object & {
sources: string[];
[IGNORE_LIST]: number[];
};
if (!map) {
continue;
}

map[IGNORE_LIST] = Object.entries(map.sources)
.filter(([, source]) => source.match(vendorsTest) || source.match(runtimeTest))
.map(([index]) => +index);

compilation.assets[name] = new SourceMapSource(source, name, map);
}
},
);
});
}
}

0 comments on commit 5632024

Please sign in to comment.