Skip to content
Draft
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
26 changes: 24 additions & 2 deletions packages/metro/src/ModuleGraph/worker/collectDependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -812,7 +812,9 @@ const DefaultDependencyTransformer: DependencyTransformer = {
>;
// Always add the debug name argument last
if (state.keepRequireNames) {
path.node.arguments.push(types.stringLiteral(dependency.name));
path.node.arguments.push(
types.stringLiteral(getDebugName(dependency.name)),
);
}
},

Expand Down Expand Up @@ -906,7 +908,7 @@ function createModuleIDExpression(
}

function createModuleNameLiteral(dependency: InternalDependency) {
return types.stringLiteral(dependency.name);
return types.stringLiteral(getDebugName(dependency.name));
}

/**
Expand All @@ -929,6 +931,26 @@ function createModuleNameLiteral(dependency: InternalDependency) {
*
* This method should be utilized by `registerDependency`.
*/
const INLINE_MODULE_PREFIX = 'metro:inline';

/**
* The name shown beside a dependency in development output. An inline module
* specifier carries a whole module in its payload, so only its header is kept.
*/
function getDebugName(name: string): string {
if (
name.length > INLINE_MODULE_PREFIX.length &&
name.slice(0, INLINE_MODULE_PREFIX.length).toLowerCase() ===
INLINE_MODULE_PREFIX
) {
const commaIndex = name.indexOf(',');
if (commaIndex !== -1) {
return name.slice(0, commaIndex + 1) + '...';
}
}
return name;
}

