From b6e5985a79ac71d6294e20c02b59ed940f663d26 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Thu, 7 Aug 2025 07:40:10 -0700 Subject: [PATCH 1/3] improve parseBundleOptionsFromBundleRequestUrl documentation Differential Revision: D79724370 --- packages/metro/src/HmrServer.js | 11 +-- packages/metro/src/Server.js | 11 +-- ...BundleOptionsFromBundleRequestUrl-test.js} | 84 ++++++++++++++----- ...parseBundleOptionsFromBundleRequestUrl.js} | 9 +- 4 files changed, 79 insertions(+), 36 deletions(-) rename packages/metro/src/lib/__tests__/{parseOptionsFromUrl-test.js => parseBundleOptionsFromBundleRequestUrl-test.js} (51%) rename packages/metro/src/lib/{parseOptionsFromUrl.js => parseBundleOptionsFromBundleRequestUrl.js} (89%) diff --git a/packages/metro/src/HmrServer.js b/packages/metro/src/HmrServer.js index 526aa2aca9..603a633990 100644 --- a/packages/metro/src/HmrServer.js +++ b/packages/metro/src/HmrServer.js @@ -27,7 +27,7 @@ const RevisionNotFoundError = require('./IncrementalBundler/RevisionNotFoundErro const debounceAsyncQueue = require('./lib/debounceAsyncQueue'); const formatBundlingError = require('./lib/formatBundlingError'); const getGraphId = require('./lib/getGraphId'); -const parseOptionsFromUrl = require('./lib/parseOptionsFromUrl'); +const parseBundleOptionsFromBundleRequestUrl = require('./lib/parseBundleOptionsFromBundleRequestUrl'); const splitBundleOptions = require('./lib/splitBundleOptions'); const transformHelpers = require('./lib/transformHelpers'); const { @@ -101,10 +101,11 @@ class HmrServer { ): Promise { requestUrl = this._config.server.rewriteRequestUrl(requestUrl); const clientUrl = nullthrows(url.parse(requestUrl, true)); - const {bundleType: _bundleType, ...options} = parseOptionsFromUrl( - requestUrl, - new Set(this._config.resolver.platforms), - ); + const {bundleType: _bundleType, ...options} = + parseBundleOptionsFromBundleRequestUrl( + requestUrl, + new Set(this._config.resolver.platforms), + ); const {entryFile, resolverOptions, transformOptions, graphOptions} = splitBundleOptions(options); diff --git a/packages/metro/src/Server.js b/packages/metro/src/Server.js index 1b0ad1b4e0..d773209b54 100644 --- a/packages/metro/src/Server.js +++ b/packages/metro/src/Server.js @@ -62,8 +62,8 @@ const ResourceNotFoundError = require('./IncrementalBundler/ResourceNotFoundErro const bundleToString = require('./lib/bundleToString'); const formatBundlingError = require('./lib/formatBundlingError'); const getGraphId = require('./lib/getGraphId'); +const parseBundleOptionsFromBundleRequestUrl = require('./lib/parseBundleOptionsFromBundleRequestUrl'); const parseJsonBody = require('./lib/parseJsonBody'); -const parseOptionsFromUrl = require('./lib/parseOptionsFromUrl'); const splitBundleOptions = require('./lib/splitBundleOptions'); const transformHelpers = require('./lib/transformHelpers'); const { @@ -578,10 +578,11 @@ class Server { }; _parseOptions(url: string): BundleOptions { - const {bundleType: _bundleType, ...bundleOptions} = parseOptionsFromUrl( - url, - new Set(this._config.resolver.platforms), - ); + const {bundleType: _bundleType, ...bundleOptions} = + parseBundleOptionsFromBundleRequestUrl( + url, + new Set(this._config.resolver.platforms), + ); return bundleOptions; } diff --git a/packages/metro/src/lib/__tests__/parseOptionsFromUrl-test.js b/packages/metro/src/lib/__tests__/parseBundleOptionsFromBundleRequestUrl-test.js similarity index 51% rename from packages/metro/src/lib/__tests__/parseOptionsFromUrl-test.js rename to packages/metro/src/lib/__tests__/parseBundleOptionsFromBundleRequestUrl-test.js index ea6c8f6ebd..cfe056fa8f 100644 --- a/packages/metro/src/lib/__tests__/parseOptionsFromUrl-test.js +++ b/packages/metro/src/lib/__tests__/parseBundleOptionsFromBundleRequestUrl-test.js @@ -11,18 +11,21 @@ 'use strict'; -const parseOptionsFromUrl = require('../parseOptionsFromUrl'); +const parseBundleOptionsFromBundleRequestUrl = require('../parseBundleOptionsFromBundleRequestUrl'); -describe('parseOptionsFromUrl', () => { +describe('parseBundleOptionsFromBundleRequestUrl', () => { test.each([['map'], ['bundle']])('detects %s requests', type => { expect( - parseOptionsFromUrl(`http://localhost/my/bundle.${type}`, new Set([])), + parseBundleOptionsFromBundleRequestUrl( + `http://localhost/my/bundle.${type}`, + new Set([]), + ), ).toMatchObject({bundleType: type}); }); test('retrieves the platform from the query parameters', () => { expect( - parseOptionsFromUrl( + parseBundleOptionsFromBundleRequestUrl( 'http://localhost/my/bundle.bundle?platform=ios', new Set([]), ), @@ -31,22 +34,32 @@ describe('parseOptionsFromUrl', () => { test('retrieves the platform from the pathname', () => { expect( - parseOptionsFromUrl( + parseBundleOptionsFromBundleRequestUrl( 'http://localhost/my/bundle.test.bundle', new Set(['test']), ), ).toMatchObject({platform: 'test'}); }); - test('infers the source map url from the pathname', () => { - expect( - parseOptionsFromUrl('http://localhost/my/bundle.bundle', new Set([])), - ).toMatchObject({sourceMapUrl: '//localhost/my/bundle.map'}); - }); + test.each(['absolute', 'relative'])( + '%s urls- infers the source url and source map url from the pathname', + type => { + const protocol = type === 'absolute' ? 'http:' : ''; + expect( + parseBundleOptionsFromBundleRequestUrl( + `${protocol}//localhost/my/bundle.bundle`, + new Set([]), + ), + ).toMatchObject({ + sourceMapUrl: '//localhost/my/bundle.map', + sourceUrl: `${protocol}//localhost/my/bundle.bundle`, + }); + }, + ); test('forces the HTTP protocol for iOS and Android platforms', () => { expect( - parseOptionsFromUrl( + parseBundleOptionsFromBundleRequestUrl( 'http://localhost/my/bundle.bundle?platform=ios', new Set(['ios']), ), @@ -55,7 +68,7 @@ describe('parseOptionsFromUrl', () => { }); expect( - parseOptionsFromUrl( + parseBundleOptionsFromBundleRequestUrl( 'http://localhost/my/bundle.bundle?platform=android', new Set(['android']), ), @@ -64,18 +77,42 @@ describe('parseOptionsFromUrl', () => { }); }); - test('always sets the `hot` option to `true`', () => { - expect( - parseOptionsFromUrl('http://localhost/my/bundle.bundle', new Set([])), - ).toMatchObject({hot: true}); - }); - test('retrieves stuff from HMR urls', () => { - expect(parseOptionsFromUrl('my/bundle.bundle', new Set([]))).toMatchObject({ + expect( + parseBundleOptionsFromBundleRequestUrl('my/bundle.bundle', new Set([])), + ).toMatchObject({ entryFile: './my/bundle', }); }); + // todo: fix + xtest.each(['absolute', 'relative'])( + '%s urls with ascii characters are encoded correctly', + type => { + const protocol = type === 'absolute' ? 'http:' : ''; + expect( + parseBundleOptionsFromBundleRequestUrl( + `${protocol}//localhost/my/%2530/%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.%C3%98%E0%B2%9A%F0%9F%98%81AA.bundle`, + new Set([]), + ), + ).toMatchObject({ + sourceMapUrl: + '//localhost/my/%2530/%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.%C3%98%E0%B2%9A%F0%9F%98%81AA.map', + sourceUrl: `${protocol}//localhost/my/%2530/%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.%C3%98%E0%B2%9A%F0%9F%98%81AA.bundle`, + entryFile: './my/%30/бундл.Øಚ😁AA', + }); + }, + ); + + test('always sets the `hot` option to `true`', () => { + expect( + parseBundleOptionsFromBundleRequestUrl( + 'http://localhost/my/bundle.bundle', + new Set([]), + ), + ).toMatchObject({hot: true}); + }); + describe.each([ ['dev', true], ['minify', false], @@ -85,20 +122,23 @@ describe('parseOptionsFromUrl', () => { ])('boolean option `%s`', (optionName, defaultValue) => { test(`defaults to \`${String(defaultValue)}\``, () => { expect( - parseOptionsFromUrl('http://localhost/my/bundle.bundle', new Set([])), + parseBundleOptionsFromBundleRequestUrl( + 'http://localhost/my/bundle.bundle', + new Set([]), + ), ).toMatchObject({[optionName]: defaultValue}); }); test('is retrieved from the url', () => { expect( - parseOptionsFromUrl( + parseBundleOptionsFromBundleRequestUrl( `http://localhost/my/bundle.bundle?${optionName}=true`, new Set([]), ), ).toMatchObject({[optionName]: true}); expect( - parseOptionsFromUrl( + parseBundleOptionsFromBundleRequestUrl( `http://localhost/my/bundle.bundle?${optionName}=false`, new Set([]), ), diff --git a/packages/metro/src/lib/parseOptionsFromUrl.js b/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js similarity index 89% rename from packages/metro/src/lib/parseOptionsFromUrl.js rename to packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js index 22e2ac19d6..95be577be1 100644 --- a/packages/metro/src/lib/parseOptionsFromUrl.js +++ b/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js @@ -41,15 +41,15 @@ const getTransformProfile = (transformProfile: string): TransformProfile => ? transformProfile : 'default'; -module.exports = function parseOptionsFromUrl( - normalizedRequestUrl: string, +module.exports = function parseBundleOptionsFromBundleRequestUrl( + rawNonJscSafeUrlEncodedUrl: string, platforms: Set, ): { ...BundleOptions, // Retained for backwards compatibility, unused in Metro, to be removed. bundleType: string, } { - const parsedURL = nullthrows(url.parse(normalizedRequestUrl, true)); // `true` to parse the query param as an object. + const parsedURL = nullthrows(url.parse(rawNonJscSafeUrlEncodedUrl, true)); // `true` to parse the query param as an object. const query = nullthrows(parsedURL.query); const pathname = query.bundleEntry || @@ -63,6 +63,7 @@ module.exports = function parseOptionsFromUrl( customResolverOptions: parseCustomResolverOptions(parsedURL), customTransformOptions: parseCustomTransformOptions(parsedURL), dev: getBoolean(query, 'dev', true), + // absolute and relative paths are converted to paths relative to root entryFile: pathname.replace(/^(?:\.?\/)?/, './').replace(/\.[^/.]+$/, ''), excludeSource: getBoolean(query, 'excludeSource', false), hot: true, @@ -89,7 +90,7 @@ module.exports = function parseOptionsFromUrl( }), sourcePaths: SourcePathsMode.cast(query.sourcePaths) ?? SourcePathsMode.Absolute, - sourceUrl: jscSafeUrl.toJscSafeUrl(normalizedRequestUrl), + sourceUrl: jscSafeUrl.toJscSafeUrl(rawNonJscSafeUrlEncodedUrl), unstable_transformProfile: getTransformProfile( query.unstable_transformProfile, ), From 7d0de18b241d5702b5d5016bf42f6375858d6b91 Mon Sep 17 00:00:00 2001 From: Bradley Ayers Date: Thu, 7 Aug 2025 07:40:25 -0700 Subject: [PATCH 2/3] support non-ASCII filesystem bundle paths with metro server (#1538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: In a codebase with non-ascii characters in file paths (e.g. Japanese or Chinese characters), metro has an error when serving bundles: ``` TypeError: Invalid character in header content ``` This was reported in the Expo project https://github.com/expo/expo/issues/27397 but it's not Expo code and was later closed. The problem is fairly simple—HTTP headers need to use ASCII characters only, but filesystems are not limited to ASCII so trying to put a filesystem path directly into a HTTP header value is going to cause a problem. The solution also seems relatively simple—URL encode the path so that it uses percent encoding of non-ASCII characters. As suggested in https://velog.io/demian/RN-%EC%98%A4%EB%A5%98-%ED%95%B4%EA%B2%B0-TypeError-Invalid-character-in-header-content-X-React-Native-Project-Root, some people were adding the following middleware to fix that issue: ``` function createMetroStatusMiddleware(metroConfig) { return (_req, res)=>{ res.setHeader("X-React-Native-Project-Root", new URL(metroConfig.projectRoot)); res.end("packager-status:running"); }; } ``` Instead, I went to the root cause, and improved the parsing of the URL for both `sourceUrl` and `sourceMapUrl` fixing both the issue with serving bundles and the issue with serving source maps. > **_NOTE:_** this problem stems from using the [deprecated `url.parse` instead of the newer URL](https://nodejs.org/api/url.html#urlparseurlstring-parsequerystring-slashesdenotehost). Changelog: [General][Fixed] Metro supporting non-ASCII bundle/sourcemaps paths Pull Request resolved: https://github.com/facebook/metro/pull/1538 Test Plan: Using a test diff D78806803 I managed to reproduce Metro not launching RNTester before this diff. 1. `buck2 install -r rntester-android` (with the modified bundle path) 2. `DEV=1 js1 run` 3. launch `RNTester` Before- app crashes: https://pxl.cl/7LFkb After- app launches, debugging works: {F1980565661} can even add the bundle to ignore-list and then it gets ignored: {F1980565660} Differential Revision: D78799358 --- ...eBundleOptionsFromBundleRequestUrl-test.js | 3 +-- .../parseBundleOptionsFromBundleRequestUrl.js | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/metro/src/lib/__tests__/parseBundleOptionsFromBundleRequestUrl-test.js b/packages/metro/src/lib/__tests__/parseBundleOptionsFromBundleRequestUrl-test.js index cfe056fa8f..38f39d3b50 100644 --- a/packages/metro/src/lib/__tests__/parseBundleOptionsFromBundleRequestUrl-test.js +++ b/packages/metro/src/lib/__tests__/parseBundleOptionsFromBundleRequestUrl-test.js @@ -85,8 +85,7 @@ describe('parseBundleOptionsFromBundleRequestUrl', () => { }); }); - // todo: fix - xtest.each(['absolute', 'relative'])( + test.each(['absolute', 'relative'])( '%s urls with ascii characters are encoded correctly', type => { const protocol = type === 'absolute' ? 'http:' : ''; diff --git a/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js b/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js index 95be577be1..b7ffbf6a5d 100644 --- a/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js +++ b/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js @@ -49,11 +49,20 @@ module.exports = function parseBundleOptionsFromBundleRequestUrl( // Retained for backwards compatibility, unused in Metro, to be removed. bundleType: string, } { - const parsedURL = nullthrows(url.parse(rawNonJscSafeUrlEncodedUrl, true)); // `true` to parse the query param as an object. + const relativeProtocolUrl = rawNonJscSafeUrlEncodedUrl.startsWith('//'); + const parsedURL = nullthrows( + url.parse( + (relativeProtocolUrl ? 'temp-protocol:' : '') + + rawNonJscSafeUrlEncodedUrl, + true, + ), + ); // `true` to parse the query param as an object. + const query = nullthrows(parsedURL.query); - const pathname = - query.bundleEntry || - (parsedURL.pathname != null ? decodeURIComponent(parsedURL.pathname) : ''); + + const pathname = query.bundleEntry || (parsedURL?.pathname ?? ''); + const decodedPathname = decodeURIComponent(pathname); + const platform = query.platform || parsePlatformFilePath(pathname, platforms).platform; const bundleType = getBundleType(path.extname(pathname).substr(1)); @@ -64,7 +73,9 @@ module.exports = function parseBundleOptionsFromBundleRequestUrl( customTransformOptions: parseCustomTransformOptions(parsedURL), dev: getBoolean(query, 'dev', true), // absolute and relative paths are converted to paths relative to root - entryFile: pathname.replace(/^(?:\.?\/)?/, './').replace(/\.[^/.]+$/, ''), + entryFile: decodedPathname + .replace(/^(?:\.?\/)?/, './') + .replace(/\.[^/.]+$/, ''), excludeSource: getBoolean(query, 'excludeSource', false), hot: true, inlineSourceMap: getBoolean(query, 'inlineSourceMap', false), From 2783c0bfb8ee381f899a17eef00fc66b40876993 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Thu, 7 Aug 2025 09:10:53 -0700 Subject: [PATCH 3/3] replaced deprecated `url.parse` with `URL` in `parseOptionsFromUrl` (#1548) Summary: Pull Request resolved: https://github.com/facebook/metro/pull/1548 Gets rid of the deprecated [`url.parse`](https://nodejs.org/api/url.html#urlparseurlstring-parsequerystring-slashesdenotehost) in favor of `URL` in `parseOptionsFromUrl`. Changelog: [Fixed] Replaced the deprecated `url.parse` with `URL` and correctly encode and decode source urls and source map urls Differential Revision: D78890065 --- .../__tests__/baseJSBundle-test.js | 63 ++++++ .../Serializers/__tests__/hmrJSBundle-test.js | 186 ++++++++++++++++++ .../DeltaBundler/Serializers/hmrJSBundle.js | 44 ++--- packages/metro/src/HmrServer.js | 39 ++-- packages/metro/src/Server.js | 70 ++++--- packages/metro/src/index.flow.js | 5 +- .../parseCustomResolverOptions-test.js | 7 +- .../parseCustomTransformOptions-test.js | 7 +- .../parseBundleOptionsFromBundleRequestUrl.js | 133 ++++++++----- .../src/lib/parseCustomResolverOptions.js | 14 +- .../src/lib/parseCustomTransformOptions.js | 21 +- scripts/eslint/base.js | 17 ++ 12 files changed, 448 insertions(+), 158 deletions(-) create mode 100644 packages/metro/src/DeltaBundler/Serializers/__tests__/hmrJSBundle-test.js diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js index 8692551378..373b931045 100644 --- a/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/baseJSBundle-test.js @@ -79,6 +79,23 @@ const barModule: Module<> = { getSource: () => Buffer.from('bar-source'), }; +const nonAsciiModule: Module<> = { + path: '/root/%30.бундл.Øಚ😁AA', + dependencies: new Map(), + inverseDependencies: new CountingSet(), + output: [ + { + type: 'js/module', + data: { + code: '__d(function() {/* code for ascii file with non ascii characters: %30.бундл.Øಚ😁AA */});', + map: [], + lineCount: 1, + }, + }, + ], + getSource: () => Buffer.from('bar-source'), +}; + const getRunModuleStatement = (moduleId: number | string) => `require(${JSON.stringify(moduleId)});`; @@ -144,6 +161,52 @@ test('should generate a very simple bundle', () => { `); }); +test('should generate a bundle with correct ascii characters parsing', () => { + expect( + baseJSBundle( + '/root/', + [polyfill], + { + dependencies: new Map([['/root/%30.бундл.Øಚ😁AA', nonAsciiModule]]), + entryPoints: new Set(['/root/%30.бундл.Øಚ😁AA']), + transformOptions, + }, + { + asyncRequireModulePath: '', + // $FlowFixMe[incompatible-call] createModuleId assumes numeric IDs - is this too strict? + createModuleId: filePath => path.basename(filePath), + dev: true, + getRunModuleStatement, + includeAsyncPaths: false, + inlineSourceMap: false, + modulesOnly: false, + processModuleFilter: () => true, + projectRoot: '/root', + runBeforeMainModule: [], + runModule: true, + serverRoot: '/root', + shouldAddToIgnoreList: () => false, + // expecting to receive these as already encoded URIs + sourceMapUrl: encodeURI('http://localhost/bundle.%30.бундл.Øಚ😁AA.map'), + sourceUrl: encodeURI('http://localhost/bundle.%30.бундл.Øಚ😁AA.js'), + getSourceUrl: null, + }, + ), + ).toMatchInlineSnapshot(` + Object { + "modules": Array [ + Array [ + "%30.бундл.Øಚ😁AA", + "__d(function() {/* code for ascii file with non ascii characters: %30.бундл.Øಚ😁AA */},\\"%30.бундл.Øಚ😁AA\\",[],\\"%30.бундл.Øಚ😁AA\\");", + ], + ], + "post": "//# sourceMappingURL=http://localhost/bundle.%2530.%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.%C3%98%E0%B2%9A%F0%9F%98%81AA.map + //# sourceURL=http://localhost/bundle.%2530.%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.%C3%98%E0%B2%9A%F0%9F%98%81AA.js", + "pre": "__d(function() {/* code for polyfill */});", + } + `); +}); + test('should add runBeforeMainModule statements if found in the graph', () => { expect( baseJSBundle( diff --git a/packages/metro/src/DeltaBundler/Serializers/__tests__/hmrJSBundle-test.js b/packages/metro/src/DeltaBundler/Serializers/__tests__/hmrJSBundle-test.js new file mode 100644 index 0000000000..50ded8e3e4 --- /dev/null +++ b/packages/metro/src/DeltaBundler/Serializers/__tests__/hmrJSBundle-test.js @@ -0,0 +1,186 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @oncall react_native + */ + +'use strict'; + +import type {Module, ReadOnlyGraph, TransformInputOptions} from '../../types'; + +import CountingSet from '../../../lib/CountingSet'; + +const hmrJSBundle = require('../hmrJSBundle'); + +const fooModule: Module<> = { + path: '/root/foo', + dependencies: new Map([ + [ + './bar', + { + absolutePath: '/root/bar', + data: { + data: {asyncType: null, isESMImport: false, locs: [], key: './bar'}, + name: './bar', + }, + }, + ], + ]), + inverseDependencies: new CountingSet(), + output: [ + { + type: 'js/module', + data: { + code: '__d(function() {/* code for foo */});', + map: [], + lineCount: 1, + }, + }, + ], + getSource: () => Buffer.from('foo-source'), +}; + +const barModule: Module<> = { + path: '/root/bar', + dependencies: new Map(), + inverseDependencies: new CountingSet(['/root/foo']), + output: [ + { + type: 'js/module', + data: { + code: '__d(function() {/* code for bar */});', + map: [], + lineCount: 1, + }, + }, + ], + getSource: () => Buffer.from('bar-source'), +}; + +const nonAsciiModule: Module<> = { + path: '/root/%30.бундл.Øಚ😁AA', + dependencies: new Map(), + inverseDependencies: new CountingSet(), + output: [ + { + type: 'js/module', + data: { + code: '__d(function() {/* code for ascii file with non ascii characters: %30.бундл.Øಚ😁AA */});', + map: [], + lineCount: 1, + }, + }, + ], + getSource: () => Buffer.from('bar-source'), +}; + +const transformOptions: TransformInputOptions = { + customTransformOptions: {}, + dev: true, + hot: true, + minify: true, + platform: 'web', + type: 'module', + unstable_transformProfile: 'default', +}; + +const graph: ReadOnlyGraph<> = { + entryPoints: new Set(['root/foo']), + dependencies: new Map([ + ['root/foo', fooModule], + ['root/bar', barModule], + ]), + transformOptions, +}; + +const options = { + clientUrl: new URL('http://localhost/root/foo/bundle.js'), + createModuleId: (s: string) => + s.includes('foo') ? (s.includes('bar') ? 2 : 1) : 0, + includeAsyncPaths: false, + projectRoot: '/root/bar', + serverRoot: '/root/bar', +}; + +test('should generate a simple hot reload bundle from a change', () => { + expect( + hmrJSBundle( + { + added: new Map([['root/foo', fooModule]]), + modified: new Map([['root/bar', barModule]]), + deleted: new Set(), + reset: false, + }, + graph, + options, + ), + ).toMatchInlineSnapshot(` + Object { + "added": Array [ + Object { + "module": Array [ + 1, + "__d(function() {/* code for foo */},1,[0],\\"../foo\\",{}); + //# sourceMappingURL=http://localhost/foo.map + //# sourceURL=http://localhost/foo.bundle + ", + ], + "sourceMappingURL": "http://localhost/foo.map", + "sourceURL": "http://localhost/foo.bundle", + }, + ], + "deleted": Array [], + "modified": Array [ + Object { + "module": Array [ + 0, + "__d(function() {/* code for bar */},0,[],\\"\\",{}); + //# sourceMappingURL=http://localhost/bar.map + //# sourceURL=http://localhost/bar.bundle + ", + ], + "sourceMappingURL": "http://localhost/bar.map", + "sourceURL": "http://localhost/bar.bundle", + }, + ], + } + `); +}); + +test('should turn non ascii filesystem characters into proper encoded urls for source url and source map url', () => { + expect( + hmrJSBundle( + { + added: new Map(), + modified: new Map([['root/%30.бундл.Øಚ😁AA', nonAsciiModule]]), + deleted: new Set(), + reset: false, + }, + graph, + options, + ), + ).toMatchInlineSnapshot(` + Object { + "added": Array [], + "deleted": Array [], + "modified": Array [ + Object { + "module": Array [ + 0, + "__d(function() {/* code for ascii file with non ascii characters: %30.бундл.Øಚ😁AA */},0,[],\\"../%30.бундл.Øಚ😁AA\\",{}); + //# sourceMappingURL=http://localhost/%2530.%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.map + //# sourceURL=http://localhost/%2530.%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.bundle + ", + ], + "sourceMappingURL": "http://localhost/%2530.%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.map", + "sourceURL": "http://localhost/%2530.%D0%B1%D1%83%D0%BD%D0%B4%D0%BB.bundle", + }, + ], + } + `); +}); diff --git a/packages/metro/src/DeltaBundler/Serializers/hmrJSBundle.js b/packages/metro/src/DeltaBundler/Serializers/hmrJSBundle.js index 90bbd18490..0f72027f71 100644 --- a/packages/metro/src/DeltaBundler/Serializers/hmrJSBundle.js +++ b/packages/metro/src/DeltaBundler/Serializers/hmrJSBundle.js @@ -11,7 +11,6 @@ 'use strict'; -import type {EntryPointURL} from '../../HmrServer'; import type {DeltaResult, Module, ReadOnlyGraph} from '../types'; import type {HmrModule} from 'metro-runtime/src/modules/types'; @@ -19,10 +18,9 @@ const {isJsModule, wrapModule} = require('./helpers/js'); const jscSafeUrl = require('jsc-safe-url'); const {addParamsToDefineCall} = require('metro-transform-plugins'); const path = require('path'); -const url = require('url'); type Options = $ReadOnly<{ - clientUrl: EntryPointURL, + clientUrl: URL, createModuleId: string => number, includeAsyncPaths: boolean, projectRoot: string, @@ -39,28 +37,30 @@ function generateModules( for (const module of sourceModules) { if (isJsModule(module)) { - // Construct a bundle URL for this specific module only - const getURL = (extension: 'bundle' | 'map') => { - const moduleUrl = url.parse(url.format(options.clientUrl), true); - // the legacy url object is parsed with both "search" and "query" fields. - // for the "query" field to be used when formatting the object bach to string, the "search" field must be empty. - // https://nodejs.org/api/url.html#urlformaturlobject:~:text=If%20the%20urlObject.search%20property%20is%20undefined - moduleUrl.search = ''; - moduleUrl.pathname = path.relative( - options.serverRoot ?? options.projectRoot, - path.join( - path.dirname(module.path), - path.basename(module.path, path.extname(module.path)) + - '.' + - extension, + const getPathname = (extension: 'bundle' | 'map') => { + // encoding a file path as a URL path so it could be dencoded back to a file path upon receiving + return encodeURI( + path.relative( + options.serverRoot ?? options.projectRoot, + path.join( + path.dirname(module.path), + path.basename(module.path, path.extname(module.path)) + + '.' + + extension, + ), ), ); - delete moduleUrl.query.excludeSource; - return url.format(moduleUrl); }; - const sourceMappingURL = getURL('map'); - const sourceURL = jscSafeUrl.toJscSafeUrl(getURL('bundle')); + const clientUrl = new URL(options.clientUrl); + clientUrl.searchParams.delete('excludeSource'); + + clientUrl.pathname = getPathname('map'); + const sourceMappingURL = clientUrl.toString(); + + clientUrl.pathname = getPathname('bundle'); + const sourceURL = jscSafeUrl.toJscSafeUrl(clientUrl.toString()); + const code = prepareModule(module, graph, options) + `\n//# sourceMappingURL=${sourceMappingURL}\n` + @@ -84,7 +84,7 @@ function prepareModule( ): string { const code = wrapModule(module, { ...options, - sourceUrl: url.format(options.clientUrl), + sourceUrl: options.clientUrl.toString(), dev: true, }); diff --git a/packages/metro/src/HmrServer.js b/packages/metro/src/HmrServer.js index 603a633990..c56c335be9 100644 --- a/packages/metro/src/HmrServer.js +++ b/packages/metro/src/HmrServer.js @@ -19,7 +19,6 @@ import type { HmrMessage, HmrUpdateMessage, } from 'metro-runtime/src/modules/types'; -import type {UrlWithParsedQuery} from 'url'; const hmrJSBundle = require('./DeltaBundler/Serializers/hmrJSBundle'); const GraphNotFoundError = require('./IncrementalBundler/GraphNotFoundError'); @@ -34,9 +33,6 @@ const { Logger: {createActionStartEntry, createActionEndEntry, log}, } = require('metro-core'); const nullthrows = require('nullthrows'); -const url = require('url'); - -export type EntryPointURL = UrlWithParsedQuery; export type Client = { optedIntoHMR: boolean, @@ -46,7 +42,7 @@ export type Client = { type ClientGroup = { +clients: Set, - clientUrl: EntryPointURL, + clientUrl: URL, revisionId: RevisionId, +unlisten: () => void, +graphOptions: GraphOptions, @@ -100,7 +96,7 @@ class HmrServer { sendFn: (data: string) => void, ): Promise { requestUrl = this._config.server.rewriteRequestUrl(requestUrl); - const clientUrl = nullthrows(url.parse(requestUrl, true)); + const clientUrl = new URL(requestUrl); const {bundleType: _bundleType, ...options} = parseBundleOptionsFromBundleRequestUrl( requestUrl, @@ -156,29 +152,18 @@ class HmrServer { } else { // Prepare the clientUrl to be used as sourceUrl in HMR updates. clientUrl.protocol = 'http'; - const { - dev, - minify, - runModule, - bundleEntry: _bundleEntry, - ...query - } = clientUrl.query || {}; - clientUrl.query = { - ...query, - dev: dev || 'true', - minify: minify || 'false', - modulesOnly: 'true', - runModule: runModule || 'false', - shallow: 'true', - }; - // the legacy url object is parsed with both "search" and "query" fields. - // for the "query" field to be used when formatting the object bach to string, the "search" field must be empty. - // https://nodejs.org/api/url.html#urlformaturlobject:~:text=If%20the%20urlObject.search%20property%20is%20undefined - clientUrl.search = ''; + + const clientQuery = clientUrl.searchParams; + clientQuery.delete('bundleEntry'); + clientQuery.set('dev', clientQuery.get('dev') || 'true'); + clientQuery.set('minify', clientQuery.get('minify') || 'false'); + clientQuery.set('modulesOnly', 'true'); + clientQuery.set('runModule', clientQuery.get('runModule') || 'false'); + clientQuery.set('shallow', 'true'); clientGroup = { clients: new Set([client]), - clientUrl, + clientUrl: new URL(clientUrl), revisionId: id, graphOptions, unlisten: (): void => unlisten(), @@ -370,7 +355,7 @@ class HmrServer { logger?.point('serialize_start'); const hmrUpdate = hmrJSBundle(delta, revision.graph, { - clientUrl: group.clientUrl, + clientUrl: new URL(group.clientUrl), createModuleId: this._createModuleId, includeAsyncPaths: group.graphOptions.lazy, projectRoot: this._config.projectRoot, diff --git a/packages/metro/src/Server.js b/packages/metro/src/Server.js index d773209b54..f995bd1ff8 100644 --- a/packages/metro/src/Server.js +++ b/packages/metro/src/Server.js @@ -86,7 +86,6 @@ const nullthrows = require('nullthrows'); const path = require('path'); const {performance} = require('perf_hooks'); const querystring = require('querystring'); -const url = require('url'); const noopLogger: RootPerfLogger = { start: () => {}, @@ -510,20 +509,29 @@ class Server { req: IncomingMessage, res: ServerResponse, ): Promise { - const urlObj = url.parse(decodeURI(req.url), true); + debug('Processing single asset request: %s', req.url); + const urlObj = new URL( + req.url, + 'mock-protocol://mock-host' /* URL expects a protocol and a host to be present to parse a URL */, + ); + const formattedUrl = urlObj.toString(); + if (req.url !== formattedUrl) { + debug('Formatted as: %s', formattedUrl); + } let [, assetPath] = - (urlObj && - urlObj.pathname && - urlObj.pathname.match(/^\/assets\/(.+)$/)) || - []; - - if (!assetPath && urlObj && urlObj.query && urlObj.query.unstable_path) { + decodeURI(urlObj.pathname).match(/^\/assets\/(.+)$/) || []; + if (!assetPath && urlObj.searchParams.get('unstable_path')) { const [, actualPath, secondaryQuery] = nullthrows( - urlObj.query.unstable_path.match(/^([^?]*)\??(.*)$/), + (urlObj.searchParams.get('unstable_path') || '').match( + /^([^?]*)\??(.*)$/, + ), ); if (secondaryQuery) { - // $FlowFixMe[unsafe-object-assign] - Object.assign(urlObj.query, querystring.parse(secondaryQuery)); + Object.entries(querystring.parse(secondaryQuery)).forEach( + ([key, value]) => { + urlObj.searchParams.set(key, value); + }, + ); } assetPath = actualPath; } @@ -544,7 +552,7 @@ class Server { assetPath, this._config.projectRoot, this._config.watchFolders, - urlObj.query.platform, + urlObj.searchParams.get('platform'), this._config.resolver.assetExts, ); // Tell clients to cache this for 1 year. @@ -598,19 +606,21 @@ class Server { next: (?Error) => void, ) { const originalUrl = req.url; + debug('Handling request: %s', originalUrl); req.url = this._rewriteAndNormalizeUrl(req.url); - const urlObj = url.parse(decodeURI(req.url), true); + if (req.url !== originalUrl) { + debug('Rewritten to: %s', req.url); + } const {host} = req.headers; - debug( - `Handling request: ${host ? 'http://' + host : ''}${req.url}` + - (originalUrl !== req.url ? ` (rewritten from ${originalUrl})` : ''), - ); - const formattedUrl = url.format({ - ...urlObj, - host, - protocol: 'http', - }); + const urlObj = new URL(req.url, 'http://' + host); + const formattedUrl = urlObj.toString(); + if (req.url !== formattedUrl) { + debug('Formatted as: %s', formattedUrl); + } + const pathname = urlObj.pathname || ''; + const decodedPathname = decodeURI(urlObj.pathname || ''); + const buildNumber = this.getNewBuildNumber(); if (pathname.endsWith('.bundle')) { const options = this._parseOptions(formattedUrl); @@ -623,7 +633,7 @@ class Server { }); if (this._serverOptions && this._serverOptions.onBundleBuilt) { - this._serverOptions.onBundleBuilt(pathname); + this._serverOptions.onBundleBuilt(decodedPathname); } } else if (pathname.endsWith('.map')) { // Chrome dev tools may need to access the source maps. @@ -655,8 +665,10 @@ class Server { let handled = false; for (const [pathnamePrefix, normalizedRootDir] of this ._sourceRequestRoutingMap) { - if (pathname.startsWith(pathnamePrefix)) { - const relativePathname = pathname.substr(pathnamePrefix.length); + if (decodedPathname.startsWith(pathnamePrefix)) { + const relativePathname = decodedPathname.substr( + pathnamePrefix.length, + ); await this._processSourceRequest( relativePathname, normalizedRootDir, @@ -673,13 +685,13 @@ class Server { } async _processSourceRequest( - relativePathname: string, + relativeFilePathname: string, rootDir: string, res: ServerResponse, ): Promise { if ( !this._allowedSuffixesForSourceRequests.some(suffix => - relativePathname.endsWith(suffix), + relativeFilePathname.endsWith(suffix), ) ) { res.writeHead(404); @@ -687,7 +699,7 @@ class Server { return; } const depGraph = await this._bundler.getBundler().getDependencyGraph(); - const filePath = path.join(rootDir, relativePathname); + const filePath = path.join(rootDir, relativeFilePathname); try { await depGraph.getOrComputeSha1(filePath); } catch { @@ -695,7 +707,7 @@ class Server { res.end(); return; } - const mimeType = mime.lookup(path.basename(relativePathname)); + const mimeType = mime.lookup(path.basename(relativeFilePathname)); res.setHeader('Content-Type', mimeType); const stream = fs.createReadStream(filePath); stream.pipe(res); diff --git a/packages/metro/src/index.flow.js b/packages/metro/src/index.flow.js index 027e964db2..0d5a9a0a8f 100644 --- a/packages/metro/src/index.flow.js +++ b/packages/metro/src/index.flow.js @@ -52,7 +52,6 @@ const { const {Terminal} = require('metro-core'); const net = require('net'); const nullthrows = require('nullthrows'); -const {parse} = require('url'); type MetroMiddleWare = { attachHmrServer: (httpServer: HttpServer | HttpsServer) => void, @@ -237,7 +236,7 @@ const createConnectMiddleware = async function ( ), }); httpServer.on('upgrade', (request, socket, head) => { - const {pathname} = parse(request.url); + const {pathname} = new URL(request.url, 'resolve://'); if (pathname === '/hot') { wss.handleUpgrade(request, socket, head, ws => { wss.emit('connection', ws, request); @@ -351,7 +350,7 @@ exports.runServer = async ( }; httpServer.on('upgrade', (request, socket, head) => { - const {pathname} = parse(request.url); + const {pathname} = new URL(request.url, 'resolve://'); if (pathname != null && websocketEndpoints[pathname]) { websocketEndpoints[pathname].handleUpgrade( request, diff --git a/packages/metro/src/lib/__tests__/parseCustomResolverOptions-test.js b/packages/metro/src/lib/__tests__/parseCustomResolverOptions-test.js index b0b65ae827..7849ea6ffa 100644 --- a/packages/metro/src/lib/__tests__/parseCustomResolverOptions-test.js +++ b/packages/metro/src/lib/__tests__/parseCustomResolverOptions-test.js @@ -12,13 +12,12 @@ 'use strict'; const parseCustomResolverOptions = require('../parseCustomResolverOptions'); -const url = require('url'); test('should parse some custom options from a http url', () => { const myUrl = 'http://localhost/my/bundle.bundle?dev=true&resolver.foo=value&resolver.bar=other'; - expect(parseCustomResolverOptions(url.parse(myUrl, true))).toEqual({ + expect(parseCustomResolverOptions(new URL(myUrl).searchParams)).toEqual({ foo: 'value', bar: 'other', }); @@ -27,7 +26,7 @@ test('should parse some custom options from a http url', () => { test('should parse some custom options from a websocket url', () => { const myUrl = 'ws://localhost/hot?resolver.foo=value&resolver.bar=other'; - expect(parseCustomResolverOptions(url.parse(myUrl, true))).toEqual({ + expect(parseCustomResolverOptions(new URL(myUrl).searchParams)).toEqual({ foo: 'value', bar: 'other', }); @@ -36,5 +35,5 @@ test('should parse some custom options from a websocket url', () => { test('should return an empty object if there are no custom params', () => { const myUrl = 'http://localhost/my/bundle.bundle?dev=true'; - expect(parseCustomResolverOptions(url.parse(myUrl, true))).toEqual({}); + expect(parseCustomResolverOptions(new URL(myUrl).searchParams)).toEqual({}); }); diff --git a/packages/metro/src/lib/__tests__/parseCustomTransformOptions-test.js b/packages/metro/src/lib/__tests__/parseCustomTransformOptions-test.js index 056ee9850c..9b766a3d0f 100644 --- a/packages/metro/src/lib/__tests__/parseCustomTransformOptions-test.js +++ b/packages/metro/src/lib/__tests__/parseCustomTransformOptions-test.js @@ -12,13 +12,12 @@ 'use strict'; const parseCustomTransformOptions = require('../parseCustomTransformOptions'); -const url = require('url'); test('should parse some custom options from a http url', () => { const myUrl = 'http://localhost/my/bundle.bundle?dev=true&transform.foo=value&transform.bar=other'; - expect(parseCustomTransformOptions(url.parse(myUrl, true))).toEqual({ + expect(parseCustomTransformOptions(new URL(myUrl).searchParams)).toEqual({ foo: 'value', bar: 'other', }); @@ -27,7 +26,7 @@ test('should parse some custom options from a http url', () => { test('should parse some custom options from a websocket url', () => { const myUrl = 'ws://localhost/hot?transform.foo=value&transform.bar=other'; - expect(parseCustomTransformOptions(url.parse(myUrl, true))).toEqual({ + expect(parseCustomTransformOptions(new URL(myUrl).searchParams)).toEqual({ foo: 'value', bar: 'other', }); @@ -36,5 +35,5 @@ test('should parse some custom options from a websocket url', () => { test('should return an empty object if there are no custom params', () => { const myUrl = 'http://localhost/my/bundle.bundle?dev=true'; - expect(parseCustomTransformOptions(url.parse(myUrl, true))).toEqual({}); + expect(parseCustomTransformOptions(new URL(myUrl).searchParams)).toEqual({}); }); diff --git a/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js b/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js index b7ffbf6a5d..c08c5baaad 100644 --- a/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js +++ b/packages/metro/src/lib/parseBundleOptionsFromBundleRequestUrl.js @@ -19,24 +19,29 @@ import {SourcePathsMode} from '../shared/types'; const parsePlatformFilePath = require('../node-haste/lib/parsePlatformFilePath'); const parseCustomResolverOptions = require('./parseCustomResolverOptions'); const parseCustomTransformOptions = require('./parseCustomTransformOptions'); +const debug = require('debug')('Metro:Server'); const jscSafeUrl = require('jsc-safe-url'); -const nullthrows = require('nullthrows'); const path = require('path'); -const url = require('url'); -const getBoolean = ( - query: $ReadOnly<{[opt: string]: string}>, +const TRUE_STRINGS = new Set(['true', '1']); + +// This is a bit weird but this is the recommended way of getting around "URL" demanding to have a valid protocol +// for when handling relative URLs: https://nodejs.org/docs/latest-v24.x/api/url.html#urlresolvefrom-to +const RESOLVE_BASE_URL = 'resolve://'; + +const getBoolQueryParam = ( + searchParams: URLSearchParams, opt: string, defaultValue: boolean, ) => - query[opt] == null - ? defaultValue - : query[opt] === 'true' || query[opt] === '1'; + searchParams.has(opt) + ? TRUE_STRINGS.has(searchParams.get(opt) || '') + : defaultValue; const getBundleType = (bundleType: string): 'map' | 'bundle' => bundleType === 'map' ? bundleType : 'bundle'; -const getTransformProfile = (transformProfile: string): TransformProfile => +const getTransformProfile = (transformProfile: ?string): TransformProfile => transformProfile === 'hermes-stable' || transformProfile === 'hermes-canary' ? transformProfile : 'default'; @@ -49,61 +54,91 @@ module.exports = function parseBundleOptionsFromBundleRequestUrl( // Retained for backwards compatibility, unused in Metro, to be removed. bundleType: string, } { - const relativeProtocolUrl = rawNonJscSafeUrlEncodedUrl.startsWith('//'); - const parsedURL = nullthrows( - url.parse( - (relativeProtocolUrl ? 'temp-protocol:' : '') + - rawNonJscSafeUrlEncodedUrl, - true, - ), - ); // `true` to parse the query param as an object. + const { + protocol: _tempProtocol, + host, + searchParams, + pathname: requestPathname, + search, + hash, + } = new URL(rawNonJscSafeUrlEncodedUrl, RESOLVE_BASE_URL /* baseURL */); + + const isRelativeProtocol = rawNonJscSafeUrlEncodedUrl.startsWith('//'); + const isNoProtocol = + !isRelativeProtocol && _tempProtocol + '//' === RESOLVE_BASE_URL; + + // TODO: next diff (D79809398) will remove the support for "isNoProtocol" to make the requested URL more expected (either "//" or "http://") + const protocol = isNoProtocol // e.g. "./foo/bar.js" or "foo/bar.js" both converted to paths relative to root + ? '' + : isRelativeProtocol // e.g. "//localhost:8081/foo/bar.js?platform=ios" + ? '//' + : _tempProtocol + '//'; // e.g. "http://localhost:8081/foo/bar.js?platform=ios" - const query = nullthrows(parsedURL.query); + const sourceUrl = jscSafeUrl.toJscSafeUrl( + protocol + host + requestPathname + search + hash, + ); - const pathname = query.bundleEntry || (parsedURL?.pathname ?? ''); - const decodedPathname = decodeURIComponent(pathname); + const pathname = searchParams.get('bundleEntry') || requestPathname || ''; const platform = - query.platform || parsePlatformFilePath(pathname, platforms).platform; + searchParams.get('platform') || + parsePlatformFilePath(pathname, platforms).platform; + const bundleType = getBundleType(path.extname(pathname).substr(1)); + // The Chrome Debugger loads bundles via Blob urls, whose + // protocol is blob:http. This breaks loading source maps through + // protocol-relative URLs, which is why we must force the HTTP protocol + // when loading the bundle for either Android or iOS. + // TODO(T167298674): Remove when remote debugging is not needed in React Native + const sourceMapUrlProtocol = + platform != null && platform.match(/^(android|ios|vr|windows|macos)$/) + ? 'http://' + : '//'; + const {pathname: sourceMapPathname} = new URL( + pathname.replace(/\.(bundle|delta)$/, '.map'), + RESOLVE_BASE_URL /* baseURL */, + ); + const sourceMapUrl = + sourceMapUrlProtocol + host + sourceMapPathname + search + hash; + + // decoding URL into a file path + const entryFile = decodeURI(pathname) + .replace(/^(?:\.?\/)?/, './') + .replace(/\.[^/.]+$/, ''); + + debug( + 'Bundle options parsed from rawNonJscSafeUrlEncodedUrl: %s:\nsourceUrl: %s\nsourceMapUrl: %s\nentryFile: %s', + rawNonJscSafeUrlEncodedUrl, + sourceUrl, + sourceMapUrl, + entryFile, + ); + return { bundleType, - customResolverOptions: parseCustomResolverOptions(parsedURL), - customTransformOptions: parseCustomTransformOptions(parsedURL), - dev: getBoolean(query, 'dev', true), + customResolverOptions: parseCustomResolverOptions(searchParams), + customTransformOptions: parseCustomTransformOptions(searchParams), + dev: getBoolQueryParam(searchParams, 'dev', true), // absolute and relative paths are converted to paths relative to root - entryFile: decodedPathname - .replace(/^(?:\.?\/)?/, './') - .replace(/\.[^/.]+$/, ''), - excludeSource: getBoolean(query, 'excludeSource', false), + entryFile, + excludeSource: getBoolQueryParam(searchParams, 'excludeSource', false), hot: true, - inlineSourceMap: getBoolean(query, 'inlineSourceMap', false), - lazy: getBoolean(query, 'lazy', false), - minify: getBoolean(query, 'minify', false), - modulesOnly: getBoolean(query, 'modulesOnly', false), + inlineSourceMap: getBoolQueryParam(searchParams, 'inlineSourceMap', false), + lazy: getBoolQueryParam(searchParams, 'lazy', false), + minify: getBoolQueryParam(searchParams, 'minify', false), + modulesOnly: getBoolQueryParam(searchParams, 'modulesOnly', false), onProgress: null, platform, - runModule: getBoolean(query, 'runModule', true), - shallow: getBoolean(query, 'shallow', false), - sourceMapUrl: url.format({ - ...parsedURL, - // The Chrome Debugger loads bundles via Blob urls, whose - // protocol is blob:http. This breaks loading source maps through - // protocol-relative URLs, which is why we must force the HTTP protocol - // when loading the bundle for either Android or iOS. - // TODO(T167298674): Remove when remote debugging is not needed in React Native - protocol: - platform != null && platform.match(/^(android|ios|vr|windows|macos)$/) - ? 'http' - : '', - pathname: pathname.replace(/\.(bundle|delta)$/, '.map'), - }), + runModule: getBoolQueryParam(searchParams, 'runModule', true), + shallow: getBoolQueryParam(searchParams, 'shallow', false), + sourceMapUrl, sourcePaths: - SourcePathsMode.cast(query.sourcePaths) ?? SourcePathsMode.Absolute, - sourceUrl: jscSafeUrl.toJscSafeUrl(rawNonJscSafeUrlEncodedUrl), + SourcePathsMode.cast(searchParams.get('sourcePaths')) ?? + SourcePathsMode.Absolute, + sourceUrl, unstable_transformProfile: getTransformProfile( - query.unstable_transformProfile, + searchParams.get('unstable_transformProfile'), ), }; }; diff --git a/packages/metro/src/lib/parseCustomResolverOptions.js b/packages/metro/src/lib/parseCustomResolverOptions.js index 1012c48692..956581f8ec 100644 --- a/packages/metro/src/lib/parseCustomResolverOptions.js +++ b/packages/metro/src/lib/parseCustomResolverOptions.js @@ -13,24 +13,20 @@ import type {CustomResolverOptions} from '../../../metro-resolver/src/types'; -const nullthrows = require('nullthrows'); - const PREFIX = 'resolver.'; -module.exports = function parseCustomResolverOptions(urlObj: { - +query?: {[string]: string, ...}, - ... -}): CustomResolverOptions { +module.exports = function parseCustomResolverOptions( + searchParams: URLSearchParams, +): CustomResolverOptions { const customResolverOptions: { __proto__: null, [string]: mixed, ... } = Object.create(null); - const query = nullthrows(urlObj.query); - Object.keys(query).forEach((key: string) => { + searchParams.forEach((value: string, key: string) => { if (key.startsWith(PREFIX)) { - customResolverOptions[key.substr(PREFIX.length)] = query[key]; + customResolverOptions[key.substr(PREFIX.length)] = value; } }); diff --git a/packages/metro/src/lib/parseCustomTransformOptions.js b/packages/metro/src/lib/parseCustomTransformOptions.js index b327ac2190..45ae78c743 100644 --- a/packages/metro/src/lib/parseCustomTransformOptions.js +++ b/packages/metro/src/lib/parseCustomTransformOptions.js @@ -13,21 +13,20 @@ import type {CustomTransformOptions} from 'metro-transform-worker'; -const nullthrows = require('nullthrows'); - const PREFIX = 'transform.'; -module.exports = function parseCustomTransformOptions(urlObj: { - +query?: {[string]: string, ...}, - ... -}): CustomTransformOptions { - const customTransformOptions = Object.create(null); - const query = nullthrows(urlObj.query); +module.exports = function parseCustomTransformOptions( + searchParams: URLSearchParams, +): CustomTransformOptions { + const customTransformOptions: { + __proto__: null, + [string]: mixed, + ... + } = Object.create(null); - Object.keys(query).forEach((key: string) => { + searchParams.forEach((value: string, key: string) => { if (key.startsWith(PREFIX)) { - // $FlowFixMe[prop-missing] - customTransformOptions[key.substr(PREFIX.length)] = query[key]; + customTransformOptions[key.substr(PREFIX.length)] = value; } }); diff --git a/scripts/eslint/base.js b/scripts/eslint/base.js index eee3f394a4..a7646d4223 100644 --- a/scripts/eslint/base.js +++ b/scripts/eslint/base.js @@ -42,6 +42,23 @@ module.exports = { quotes: 'off', 'sort-keys': 'off', + 'no-restricted-imports': [ + 'error', + { + name: 'url', + message: + 'Deprecated. Please use URL instead. https://nodejs.org/docs/latest/api/url.html#legacy-url-api', + }, + ], + 'no-restricted-modules': [ + 'error', + { + name: 'url', + message: + 'Deprecated. Please use URL instead. https://nodejs.org/docs/latest/api/url.html#legacy-url-api', + }, + ], + // TODO: This was added after migrating from `eslint-plugin-prettier` to // `eslint-config-prettier`. The former used to disable this rule, so this // was added to avoid introducing lint errors during the migration. Either