Skip to content
Open
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 @@ -102,6 +102,25 @@ describe('getOrderedDependencyPaths', () => {
]);
});

test('Should exclude virtual modules, which have no file', async () => {
const jsOutput = [
{
type: 'js/module',
data: {code: '//', lineCount: 1, map: [], functionMap: null},
},
];
const graph = {
dependencies: new Map([
[1, {path: '/tmp/1.js', output: jsOutput}],
[2, {path: 'virtual:/tmp/1.generated.js', output: jsOutput}],
]),
};

expect(
await getAllFiles([], graph, {processModuleFilter: () => true}),
).toEqual(['/tmp/1.js']);
});

test('Should add assets data dependencies correctly', async () => {
const graph = {
dependencies: new Map([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,33 @@ Object {
}
`);
});

test('gives a virtual module anchored at a real file URLs distinct from that file', () => {
const virtualFooModule: Module<> = {
...fooModule,
path: 'virtual:/root/foo',
dependencies: new Map(),
};
const {added} = hmrJSBundle(
{
added: new Map([
['root/foo', fooModule],
['virtual:/root/foo', virtualFooModule],
]),
modified: new Map(),
deleted: new Set(),
reset: false,
},
graph,
options,
);
expect(
added.map(({sourceURL, sourceMappingURL}) => [sourceURL, sourceMappingURL]),
).toEqual([
['http://localhost/foo.bundle', 'http://localhost/foo.map'],
[
'http://localhost/foo.bundle//&virtual=true',
'http://localhost/foo.map?virtual=true',
],
]);
});
8 changes: 7 additions & 1 deletion packages/metro/src/DeltaBundler/Serializers/getAllFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import type {Module, ReadOnlyGraph} from '../types';

import {getAssetFiles} from '../../Assets';
import {isVirtualModulePath} from '../../lib/modulePath';
import {getJsOutput, isJsModule} from './helpers/js';

type Options = {
Expand All @@ -36,7 +37,12 @@ export default async function getAllFiles(
}

for (const module of modules.values()) {
if (!isJsModule(module) || !processModuleFilter(module)) {
if (
!isJsModule(module) ||
!processModuleFilter(module) ||
// A virtual module has no file.
isVirtualModulePath(module.path)
) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,49 @@ describe('wrapModule()', () => {
);
});

test('names a virtual module by its virtual path, in its own namespace', () => {
expect(
raw(
wrapModule(
{...myModule, path: 'virtual:' + p('/root/foo.js')},
{
createModuleId: createModuleIdFactory(),
dev: true,
includeAsyncPaths: false,
projectRoot: p('/root'),
serverRoot: p('/root'),
sourceUrl: null,
},
),
),
).toMatchInlineSnapshot(
`__d(function() { console.log("foo") },0,[1,2],"virtual:foo.js");`,
);
});

test('marks the path of an async virtual dependency as virtual', () => {
const dep = nullthrows(myModule.dependencies.get('bar'));
myModule.dependencies.set('bar', {
...dep,
absolutePath: 'virtual:' + p('/root/bar.js'),
data: {...dep.data, data: {...dep.data.data, asyncType: 'async'}},
});
expect(
raw(
wrapModule(myModule, {
createModuleId: createModuleIdFactory(),
dev: false,
includeAsyncPaths: true,
projectRoot: p('/root'),
serverRoot: p('/root'),
sourceUrl: 'http://localhost/Main.bundle?param1=true',
}),
),
).toMatchInlineSnapshot(
`__d(function() { console.log("foo") },0,{"0":1,"1":2,"paths":{"1":"/bar.bundle?param1=true&modulesOnly=true&runModule=false&virtual=true"}});`,
);
});

test('async dependency paths respect serverRoot', () => {
const dep = nullthrows(myModule.dependencies.get('bar'));
myModule.dependencies.set('bar', {
Expand Down
18 changes: 14 additions & 4 deletions packages/metro/src/DeltaBundler/Serializers/helpers/js.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import type {
import type {JsOutput} from 'metro-transform-worker';

import {isResolvedDependency} from '../../../lib/isResolvedDependency';
import {
VIRTUAL_MODULE_URL_PARAM,
getModuleDisplayPath,
getModuleSourcePath,
isVirtualModulePath,
} from '../../../lib/modulePath';
import {normalizePathSeparatorsToPosix} from '../../../lib/pathUtils';
import invariant from 'invariant';
import * as jscSafeUrl from 'jsc-safe-url';
Expand Down Expand Up @@ -78,9 +84,7 @@ export function wrapModule(module: Module<>, options: Options): string {
function getModuleVerboseName(module: Module<>, options: Options): string {
// The relative path of the module, to make debugging easier. This is mapped
// to `module.verboseName` in `require.js`.
return normalizePathSeparatorsToPosix(
path.relative(options.projectRoot, module.path),
);
return getModuleDisplayPath(module.path, options.projectRoot);
}

// Default value stored in the `__d(...)` `paths` object for an async
Expand All @@ -104,8 +108,14 @@ function getDefaultAsyncDependencyPath(
const {searchParams} = new URL(jscSafeUrl.toNormalUrl(options.sourceUrl));
searchParams.set('modulesOnly', 'true');
searchParams.set('runModule', 'false');
if (isVirtualModulePath(dependency.absolutePath)) {
searchParams.set(VIRTUAL_MODULE_URL_PARAM, 'true');
}

const bundlePath = path.relative(options.serverRoot, dependency.absolutePath);
const bundlePath = path.relative(
options.serverRoot,
getModuleSourcePath(dependency.absolutePath),
);
return (
'/' +
normalizePathSeparatorsToPosix(
Expand Down
13 changes: 11 additions & 2 deletions packages/metro/src/DeltaBundler/Serializers/hmrJSBundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import type {DeltaResult, Module, ReadOnlyGraph} from '../types';
import type {HmrModule} from 'metro-runtime/src/modules/types';

import {
VIRTUAL_MODULE_URL_PARAM,
getModuleSourcePath,
isVirtualModulePath,
} from '../../lib/modulePath';
import {isJsModule, wrapModule} from './helpers/js';
import debugModule from 'debug';
import * as jscSafeUrl from 'jsc-safe-url';
Expand All @@ -38,14 +43,15 @@ function generateModules(

for (const module of sourceModules) {
if (isJsModule(module)) {
const sourcePath = getModuleSourcePath(module.path);
const getPathname = (extension: 'bundle' | 'map') => {
return (
path
.relative(
options.serverRoot ?? options.projectRoot,
path.join(
path.dirname(module.path),
path.basename(module.path, path.extname(module.path)) +
path.dirname(sourcePath),
path.basename(sourcePath, path.extname(sourcePath)) +
'.' +
extension,
),
Expand All @@ -59,6 +65,9 @@ function generateModules(

const clientUrl = new URL(options.clientUrl);
clientUrl.searchParams.delete('excludeSource');
if (isVirtualModulePath(module.path)) {
clientUrl.searchParams.set(VIRTUAL_MODULE_URL_PARAM, 'true');
}

clientUrl.pathname = getPathname('map');
const sourceMappingURL = clientUrl.toString();
Expand Down
19 changes: 16 additions & 3 deletions packages/metro/src/Server.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ import {calculateBundleProgressRatio} from './lib/bundleProgressUtils';
import bundleToString from './lib/bundleToString';
import formatBundlingError from './lib/formatBundlingError';
import getGraphId from './lib/getGraphId';
import {
VIRTUAL_MODULE_URL_PARAM,
getModuleSourcePath,
isVirtualModulePath,
isVirtualModuleUrl,
} from './lib/modulePath';
import parseBundleOptionsFromBundleRequestUrl from './lib/parseBundleOptionsFromBundleRequestUrl';
import parseJsonBody from './lib/parseJsonBody';
import RootUrlMap from './lib/RootUrlMap';
Expand Down Expand Up @@ -705,7 +711,8 @@ export default class Server {
} else {
const sourceFilePath =
this._rootUrlMap.filePathOfUrlDecodedPathname(filePathname);
if (sourceFilePath != null) {
// Virtual modules aren't served by URL yet.
if (sourceFilePath != null && !isVirtualModuleUrl(urlObj.searchParams)) {
await this._processSourceRequest(sourceFilePath, res);
} else {
next();
Expand Down Expand Up @@ -1750,8 +1757,14 @@ export default class Server {
// eslint-disable-next-line consistent-return
_getModuleSourceUrl(module: Module<>, mode: SourcePathsMode): string {
switch (mode) {
case SourcePathsMode.ServerUrl:
return this._rootUrlMap.urlPathnameOfFilePath(module.path);
case SourcePathsMode.ServerUrl: {
const pathname = this._rootUrlMap.urlPathnameOfFilePath(
getModuleSourcePath(module.path),
);
return isVirtualModulePath(module.path)
? `${pathname}?${VIRTUAL_MODULE_URL_PARAM}=true`
: pathname;
}
case SourcePathsMode.Absolute:
return module.path;
}
Expand Down
34 changes: 34 additions & 0 deletions packages/metro/src/Server/__tests__/Server-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {posixToSystemPath as p} from 'metro-resolver/private/__tests__/utils';
import MockRequest from 'mock-req';
// $FlowFixMe[untyped-import]
import MockResponse from 'mock-res';
import nullthrows from 'nullthrows';

const {
getDefaultConfig: {getDefaultValues},
Expand Down Expand Up @@ -802,6 +803,32 @@ describe('processRequest', () => {
});
});

test('marks the url-server source path of a virtual module', async () => {
// $FlowFixMe[prop-missing] Missing from the flow-typed Jest definitions.
const buildGraphImpl = buildGraph.getMockImplementation();
buildGraph.mockImplementationOnce(async (...args) => {
const graph = await buildGraphImpl(...args);
// A virtual module at the path of a real one.
dependencies.set('virtual:' + p('/root/foo.js'), {
...nullthrows(dependencies.get(p('/root/foo.js'))),
path: 'virtual:' + p('/root/foo.js'),
getSource: () => Buffer.from('code-virtual'),
});
return graph;
});

const response = await makeRequest('mybundle.map?sourcePaths=url-server');

expect(
response._getJSON().sections.map(section => section.map.sources),
).toEqual([
['/require-js'],
['/[metro-project]/mybundle.js'],
['/[metro-project]/foo.js'],
['/[metro-project]/foo.js?virtual=true'],
]);
});

test('passes in the unstable_transformProfile param', async () => {
await makeRequest('index.bundle?unstable_transformProfile=hermes-stable');

Expand Down Expand Up @@ -1097,6 +1124,13 @@ describe('processRequest', () => {

expect(response.getHeader('content-type')).toBe('image/png');
});

test('does not serve a file for a virtual module at its path', async () => {
// The request is passed on to the next handler, with no error.
await expect(
makeRequest('/[metro-project]/foo.js?virtual=true'),
).rejects.toBeUndefined();
});
});

describe('build(options)', () => {
Expand Down
51 changes: 51 additions & 0 deletions packages/metro/src/lib/__tests__/modulePath-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* 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 {
deriveVirtualModulePath,
getModuleDisplayPath,
getModuleSourcePath,
isVirtualModulePath,
isVirtualModuleUrl,
} from '../modulePath';

test('a virtual module path is the virtual path in the virtual namespace', () => {
const modulePath = deriveVirtualModulePath('/root/App.generated.tsx');
expect(modulePath).toBe('virtual:/root/App.generated.tsx');
expect(isVirtualModulePath(modulePath)).toBe(true);
expect(getModuleSourcePath(modulePath)).toBe('/root/App.generated.tsx');
});

test('real module paths pass through unchanged', () => {
expect(isVirtualModulePath('/root/App.tsx')).toBe(false);
expect(getModuleSourcePath('/root/App.tsx')).toBe('/root/App.tsx');
expect(getModuleDisplayPath('/root/src/App.tsx', '/root')).toBe(
'src/App.tsx',
);
});

test('a virtual module is displayed relative to the root, in its namespace', () => {
expect(
getModuleDisplayPath('virtual:/root/src/App.generated.tsx', '/root'),
).toBe('virtual:src/App.generated.tsx');
});

test.each([
['?virtual=true', true],
['?platform=ios&virtual=true', true],
['?virtual', true],
['?platform=ios', false],
['', false],
])('isVirtualModuleUrl(%s) is %s', (search, expected) => {
expect(isVirtualModuleUrl(new URLSearchParams(search))).toBe(expected);
});
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ describe('parseBundleOptionsFromBundleRequestUrl', () => {
);
});

test('rejects a virtual module URL', () => {
expect(() =>
parseBundleOptionsFromBundleRequestUrl(
'http://localhost/App.bundle?platform=ios&virtual=true',
new Set(['ios']),
),
).toThrowError('Virtual modules cannot be requested by URL yet.');
});

test.each(['absolute', 'relative'])(
'%s protocol urls- infers the source url and source map url from the pathname',
type => {
Expand Down
Loading
Loading