diff --git a/packages/webpack5/__tests__/plugins/FixSourceMapUrlPlugin.spec.ts b/packages/webpack5/__tests__/plugins/FixSourceMapUrlPlugin.spec.ts index 55b471880b..2fe74bc973 100644 --- a/packages/webpack5/__tests__/plugins/FixSourceMapUrlPlugin.spec.ts +++ b/packages/webpack5/__tests__/plugins/FixSourceMapUrlPlugin.spec.ts @@ -1,6 +1,3 @@ -import { resolve } from 'path'; -import { pathToFileURL } from 'url'; - import FixSourceMapUrlPlugin from '../../src/plugins/FixSourceMapUrlPlugin'; function createCompiler() { @@ -23,6 +20,9 @@ function createCompiler() { emitHandler(compilation); }, + hasEmitHandler() { + return !!emitHandler; + }, }; } @@ -38,11 +38,46 @@ function createCompilation(source: string) { } describe('FixSourceMapUrlPlugin', () => { - it('encodes spaces in rewritten source map urls', () => { - const outputPath = '/Users/test/my tns app/platforms/android/dist'; + it('rewrites sourceMappingURL to the devtoolsHost origin', () => { + const devtoolsHost = 'http://127.0.0.1:41500'; + const { compiler, run } = createCompiler(); + + new FixSourceMapUrlPlugin({ devtoolsHost }).apply(compiler); + + const compilation = createCompilation( + 'console.log("test");\n//# sourceMappingURL=bundle.js.map', + ); + run(compilation); + + expect(compilation.assets['bundle.js'].source()).toContain( + `//# sourceMappingURL=${devtoolsHost}/bundle.js.map`, + ); + }); + + it('strips leading slashes from the map path when joining with the host', () => { + const devtoolsHost = 'http://127.0.0.1:41500'; + const { compiler, run } = createCompiler(); + + new FixSourceMapUrlPlugin({ devtoolsHost }).apply(compiler); + + const compilation = createCompilation( + 'console.log("test");\n//# sourceMappingURL=/bundle.js.map', + ); + run(compilation); + + expect(compilation.assets['bundle.js'].source()).toContain( + `//# sourceMappingURL=${devtoolsHost}/bundle.js.map`, + ); + expect(compilation.assets['bundle.js'].source()).not.toContain( + `${devtoolsHost}//`, + ); + }); + + it('strips trailing slashes from the devtoolsHost', () => { + const devtoolsHost = 'http://127.0.0.1:41500/'; const { compiler, run } = createCompiler(); - new FixSourceMapUrlPlugin({ outputPath }).apply(compiler); + new FixSourceMapUrlPlugin({ devtoolsHost }).apply(compiler); const compilation = createCompilation( 'console.log("test");\n//# sourceMappingURL=bundle.js.map', @@ -50,18 +85,16 @@ describe('FixSourceMapUrlPlugin', () => { run(compilation); expect(compilation.assets['bundle.js'].source()).toContain( - `//# sourceMappingURL=${pathToFileURL(resolve(outputPath, 'bundle.js.map')).toString()}`, + `//# sourceMappingURL=http://127.0.0.1:41500/bundle.js.map`, ); - expect(compilation.assets['bundle.js'].source()).toContain('%20'); }); it('leaves absolute source map urls unchanged', () => { - const outputPath = '/Users/test/my tns app/platforms/android/dist'; - const existingUrl = - 'file:///Users/test/my%20tns%20app/platforms/android/dist/bundle.js.map'; + const devtoolsHost = 'http://127.0.0.1:41500'; + const existingUrl = 'http://example.test/bundle.js.map'; const { compiler, run } = createCompiler(); - new FixSourceMapUrlPlugin({ outputPath }).apply(compiler); + new FixSourceMapUrlPlugin({ devtoolsHost }).apply(compiler); const compilation = createCompilation( `console.log("test");\n//# sourceMappingURL=${existingUrl}`, @@ -72,4 +105,12 @@ describe('FixSourceMapUrlPlugin', () => { `//# sourceMappingURL=${existingUrl}`, ); }); + + it('no-ops and skips tapping hooks when devtoolsHost is not provided', () => { + const { compiler, hasEmitHandler } = createCompiler(); + + new FixSourceMapUrlPlugin({}).apply(compiler); + + expect(hasEmitHandler()).toBe(false); + }); }); diff --git a/packages/webpack5/src/configuration/base.ts b/packages/webpack5/src/configuration/base.ts index 516347c240..3c2a7c7d2e 100644 --- a/packages/webpack5/src/configuration/base.ts +++ b/packages/webpack5/src/configuration/base.ts @@ -184,11 +184,20 @@ export default function (config: Config, env: IWebpackEnv = _env): Config { // Use devtool for both CommonJS and ESM - let webpack handle source mapping properly config.devtool(sourceMapType); - // For ESM builds, fix the sourceMappingURL to use correct paths - if (sourceMapType && sourceMapType !== 'hidden-source-map') { + // When the CLI has started its loopback dev-host HTTP server, rewrite the + // sourceMappingURL to point at it so Chrome DevTools can fetch .map files + // over HTTP with CORS. Dev-only; plugin self-noops without env.devtoolsHost. + if ( + mode === 'development' && + env.devtoolsHost && + sourceMapType && + sourceMapType !== 'hidden-source-map' + ) { config .plugin('FixSourceMapUrlPlugin') - .use(FixSourceMapUrlPlugin as any, [{ outputPath }]); + .use(FixSourceMapUrlPlugin as any, [ + { devtoolsHost: env.devtoolsHost }, + ]); } // when using hidden-source-map, output source maps to the `platforms/{platformName}-sourceMaps` folder diff --git a/packages/webpack5/src/index.ts b/packages/webpack5/src/index.ts index 8adc3bfaa8..6f4e35359a 100644 --- a/packages/webpack5/src/index.ts +++ b/packages/webpack5/src/index.ts @@ -53,6 +53,16 @@ export interface IWebpackEnv { // enable commonjs modules (default: ES modules, esm) commonjs?: boolean; + /** + * CLI-populated origin of the loopback HTTP server that serves the + * webpack output directory (e.g. "http://127.0.0.1:41500"). Used only + * in development to rewrite sourceMappingURL comments so Chrome + * DevTools can fetch .map files over HTTP with CORS. + * + * Do not set manually. + */ + devtoolsHost?: string; + // misc replace?: string[] | string; watchNodeModules?: boolean; diff --git a/packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts b/packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts index 1ae51b2d06..2d4466bbbb 100644 --- a/packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts +++ b/packages/webpack5/src/plugins/FixSourceMapUrlPlugin.ts @@ -1,33 +1,42 @@ -import { resolve } from 'path'; -import { pathToFileURL } from 'url'; import type { Compiler } from 'webpack'; import { sources } from 'webpack'; export interface FixSourceMapUrlPluginOptions { - outputPath: string; + devtoolsHost?: string; } /** - * Ensures sourceMappingURL points to the actual file:// location on device/emulator. - * Handles Webpack 5 asset sources (string/Buffer/Source objects). + * Rewrites `//# sourceMappingURL=` comments for development builds. + * + * When `devtoolsHost` (e.g. "http://127.0.0.1:41500") is provided by the CLI, + * the URL is rewritten to "/" so Chrome DevTools + * can fetch the map over HTTP with CORS. Without a devtoolsHost the + * existing URL is left untouched and webpack's default relative filename + * rides through to the runtime. */ export default class FixSourceMapUrlPlugin { constructor(private readonly options: FixSourceMapUrlPluginOptions) {} apply(compiler: Compiler) { + const { devtoolsHost } = this.options; + if (!devtoolsHost) { + return; + } + const wp: any = (compiler as any).webpack; const hasProcessAssets = !!wp?.Compilation?.PROCESS_ASSETS_STAGE_DEV_TOOLING && !!(compiler as any).hooks?.thisCompilation; + const hostBase = devtoolsHost.replace(/\/+$/, ''); + const getSourceMapUrl = (sourceMapPath: string): string => { if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(sourceMapPath)) { return sourceMapPath; } - return pathToFileURL( - resolve(this.options.outputPath, sourceMapPath), - ).toString(); + const normalized = sourceMapPath.replace(/^\/+/, ''); + return `${hostBase}/${normalized}`; }; const toStringContent = (content: any): string => { @@ -73,7 +82,6 @@ export default class FixSourceMapUrlPlugin { } let source = toStringContent(rawSource); - // Replace sourceMappingURL to use file:// protocol pointing to actual location source = source.replace( /\/\/\# sourceMappingURL=(.+\.map(?:\?[^\s]*)?)/g, (_match, sourceMapPath: string) =>