Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)});`;

Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
},
],
}
`);
});
44 changes: 22 additions & 22 deletions packages/metro/src/DeltaBundler/Serializers/hmrJSBundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,16 @@

'use strict';

import type {EntryPointURL} from '../../HmrServer';
import type {DeltaResult, Module, ReadOnlyGraph} from '../types';
import type {HmrModule} from 'metro-runtime/src/modules/types';

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,
Expand All @@ -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` +
Expand All @@ -84,7 +84,7 @@ function prepareModule(
): string {
const code = wrapModule(module, {
...options,
sourceUrl: url.format(options.clientUrl),
sourceUrl: options.clientUrl.toString(),
dev: true,
});

Expand Down
Loading
Loading