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
15 changes: 14 additions & 1 deletion packages/metro-runtime/src/modules/asyncRequire.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,20 @@ type MetroRequire = {

declare var require: MetroRequire;

type DependencyMapPaths = ?Readonly<{[moduleID: number | string]: unknown}>;
// When using `unstable_getAsyncDependencyPath`, path values may be any
// JSON-serialisable value. By default, they are string URLs - otherwise a
// custom `__loadBundleAsync` implementation must be provided.
type ReadonlyJsonData =
| null
| boolean
| number
| string
| ReadonlyArray<ReadonlyJsonData>
| Readonly<{[string]: ReadonlyJsonData}>;

type DependencyMapPaths = ?Readonly<{
[moduleID: number | string]: ReadonlyJsonData,
}>;

declare var __METRO_GLOBAL_PREFIX__: string;

Expand Down
1 change: 1 addition & 0 deletions packages/metro/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export type SerializerOptions = Readonly<{
sourceUrl: null | undefined | string;
getSourceUrl: null | undefined | (($$PARAM_0$$: Module) => string);
unstable_inlineDependencyMap?: boolean;
unstable_getAsyncDependencyPath?: (dependency: ResolvedDependency, options: unknown) => null | undefined | ReadonlyJsonData;
}>;

export type ServerOptions = Readonly<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default function baseJSBundle(
sourceUrl: options.sourceUrl,
dependencyMapReservedName: options.dependencyMapReservedName,
unstable_inlineDependencyMap: options.unstable_inlineDependencyMap,
unstable_getAsyncDependencyPath: options.unstable_getAsyncDependencyPath,
};

// Do not prepend polyfills or the require runtime when only modules are requested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,61 @@ describe('wrapModule()', () => {
`__d(function() { console.log("foo") },0,{"0":1,"1":2,"paths":{"1":"/../bar.bundle?modulesOnly=true&runModule=false"}});`,
);
});

test('unstable_getAsyncDependencyPath overrides the default `.bundle?` URL', () => {
const dep = nullthrows(myModule.dependencies.get('bar'));
myModule.dependencies.set('bar', {
...dep,
data: {...dep.data, data: {...dep.data.data, asyncType: 'async'}},
});
const seenPaths: Array<string> = [];
const output = wrapModule(myModule, {
createModuleId: createModuleIdFactory(),
dev: false,
includeAsyncPaths: true,
projectRoot: '/root',
serverRoot: '/root',
// Deliberately null: proves the hook does NOT require sourceUrl (the
// default's invariant is skipped when the hook is provided).
sourceUrl: null,
unstable_getAsyncDependencyPath: dependency => {
seenPaths.push(dependency.absolutePath);
return {
displayName: 'bar',
mobileConfig: null,
segmentIDs: [42],
};
},
});
// Hook invoked once, with the async dep only (sync dep 'baz' is skipped).
expect(seenPaths).toEqual(['/bar.js']);
expect(raw(output)).toMatchInlineSnapshot(
`__d(function() { console.log("foo") },0,{"0":1,"1":2,"paths":{"1":{"displayName":"bar","mobileConfig":null,"segmentIDs":[42]}}});`,
);
});

test('unstable_getAsyncDependencyPath omits nullish paths', () => {
const dep = nullthrows(myModule.dependencies.get('bar'));
myModule.dependencies.set('bar', {
...dep,
data: {...dep.data, data: {...dep.data.data, asyncType: 'async'}},
});
expect(
raw(
wrapModule(myModule, {
createModuleId: createModuleIdFactory(),
dev: false,
includeAsyncPaths: true,
projectRoot: '/root',
serverRoot: '/root',
sourceUrl: null,
unstable_getAsyncDependencyPath: () => null,
}),
),
).toMatchInlineSnapshot(
`__d(function() { console.log("foo") },0,{"0":1,"1":2,"paths":{}});`,
);
});
});