function getKeyForDependency(qualifier: ImportQualifier): string {
const {asyncType, contextParams, isESMImport, name} = qualifier;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* 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';

const Metro = require('../../..');
const execBundle = require('../execBundle');

jest.setTimeout(30 * 1000);

async function build({dev}: {dev: boolean}) {
const config = await Metro.loadConfig({
config: require.resolve('../metro.config.js'),
});
return Metro.runBuild(config, {
entry: 'inline-module/index.js',
dev,
minify: !dev,
});
}

test('an inline module resolves to a virtual module anchored at its importer', async () => {
const {code} = await build({dev: true});
expect(execBundle(code)).toEqual({
// The inline module's relative import resolved to the shared counter.
fromInline: 11,
// Same source, different importer: a separate module instance, sharing the
// counter it imports.
fromSibling: 102,
// The weak id refers to the same module instance as the static require.
fromWeakId: 1003,
weakIdIsOwnModuleId: true,
});
});

test('the virtual module is named after its importer and a hash of its source', async () => {
const {code} = await build({dev: true});
const names = [
...code.matchAll(/"([^"]*inline-module\/[^"]*\?virtual=[0-9a-f]+)"/g),
]
.map(match => match[1])
.sort();
expect(names).toEqual([
expect.stringMatching(/^inline-module\/index\.js\?virtual=[0-9a-f]{40}$/),
expect.stringMatching(/^inline-module\/sibling\.js\?virtual=[0-9a-f]{40}$/),
]);
});

test('development output abbreviates inline module specifiers in require names', async () => {
const {code} = await build({dev: true});
expect(code).toContain('"metro:inline;base64,..."');
expect(code).not.toContain('"metro:inline;base64,aW1wb3J0');
});

test('builds in production', async () => {
const {code} = await build({dev: false});
expect(execBundle(code)).toMatchObject({fromInline: 11, fromSibling: 102});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* 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.
*
* @format
* @oncall react_native
*/

declare var require: {
(id: string | number): any,
resolveWeak: (id: string) => string | number,
};

// The same source from a different origin is a different module with its own
// module scope.
const {default: bumpFromSibling} = require('./sibling');
// The inline module imports a sibling relatively: it resolves from this file's
// directory, because the virtual module is anchored here.
const inline = require('metro:inline;base64,aW1wb3J0IHtpbmNyZW1lbnR9IGZyb20gJy4vc3ViZGlyL2NvdW50ZXInOwpleHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBidW1wKG4pIHsKICByZXR1cm4gaW5jcmVtZW50KCkgKyBuOwp9CmV4cG9ydCBjb25zdCBvd25Nb2R1bGVJZCA9IG1vZHVsZS5pZDsK');

const weakId = require.resolveWeak(
'metro:inline;base64,aW1wb3J0IHtpbmNyZW1lbnR9IGZyb20gJy4vc3ViZGlyL2NvdW50ZXInOwpleHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBidW1wKG4pIHsKICByZXR1cm4gaW5jcmVtZW50KCkgKyBuOwp9CmV4cG9ydCBjb25zdCBvd25Nb2R1bGVJZCA9IG1vZHVsZS5pZDsK',
);

const dynamicRequire = require;

module.exports = {
fromInline: inline.default(10),
fromSibling: bumpFromSibling(100),
// Requiring by the weak id reaches the same instance as the static require.
fromWeakId: dynamicRequire(weakId).default(1000),
weakIdIsOwnModuleId: weakId === inline.ownModuleId,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* 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.
*
* @format
* @oncall react_native
*/

module.exports = require('metro:inline;base64,aW1wb3J0IHtpbmNyZW1lbnR9IGZyb20gJy4vc3ViZGlyL2NvdW50ZXInOwpleHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBidW1wKG4pIHsKICByZXR1cm4gaW5jcmVtZW50KCkgKyBuOwp9CmV4cG9ydCBjb25zdCBvd25Nb2R1bGVJZCA9IG1vZHVsZS5pZDsK');
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* 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.
*
* @format
* @oncall react_native
*/

let count = 0;

export function increment(): number {
return ++count;
}
66 changes: 66 additions & 0 deletions packages/metro/src/lib/__tests__/resolveInlineModule-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* 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 metroSchemeResolver from '../metroSchemeResolver';
import {createResolutionContext} from 'metro-resolver/private/__tests__/utils';

const ORIGIN = '/root/project/foo.js';

function resolve(specifier: string) {
return metroSchemeResolver(
{
...createResolutionContext({}),
originModulePath: ORIGIN,
resolveRequest: () => {
throw new Error('inline module resolution must not delegate');
},
},
specifier,
null,
);
}

test('decodes a base64 payload into a virtual module at the importing module path', () => {
const source = 'export default 1;';
expect(
resolve('metro:inline;base64,' + Buffer.from(source).toString('base64')),
).toEqual({
type: 'virtualModule',
virtualPath: ORIGIN,
source,
});
});

test('decodes a percent-encoded payload', () => {
expect(resolve('metro:inline,export%20default%20%22a%2Cb%22%3B')).toEqual({
type: 'virtualModule',
virtualPath: ORIGIN,
source: 'export default "a,b";',
});
});

test('ignores the case of the parameters', () => {
expect(resolve('metro:inline;BASE64,MQ==')).toMatchObject({source: '1'});
});

test('rejects an unknown parameter', () => {
expect(() => resolve('metro:inline;gzip,hello')).toThrow(
"Unsupported parameter 'gzip'",
);
});

test('rejects a specifier with no payload separator', () => {
expect(() => resolve('metro:inline')).toThrow(
'Malformed inline module specifier',
);
});
16 changes: 14 additions & 2 deletions packages/metro/src/lib/metroSchemeResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

import type {CustomResolver} from 'metro-resolver';

import {
INLINE_MODULE_SPECIFIER,
resolveInlineModule,
} from './resolveInlineModule';
import * as path from 'node:path';

const BABEL_RUNTIME_SPECIFIER = 'babel-runtime';
Expand All @@ -35,12 +39,20 @@ function getBabelRuntimePackageJsonPath(): string {
}

/**
* Resolver used for Metro's own `metro:` URI scheme, currently handling only
* metro:babel-runtime and subpaths.
* Resolver used for Metro's own `metro:` URI scheme, handling
* metro:babel-runtime (and subpaths) and metro:inline modules.
*/
export default ((context, specifier, platform) => {
const {protocol, pathname} = new URL(specifier);

if (
pathname === INLINE_MODULE_SPECIFIER ||
pathname.startsWith(INLINE_MODULE_SPECIFIER + ';') ||
pathname.startsWith(INLINE_MODULE_SPECIFIER + ',')
) {
return resolveInlineModule(context, pathname);
}

// Maps `metro:babel-runtime` (and subpaths, e.g.
// `metro:babel-runtime/helpers/interopRequireDefault`) to metro-runtime's
// `@babel/runtime` dependency, so injected Babel helpers resolve
Expand Down
64 changes: 64 additions & 0 deletions packages/metro/src/lib/resolveInlineModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* 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
*/

import type {ResolutionContext, VirtualResolution} from 'metro-resolver';

export const INLINE_MODULE_SPECIFIER = 'inline';

/**
* Resolves the `inline` pathname of Metro's own URI scheme, so that a transform
* may emit a module inline in its importer:
*
* require('metro:inline;base64,ZXhwb3J0IGRlZmF1bHQgMQ==')
*
* The payload is the module's source, encoded as in a `data:` URL. Unlike a
* `data:` URL, an inline module belongs to its importer: the same payload in
* two modules is two modules, and imports inside it resolve exactly as they
* would from the importer. That is what makes it usable for modules a
* transform derives from the importing file, which is not what `data:`
* promises, so it is deliberately not spelled `data:`.
*
* The result is a pure function of (origin module, specifier). Nothing on disk
* can invalidate it.
*/
export function resolveInlineModule(
context: ResolutionContext,
pathname: string,
): VirtualResolution {
const commaIndex = pathname.indexOf(',');
if (commaIndex === -1) {
throw new Error(`Malformed inline module specifier: metro:${pathname}`);
}
const parameters = pathname
.slice(INLINE_MODULE_SPECIFIER.length, commaIndex)
.split(';')
.map(part => part.trim().toLowerCase())
.filter(part => part !== '');
const payload = pathname.slice(commaIndex + 1);

for (const parameter of parameters) {
if (parameter !== 'base64') {
throw new Error(
`Unsupported parameter '${parameter}' in inline module specifier: metro:${pathname}`,
);
}
}

const source = parameters.includes('base64')
? Buffer.from(payload, 'base64').toString('utf8')
: decodeURIComponent(payload);

return {
type: 'virtualModule',
virtualPath: context.originModulePath,
source,
};
}
Loading