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 <[email protected]>
  • Loading branch information
victorporof committed Jul 12, 2022
1 parent 3d6ed0c commit f036376
Show file tree
Hide file tree
Showing 3 changed files with 182 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_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 @@ -190,6 +194,19 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
include.push(/css$/);
}

if (!vendorSourceMap) {
extraPlugins.push(
new DevToolsIgnorePlugin({
vendors: {
test: VENDORS_TEST,
},
runtime: !isPlatformServer && {
test: RUNTIME_TEST,
},
}),
);
}

extraPlugins.push(
new SourceMapDevToolPlugin({
filename: '[file].map',
Expand Down Expand Up @@ -434,7 +451,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,73 @@
/**
* @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 };
}

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 {
constructor(private 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,
},
(assets) => {
let vendorsTest: RegExp | null = null;
let runtimeTest: RegExp | null = null;
if (this.options.vendors) {
vendorsTest = this.options.vendors.test;
}
if (this.options.runtime) {
runtimeTest = this.options.runtime.test;
}
if (!vendorsTest && !runtimeTest) {
return;
}

for (const [name, asset] of Object.entries(assets)) {
const map = asset.map() as Object & SourceMap;
if (!map) {
continue;
}

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

compilation.updateAsset(name, new SourceMapSource(asset.source(), name, map));
}
},
);
});
}
}

0 comments on commit f036376

Please sign in to comment.