describe('wrapModule() with inlined module ids', () => {
Expand Down
96 changes: 63 additions & 33 deletions packages/metro/src/DeltaBundler/Serializers/helpers/js.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
* @oncall react_native
*/

import type {MixedOutput, Module} from '../../types';
import type {
MixedOutput,
Module,
ReadonlyJsonData,
ResolvedDependency,
} from '../../types';
import type {JsOutput} from 'metro-transform-worker';

import {isResolvedDependency} from '../../../lib/isResolvedDependency';
Expand All @@ -31,6 +36,19 @@ export type Options = Readonly<{
// references, instead of being appended as a dependency-map array argument.
dependencyMapReservedName?: ?string,
unstable_inlineDependencyMap?: boolean,
// Overrides how the value stored in the `__d(...)` `paths` object for an
// async dependency is computed. Called once per async dependency, only after
// `isResolvedDependency` has narrowed the dep. Default builds a `.bundle?`
// URL from `sourceUrl` + `serverRoot` (see `getDefaultAsyncDependencyPath`).
// The returned value is treated as opaque by the serializer and forwarded to
// `__loadBundleAsync` at runtime; the shape is a contract between the
// customSerializer and the on-device runtime. It must be JSON-serializable
// since it is embedded into the bundle via `JSON.stringify`.
// Returning nullish omits the value from `paths`.
unstable_getAsyncDependencyPath?: (
dependency: ResolvedDependency,
options: Options,
) => ?ReadonlyJsonData,
...
}>;

Expand Down Expand Up @@ -65,19 +83,57 @@ function getModuleVerboseName(module: Module<>, options: Options): string {
);
}

// Default value stored in the `__d(...)` `paths` object for an async
// dependency: a server-relative `.bundle?` URL propagating most search params
// from the main bundle's URL. Overridable via
// `options.unstable_getAsyncDependencyPath`.
function getDefaultAsyncDependencyPath(
dependency: ResolvedDependency,
options: Options,
): string {
invariant(
options.sourceUrl != null,
'sourceUrl is required when includeAsyncPaths is true',
);

// TODO: Only include path if the target is not in the bundle

// Construct a server-relative URL for the split bundle, propagating
// most parameters from the main bundle's URL.

const {searchParams} = new URL(jscSafeUrl.toNormalUrl(options.sourceUrl));
searchParams.set('modulesOnly', 'true');
searchParams.set('runModule', 'false');

const bundlePath = path.relative(options.serverRoot, dependency.absolutePath);
return (
'/' +
path.join(
// TODO: This is not the proper Metro URL encoding of a file path
path.dirname(bundlePath),
// Strip the file extension
path.basename(bundlePath, path.extname(bundlePath)),
) +
'.bundle?' +
searchParams.toString()
);
}

function getModuleDependencies(
module: Module<>,
options: Options,
): {
moduleId: number | string,
dependencyMapArray: Array<number | string | null>,
paths: {[moduleID: number | string]: unknown},
paths: {[moduleID: number | string]: ReadonlyJsonData},
hasPaths: boolean,
} {
const moduleId = options.createModuleId(module.path);

const paths: {[moduleID: number | string]: unknown} = {};
const paths: {[moduleID: number | string]: ReadonlyJsonData} = {};
let hasPaths = false;
const getAsyncDependencyPath =
options.unstable_getAsyncDependencyPath ?? getDefaultAsyncDependencyPath;
const dependencyMapArray = Array.from(module.dependencies.values()).map(
dependency => {
if (!isResolvedDependency(dependency)) {
Expand All @@ -88,36 +144,10 @@ function getModuleDependencies(
const id = options.createModuleId(dependency.absolutePath);
if (options.includeAsyncPaths && dependency.data.data.asyncType != null) {
hasPaths = true;
invariant(
options.sourceUrl != null,
'sourceUrl is required when includeAsyncPaths is true',
);

// TODO: Only include path if the target is not in the bundle

// Construct a server-relative URL for the split bundle, propagating
// most parameters from the main bundle's URL.

const {searchParams} = new URL(
jscSafeUrl.toNormalUrl(options.sourceUrl),
);
searchParams.set('modulesOnly', 'true');
searchParams.set('runModule', 'false');

const bundlePath = path.relative(
options.serverRoot,
dependency.absolutePath,
);
paths[id] =
'/' +
path.join(
// TODO: This is not the proper Metro URL encoding of a file path
path.dirname(bundlePath),
// Strip the file extension
path.basename(bundlePath, path.extname(bundlePath)),
) +
'.bundle?' +
searchParams.toString();
const asyncDependencyPath = getAsyncDependencyPath(dependency, options);
if (asyncDependencyPath != null) {
paths[id] = asyncDependencyPath;
}
}
return id;
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import type {Module} from '../../types';
import type {Options as WrapModuleOptions} from './js';

import {isJsModule, wrapModule} from './js';

Expand All @@ -25,6 +26,7 @@ export default function processModules(
sourceUrl,
dependencyMapReservedName,
unstable_inlineDependencyMap,
unstable_getAsyncDependencyPath,
}: Readonly<{
filter?: (module: Module<>) => boolean,
createModuleId: string => number,
Expand All @@ -35,6 +37,7 @@ export default function processModules(
sourceUrl: ?string,
dependencyMapReservedName?: ?string,
unstable_inlineDependencyMap?: boolean,
unstable_getAsyncDependencyPath?: WrapModuleOptions['unstable_getAsyncDependencyPath'],
}>,
): ReadonlyArray<[Module<>, string]> {
return [...modules]
Expand All @@ -51,6 +54,7 @@ export default function processModules(
sourceUrl,
dependencyMapReservedName,
unstable_inlineDependencyMap,
unstable_getAsyncDependencyPath,
}),
]);
}
14 changes: 14 additions & 0 deletions packages/metro/src/DeltaBundler/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ import type {JsTransformOptions} from 'metro-transform-worker';

import CountingSet from '../lib/CountingSet';

// Any value produced by `JSON.stringify`. Duplicated (rather than shared) with
// metro-runtime's `asyncRequire` to avoid a new cross-package type coupling.
export type ReadonlyJsonData =
| null
| boolean
| number
| string
| ReadonlyArray<ReadonlyJsonData>
| Readonly<{[string]: ReadonlyJsonData}>;

export type MixedOutput = {
readonly data: unknown,
readonly type: string,
Expand Down Expand Up @@ -188,4 +198,8 @@ export type SerializerOptions = Readonly<{
sourceUrl: ?string,
getSourceUrl: ?(Module<>) => string,
unstable_inlineDependencyMap?: boolean,
unstable_getAsyncDependencyPath?: (
dependency: ResolvedDependency,
options: unknown,
) => ?ReadonlyJsonData,
}>;
Loading