feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,25 @@
/**
* 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
*/
export interface AssetDataWithoutFiles {
readonly __packager_asset: boolean;
readonly fileSystemLocation: string;
readonly hash: string;
readonly height?: number;
readonly httpServerLocation: string;
readonly name: string;
readonly scales: number[];
readonly type: string;
readonly width?: number;
}
export interface AssetData extends AssetDataWithoutFiles {
readonly files: string[];
}

View File

@@ -0,0 +1,229 @@
"use strict";
const AssetPaths = require("./node-haste/lib/AssetPaths");
const crypto = require("crypto");
const denodeify = require("denodeify");
const fs = require("fs");
const getImageSize = require("image-size");
const path = require("path");
const readDir = denodeify(fs.readdir);
const readFile = denodeify(fs.readFile);
function isAssetTypeAnImage(type) {
return (
[
"png",
"jpg",
"jpeg",
"bmp",
"gif",
"webp",
"psd",
"svg",
"tiff",
"ktx",
].indexOf(type) !== -1
);
}
function getAssetSize(type, content, filePath) {
if (!isAssetTypeAnImage(type)) {
return null;
}
if (content.length === 0) {
throw new Error(`Image asset \`${filePath}\` cannot be an empty file.`);
}
const { width, height } = getImageSize(content);
return {
width,
height,
};
}
const hashFiles = denodeify(function hashFilesCb(files, hash, callback) {
if (!files.length) {
callback(null);
return;
}
const file = files.shift();
fs.readFile(file, (err, data) => {
if (err) {
callback(err);
return;
} else {
hash.update(data);
hashFilesCb(files, hash, callback);
}
});
});
function buildAssetMap(dir, files, platform) {
const platforms = new Set(platform != null ? [platform] : []);
const assets = files.map((file) => AssetPaths.tryParse(file, platforms));
const map = new Map();
assets.forEach(function (asset, i) {
if (asset == null) {
return;
}
const file = files[i];
const assetKey = getAssetKey(asset.assetName, asset.platform);
let record = map.get(assetKey);
if (!record) {
record = {
scales: [],
files: [],
};
map.set(assetKey, record);
}
let insertIndex;
const length = record.scales.length;
for (insertIndex = 0; insertIndex < length; insertIndex++) {
if (asset.resolution < record.scales[insertIndex]) {
break;
}
}
record.scales.splice(insertIndex, 0, asset.resolution);
record.files.splice(insertIndex, 0, path.join(dir, file));
});
return map;
}
function getAssetKey(assetName, platform) {
if (platform != null) {
return `${assetName} : ${platform}`;
} else {
return assetName;
}
}
async function getAbsoluteAssetRecord(assetPath, platform = null) {
const filename = path.basename(assetPath);
const dir = path.dirname(assetPath);
const files = await readDir(dir);
const assetData = AssetPaths.parse(
filename,
new Set(platform != null ? [platform] : [])
);
const map = buildAssetMap(dir, files, platform);
let record;
if (platform != null) {
record =
map.get(getAssetKey(assetData.assetName, platform)) ||
map.get(assetData.assetName);
} else {
record = map.get(assetData.assetName);
}
if (!record) {
throw new Error(
`Asset not found: ${assetPath} for platform: ${
platform ?? "(unspecified)"
}`
);
}
return record;
}
async function getAbsoluteAssetInfo(assetPath, platform = null) {
const nameData = AssetPaths.parse(
assetPath,
new Set(platform != null ? [platform] : [])
);
const { name, type } = nameData;
const { scales, files } = await getAbsoluteAssetRecord(assetPath, platform);
const hasher = crypto.createHash("md5");
if (files.length > 0) {
await hashFiles(Array.from(files), hasher);
}
return {
files,
hash: hasher.digest("hex"),
name,
scales,
type,
};
}
async function getAssetData(
assetPath,
localPath,
assetDataPlugins,
platform = null,
publicPath
) {
let assetUrlPath = localPath.startsWith("..")
? publicPath.replace(/\/$/, "") + "/" + path.dirname(localPath)
: path.join(publicPath, path.dirname(localPath));
if (path.sep === "\\") {
assetUrlPath = assetUrlPath.replaceAll("\\", "/");
}
const isImage = isAssetTypeAnImage(path.extname(assetPath).slice(1));
const assetInfo = await getAbsoluteAssetInfo(assetPath, platform);
const isImageInput = assetInfo.files[0].includes(".zip/")
? fs.readFileSync(assetInfo.files[0])
: assetInfo.files[0];
const dimensions = isImage ? getImageSize(isImageInput) : null;
const scale = assetInfo.scales[0];
const assetData = {
__packager_asset: true,
fileSystemLocation: path.dirname(assetPath),
httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / scale : undefined,
height: dimensions ? dimensions.height / scale : undefined,
scales: assetInfo.scales,
files: assetInfo.files,
hash: assetInfo.hash,
name: assetInfo.name,
type: assetInfo.type,
};
return await applyAssetDataPlugins(assetDataPlugins, assetData);
}
async function applyAssetDataPlugins(assetDataPlugins, assetData) {
if (!assetDataPlugins.length) {
return assetData;
}
const [currentAssetPlugin, ...remainingAssetPlugins] = assetDataPlugins;
const assetPluginFunction = require(currentAssetPlugin);
const resultAssetData = await assetPluginFunction(assetData);
return await applyAssetDataPlugins(remainingAssetPlugins, resultAssetData);
}
async function getAssetFiles(assetPath, platform = null) {
const assetData = await getAbsoluteAssetRecord(assetPath, platform);
return assetData.files;
}
async function getAsset(
relativePath,
projectRoot,
watchFolders,
platform = null,
assetExts
) {
const assetData = AssetPaths.parse(
relativePath,
new Set(platform != null ? [platform] : [])
);
const absolutePath = path.resolve(projectRoot, relativePath);
if (!assetExts.includes(assetData.type)) {
throw new Error(
`'${relativePath}' cannot be loaded as its extension is not registered in assetExts`
);
}
if (!pathBelongsToRoots(absolutePath, [projectRoot, ...watchFolders])) {
throw new Error(
`'${relativePath}' could not be found, because it cannot be found in the project root or any watch folder`
);
}
const record = await getAbsoluteAssetRecord(absolutePath, platform);
for (let i = 0; i < record.scales.length; i++) {
if (record.scales[i] >= assetData.resolution) {
return readFile(record.files[i]);
}
}
return readFile(record.files[record.files.length - 1]);
}
function pathBelongsToRoots(pathToCheck, roots) {
for (const rootFolder of roots) {
if (pathToCheck.startsWith(path.resolve(rootFolder))) {
return true;
}
}
return false;
}
module.exports = {
getAsset,
getAssetSize,
getAssetData,
getAssetFiles,
isAssetTypeAnImage,
};

View File

@@ -0,0 +1,357 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {AssetPath} from './node-haste/lib/AssetPaths';
const AssetPaths = require('./node-haste/lib/AssetPaths');
const crypto = require('crypto');
const denodeify = require('denodeify');
const fs = require('fs');
const getImageSize = require('image-size');
const path = require('path');
const readDir = denodeify(fs.readdir);
const readFile = denodeify(fs.readFile);
export type AssetInfo = {
+files: Array<string>,
+hash: string,
+name: string,
+scales: Array<number>,
+type: string,
};
export type AssetDataWithoutFiles = {
+__packager_asset: boolean,
+fileSystemLocation: string,
+hash: string,
+height: ?number,
+httpServerLocation: string,
+name: string,
+scales: Array<number>,
+type: string,
+width: ?number,
...
};
export type AssetDataFiltered = {
+__packager_asset: boolean,
+hash: string,
+height: ?number,
+httpServerLocation: string,
+name: string,
+scales: Array<number>,
+type: string,
+width: ?number,
...
};
// Test extension against all types supported by image-size module.
// If it's not one of these, we won't treat it as an image.
function isAssetTypeAnImage(type: string): boolean {
return (
[
'png',
'jpg',
'jpeg',
'bmp',
'gif',
'webp',
'psd',
'svg',
'tiff',
'ktx',
].indexOf(type) !== -1
);
}
function getAssetSize(
type: string,
content: Buffer,
filePath: string,
): ?{+width: number, +height: number} {
if (!isAssetTypeAnImage(type)) {
return null;
}
if (content.length === 0) {
throw new Error(`Image asset \`${filePath}\` cannot be an empty file.`);
}
const {width, height} = getImageSize(content);
return {width, height};
}
export type AssetData = AssetDataWithoutFiles & {+files: Array<string>, ...};
export type AssetDataPlugin = (
assetData: AssetData,
) => AssetData | Promise<AssetData>;
const hashFiles = denodeify(function hashFilesCb(files, hash, callback): void {
if (!files.length) {
callback(null);
return;
}
const file = files.shift();
fs.readFile(file, (err, data: Buffer) => {
if (err) {
callback(err);
return;
} else {
hash.update(data);
hashFilesCb(files, hash, callback);
}
});
});
function buildAssetMap(
dir: string,
files: $ReadOnlyArray<string>,
platform: ?string,
): Map<string, {files: Array<string>, scales: Array<number>}> {
const platforms = new Set(platform != null ? [platform] : []);
const assets = files.map((file: string) =>
AssetPaths.tryParse(file, platforms),
);
const map = new Map<string, {files: Array<string>, scales: Array<number>}>();
assets.forEach(function (asset: ?AssetPath, i: number) {
if (asset == null) {
return;
}
const file = files[i];
const assetKey = getAssetKey(asset.assetName, asset.platform);
let record = map.get(assetKey);
if (!record) {
record = {
scales: [],
files: [],
};
map.set(assetKey, record);
}
let insertIndex;
const length = record.scales.length;
for (insertIndex = 0; insertIndex < length; insertIndex++) {
if (asset.resolution < record.scales[insertIndex]) {
break;
}
}
record.scales.splice(insertIndex, 0, asset.resolution);
record.files.splice(insertIndex, 0, path.join(dir, file));
});
return map;
}
function getAssetKey(assetName: string, platform: ?string): string {
if (platform != null) {
return `${assetName} : ${platform}`;
} else {
return assetName;
}
}
async function getAbsoluteAssetRecord(
assetPath: string,
platform: ?string = null,
): Promise<{files: Array<string>, scales: Array<number>}> {
const filename = path.basename(assetPath);
const dir = path.dirname(assetPath);
const files = await readDir(dir);
const assetData = AssetPaths.parse(
filename,
new Set(platform != null ? [platform] : []),
);
const map = buildAssetMap(dir, files, platform);
let record;
if (platform != null) {
record =
map.get(getAssetKey(assetData.assetName, platform)) ||
map.get(assetData.assetName);
} else {
record = map.get(assetData.assetName);
}
if (!record) {
throw new Error(
`Asset not found: ${assetPath} for platform: ${
platform ?? '(unspecified)'
}`,
);
}
return record;
}
async function getAbsoluteAssetInfo(
assetPath: string,
platform: ?string = null,
): Promise<AssetInfo> {
const nameData = AssetPaths.parse(
assetPath,
new Set(platform != null ? [platform] : []),
);
const {name, type} = nameData;
const {scales, files} = await getAbsoluteAssetRecord(assetPath, platform);
const hasher = crypto.createHash('md5');
if (files.length > 0) {
await hashFiles(Array.from(files), hasher);
}
return {files, hash: hasher.digest('hex'), name, scales, type};
}
async function getAssetData(
assetPath: string,
localPath: string,
assetDataPlugins: $ReadOnlyArray<string>,
platform: ?string = null,
publicPath: string,
): Promise<AssetData> {
// If the path of the asset is outside of the projectRoot, we don't want to
// use `path.join` since this will generate an incorrect URL path. In that
// case we just concatenate the publicPath with the relative path.
let assetUrlPath = localPath.startsWith('..')
? publicPath.replace(/\/$/, '') + '/' + path.dirname(localPath)
: path.join(publicPath, path.dirname(localPath));
// On Windows, change backslashes to slashes to get proper URL path from file path.
if (path.sep === '\\') {
assetUrlPath = assetUrlPath.replaceAll('\\', '/');
}
const isImage = isAssetTypeAnImage(path.extname(assetPath).slice(1));
const assetInfo = await getAbsoluteAssetInfo(assetPath, platform);
const isImageInput = assetInfo.files[0].includes('.zip/')
? fs.readFileSync(assetInfo.files[0])
: assetInfo.files[0];
const dimensions = isImage ? getImageSize(isImageInput) : null;
const scale = assetInfo.scales[0];
const assetData = {
__packager_asset: true,
fileSystemLocation: path.dirname(assetPath),
httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / scale : undefined,
height: dimensions ? dimensions.height / scale : undefined,
scales: assetInfo.scales,
files: assetInfo.files,
hash: assetInfo.hash,
name: assetInfo.name,
type: assetInfo.type,
};
return await applyAssetDataPlugins(assetDataPlugins, assetData);
}
async function applyAssetDataPlugins(
assetDataPlugins: $ReadOnlyArray<string>,
assetData: AssetData,
): Promise<AssetData> {
if (!assetDataPlugins.length) {
return assetData;
}
const [currentAssetPlugin, ...remainingAssetPlugins] = assetDataPlugins;
// $FlowFixMe: impossible to type a dynamic require.
const assetPluginFunction: AssetDataPlugin = require(currentAssetPlugin);
const resultAssetData = await assetPluginFunction(assetData);
return await applyAssetDataPlugins(remainingAssetPlugins, resultAssetData);
}
/**
* Returns all the associated files (for different resolutions) of an asset.
**/
async function getAssetFiles(
assetPath: string,
platform: ?string = null,
): Promise<Array<string>> {
const assetData = await getAbsoluteAssetRecord(assetPath, platform);
return assetData.files;
}
/**
* Return a buffer with the actual image given a request for an image by path.
* The relativePath can contain a resolution postfix, in this case we need to
* find that image (or the closest one to it's resolution) in one of the
* project roots:
*
* 1. We first parse the directory of the asset
* 2. We then build a map of all assets and their scales in this directory
* 3. Then try to pick platform-specific asset records
* 4. Then pick the closest resolution (rounding up) to the requested one
*/
async function getAsset(
relativePath: string,
projectRoot: string,
watchFolders: $ReadOnlyArray<string>,
platform: ?string = null,
assetExts: $ReadOnlyArray<string>,
): Promise<Buffer> {
const assetData = AssetPaths.parse(
relativePath,
new Set(platform != null ? [platform] : []),
);
const absolutePath = path.resolve(projectRoot, relativePath);
if (!assetExts.includes(assetData.type)) {
throw new Error(
`'${relativePath}' cannot be loaded as its extension is not registered in assetExts`,
);
}
if (!pathBelongsToRoots(absolutePath, [projectRoot, ...watchFolders])) {
throw new Error(
`'${relativePath}' could not be found, because it cannot be found in the project root or any watch folder`,
);
}
const record = await getAbsoluteAssetRecord(absolutePath, platform);
for (let i = 0; i < record.scales.length; i++) {
if (record.scales[i] >= assetData.resolution) {
return readFile(record.files[i]);
}
}
return readFile(record.files[record.files.length - 1]);
}
function pathBelongsToRoots(
pathToCheck: string,
roots: $ReadOnlyArray<string>,
): boolean {
for (const rootFolder of roots) {
if (pathToCheck.startsWith(path.resolve(rootFolder))) {
return true;
}
}
return false;
}
module.exports = {
getAsset,
getAssetSize,
getAssetData,
getAssetFiles,
isAssetTypeAnImage,
};

View File

@@ -0,0 +1,39 @@
/**
* 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
*/
import type {TransformResultWithSource} from './DeltaBundler';
import type {TransformOptions} from './DeltaBundler/Worker';
import type DependencyGraph from './node-haste/DependencyGraph';
import type {EventEmitter} from 'events';
import type {ConfigT} from 'metro-config';
export interface BundlerOptions {
readonly hasReducedPerformance?: boolean;
readonly watch?: boolean;
}
export default class Bundler {
constructor(config: ConfigT, options?: BundlerOptions);
getWatcher(): EventEmitter;
end(): Promise<void>;
getDependencyGraph(): Promise<DependencyGraph>;
transformFile(
filePath: string,
transformOptions: TransformOptions,
/** Optionally provide the file contents, this can be used to provide virtual contents for a file. */
fileBuffer?: Buffer,
): Promise<TransformResultWithSource<void>>;
ready(): Promise<void>;
}

View File

@@ -0,0 +1,53 @@
"use strict";
const Transformer = require("./DeltaBundler/Transformer");
const DependencyGraph = require("./node-haste/DependencyGraph");
class Bundler {
constructor(config, options) {
this._depGraph = new DependencyGraph(config, options);
this._readyPromise = this._depGraph
.ready()
.then(() => {
config.reporter.update({
type: "transformer_load_started",
});
this._transformer = new Transformer(config, (...args) =>
this._depGraph.getSha1(...args)
);
config.reporter.update({
type: "transformer_load_done",
});
})
.catch((error) => {
console.error("Failed to construct transformer: ", error);
config.reporter.update({
type: "transformer_load_failed",
error,
});
});
}
getWatcher() {
return this._depGraph.getWatcher();
}
async end() {
await this._depGraph.ready();
this._transformer.end();
this._depGraph.end();
}
async getDependencyGraph() {
await this._depGraph.ready();
return this._depGraph;
}
async transformFile(filePath, transformOptions, fileBuffer) {
await this._depGraph.ready();
return this._transformer.transformFile(
filePath,
transformOptions,
fileBuffer
);
}
async ready() {
await this._readyPromise;
}
}
module.exports = Bundler;

View File

@@ -0,0 +1,92 @@
/**
* 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 {TransformResultWithSource} from './DeltaBundler';
import type {TransformOptions} from './DeltaBundler/Worker';
import type EventEmitter from 'events';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
const Transformer = require('./DeltaBundler/Transformer');
const DependencyGraph = require('./node-haste/DependencyGraph');
export type BundlerOptions = $ReadOnly<{
hasReducedPerformance?: boolean,
watch?: boolean,
}>;
class Bundler {
_depGraph: DependencyGraph;
_readyPromise: Promise<void>;
_transformer: Transformer;
constructor(config: ConfigT, options?: BundlerOptions) {
this._depGraph = new DependencyGraph(config, options);
this._readyPromise = this._depGraph
.ready()
.then(() => {
config.reporter.update({type: 'transformer_load_started'});
this._transformer = new Transformer(config, (...args) =>
this._depGraph.getSha1(...args),
);
config.reporter.update({type: 'transformer_load_done'});
})
.catch(error => {
console.error('Failed to construct transformer: ', error);
config.reporter.update({
type: 'transformer_load_failed',
error,
});
});
}
getWatcher(): EventEmitter {
return this._depGraph.getWatcher();
}
async end(): Promise<void> {
await this._depGraph.ready();
this._transformer.end();
this._depGraph.end();
}
async getDependencyGraph(): Promise<DependencyGraph> {
await this._depGraph.ready();
return this._depGraph;
}
async transformFile(
filePath: string,
transformOptions: TransformOptions,
/** Optionally provide the file contents, this can be used to provide virtual contents for a file. */
fileBuffer?: Buffer,
): Promise<TransformResultWithSource<>> {
// We need to be sure that the DependencyGraph has been initialized.
// TODO: Remove this ugly hack!
await this._depGraph.ready();
return this._transformer.transformFile(
filePath,
transformOptions,
fileBuffer,
);
}
// Waits for the bundler to become ready.
async ready(): Promise<void> {
await this._readyPromise;
}
}
module.exports = Bundler;

View File

@@ -0,0 +1,95 @@
"use strict";
const babylon = require("@babel/parser");
const template = require("@babel/template").default;
const babelTypes = require("@babel/types");
const assetPropertyBlockList = new Set(["files", "fileSystemLocation", "path"]);
function generateAssetCodeFileAst(assetRegistryPath, assetDescriptor) {
const properDescriptor = filterObject(
assetDescriptor,
assetPropertyBlockList
);
const descriptorAst = babylon.parseExpression(
JSON.stringify(properDescriptor)
);
const t = babelTypes;
const buildRequire = template.statement(`
module.exports = require(ASSET_REGISTRY_PATH).registerAsset(DESCRIPTOR_AST)
`);
return t.file(
t.program([
buildRequire({
ASSET_REGISTRY_PATH: t.stringLiteral(assetRegistryPath),
DESCRIPTOR_AST: descriptorAst,
}),
])
);
}
function filterObject(object, blockList) {
const copied = {
...object,
};
for (const key of blockList) {
delete copied[key];
}
return copied;
}
function createRamBundleGroups(ramGroups, groupableModules, subtree) {
const byPath = new Map();
const byId = new Map();
groupableModules.forEach((m) => {
byPath.set(m.sourcePath, m);
byId.set(m.id, m.sourcePath);
});
const result = new Map(
ramGroups.map((modulePath) => {
const root = byPath.get(modulePath);
if (root == null) {
throw Error(`Group root ${modulePath} is not part of the bundle`);
}
return [root.id, new Set(subtree(root, byPath))];
})
);
if (ramGroups.length > 1) {
const all = new ArrayMap();
for (const [parent, children] of result) {
for (const module of children) {
all.get(module).push(parent);
}
}
const doubles = filter(all, ([, parents]) => parents.length > 1);
for (const [moduleId, parents] of doubles) {
const parentNames = parents.map(byId.get, byId);
const lastName = parentNames.pop();
throw new Error(
`Module ${
byId.get(moduleId) || moduleId
} belongs to groups ${parentNames.join(", ")}, and ${String(
lastName
)}. Ensure that each module is only part of one group.`
);
}
}
return result;
}
function* filter(iterator, predicate) {
for (const value of iterator) {
if (predicate(value)) {
yield value;
}
}
}
class ArrayMap extends Map {
get(key) {
let array = super.get(key);
if (!array) {
array = [];
this.set(key, array);
}
return array;
}
}
module.exports = {
createRamBundleGroups,
generateAssetCodeFileAst,
};

View File

@@ -0,0 +1,153 @@
/**
* 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 {AssetDataFiltered, AssetDataWithoutFiles} from '../Assets';
import type {ModuleTransportLike} from '../shared/types.flow';
import type {File} from '@babel/types';
const babylon = require('@babel/parser');
const template = require('@babel/template').default;
const babelTypes = require('@babel/types');
type SubTree<T: ModuleTransportLike> = (
moduleTransport: T,
moduleTransportsByPath: Map<string, T>,
) => Iterable<number>;
const assetPropertyBlockList = new Set(['files', 'fileSystemLocation', 'path']);
function generateAssetCodeFileAst(
assetRegistryPath: string,
assetDescriptor: AssetDataWithoutFiles,
): File {
const properDescriptor = filterObject(
assetDescriptor,
assetPropertyBlockList,
);
// {...}
const descriptorAst = babylon.parseExpression(
JSON.stringify(properDescriptor),
);
const t = babelTypes;
// require('AssetRegistry').registerAsset({...})
const buildRequire = template.statement(`
module.exports = require(ASSET_REGISTRY_PATH).registerAsset(DESCRIPTOR_AST)
`);
return t.file(
t.program([
buildRequire({
ASSET_REGISTRY_PATH: t.stringLiteral(assetRegistryPath),
DESCRIPTOR_AST: descriptorAst,
}),
]),
);
}
function filterObject(
object: AssetDataWithoutFiles,
blockList: Set<string>,
): AssetDataFiltered {
const copied = {...object};
for (const key of blockList) {
// $FlowFixMe[prop-missing]
delete copied[key];
}
return copied;
}
function createRamBundleGroups<T: ModuleTransportLike>(
ramGroups: $ReadOnlyArray<string>,
groupableModules: $ReadOnlyArray<T>,
subtree: SubTree<T>,
): Map<number, Set<number>> {
// build two maps that allow to lookup module data
// by path or (numeric) module id;
const byPath: Map<string, T> = new Map();
const byId: Map<number, string> = new Map();
groupableModules.forEach((m: T) => {
byPath.set(m.sourcePath, m);
byId.set(m.id, m.sourcePath);
});
// build a map of group root IDs to an array of module IDs in the group
const result: Map<number, Set<number>> = new Map(
ramGroups.map((modulePath: string) => {
const root = byPath.get(modulePath);
if (root == null) {
throw Error(`Group root ${modulePath} is not part of the bundle`);
}
return [
root.id,
// `subtree` yields the IDs of all transitive dependencies of a module
new Set(subtree(root, byPath)),
];
}),
);
if (ramGroups.length > 1) {
// build a map of all grouped module IDs to an array of group root IDs
const all = new ArrayMap<number, number>();
for (const [parent, children] of result) {
for (const module of children) {
all.get(module).push(parent);
}
}
// find all module IDs that are part of more than one group
const doubles = filter(all, ([, parents]) => parents.length > 1);
for (const [moduleId, parents] of doubles) {
// $FlowFixMe[method-unbinding] added when improving typing for this parameters
const parentNames = parents.map(byId.get, byId);
const lastName = parentNames.pop();
throw new Error(
`Module ${
byId.get(moduleId) || moduleId
} belongs to groups ${parentNames.join(', ')}, and ${String(
lastName,
)}. Ensure that each module is only part of one group.`,
);
}
}
return result;
}
function* filter<A: number, B: number>(
iterator: ArrayMap<A, B>,
predicate: ([A, Array<B>]) => boolean,
): Generator<[A, Array<B>], void, void> {
for (const value of iterator) {
if (predicate(value)) {
yield value;
}
}
}
class ArrayMap<K, V> extends Map<K, Array<V>> {
get(key: K): Array<V> {
let array = super.get(key);
if (!array) {
array = [];
this.set(key, array);
}
return array;
}
}
module.exports = {
createRamBundleGroups,
generateAssetCodeFileAst,
};

View File

@@ -0,0 +1,58 @@
/**
* 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
*/
import type {
DeltaResult,
Graph,
MixedOutput,
Options,
ReadOnlyGraph,
} from './DeltaBundler/types';
import type {EventEmitter} from 'events';
export type {
DeltaResult,
Graph,
Dependencies,
MixedOutput,
Module,
ReadOnlyGraph,
TransformFn,
TransformResult,
TransformResultDependency,
TransformResultWithSource,
} from './DeltaBundler/types';
export default class DeltaBundler<T = MixedOutput> {
constructor(changeEventSource: EventEmitter);
end(): void;
getDependencies(
entryPoints: ReadonlyArray<string>,
options: Options<T>,
): Promise<ReadOnlyGraph<T>['dependencies']>;
buildGraph(
entryPoints: ReadonlyArray<string>,
options: Options<T>,
): Promise<Graph<T>>;
getDelta(
graph: Graph<T>,
{
reset,
shallow,
}: {
reset: boolean;
shallow: boolean;
},
): Promise<DeltaResult<T>>;
listen(graph: Graph<T>, callback: () => Promise<void>): () => void;
endGraph(graph: Graph<T>): void;
}

View File

@@ -0,0 +1,70 @@
"use strict";
const DeltaCalculator = require("./DeltaBundler/DeltaCalculator");
class DeltaBundler {
_deltaCalculators = new Map();
constructor(changeEventSource) {
this._changeEventSource = changeEventSource;
}
end() {
this._deltaCalculators.forEach((deltaCalculator) => deltaCalculator.end());
this._deltaCalculators = new Map();
}
async getDependencies(entryPoints, options) {
const deltaCalculator = new DeltaCalculator(
new Set(entryPoints),
this._changeEventSource,
options
);
await deltaCalculator.getDelta({
reset: true,
shallow: options.shallow,
});
const graph = deltaCalculator.getGraph();
deltaCalculator.end();
return graph.dependencies;
}
async buildGraph(entryPoints, options) {
const deltaCalculator = new DeltaCalculator(
new Set(entryPoints),
this._changeEventSource,
options
);
await deltaCalculator.getDelta({
reset: true,
shallow: options.shallow,
});
const graph = deltaCalculator.getGraph();
this._deltaCalculators.set(graph, deltaCalculator);
return graph;
}
async getDelta(graph, { reset, shallow }) {
const deltaCalculator = this._deltaCalculators.get(graph);
if (!deltaCalculator) {
throw new Error("Graph not found");
}
return await deltaCalculator.getDelta({
reset,
shallow,
});
}
listen(graph, callback) {
const deltaCalculator = this._deltaCalculators.get(graph);
if (!deltaCalculator) {
throw new Error("Graph not found");
}
deltaCalculator.on("change", callback);
return () => {
deltaCalculator.removeListener("change", callback);
};
}
endGraph(graph) {
const deltaCalculator = this._deltaCalculators.get(graph);
if (!deltaCalculator) {
throw new Error("Graph not found");
}
deltaCalculator.end();
this._deltaCalculators.delete(graph);
}
}
module.exports = DeltaBundler;

View File

@@ -0,0 +1,144 @@
/**
* 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 {
DeltaResult,
Graph,
// eslint-disable-next-line no-unused-vars
MixedOutput,
Options,
ReadOnlyGraph,
} from './DeltaBundler/types.flow';
import type EventEmitter from 'events';
const DeltaCalculator = require('./DeltaBundler/DeltaCalculator');
export type {
DeltaResult,
Graph,
Dependencies,
MixedOutput,
Module,
ReadOnlyGraph,
TransformFn,
TransformResult,
TransformResultDependency,
TransformResultWithSource,
} from './DeltaBundler/types.flow';
/**
* `DeltaBundler` uses the `DeltaTransformer` to build bundle deltas. This
* module handles all the transformer instances so it can support multiple
* concurrent clients requesting their own deltas. This is done through the
* `clientId` param (which maps a client to a specific delta transformer).
*/
class DeltaBundler<T = MixedOutput> {
_changeEventSource: EventEmitter;
_deltaCalculators: Map<Graph<T>, DeltaCalculator<T>> = new Map();
constructor(changeEventSource: EventEmitter) {
this._changeEventSource = changeEventSource;
}
end(): void {
this._deltaCalculators.forEach((deltaCalculator: DeltaCalculator<T>) =>
deltaCalculator.end(),
);
this._deltaCalculators = new Map();
}
async getDependencies(
entryPoints: $ReadOnlyArray<string>,
options: Options<T>,
): Promise<ReadOnlyGraph<T>['dependencies']> {
const deltaCalculator = new DeltaCalculator(
new Set(entryPoints),
this._changeEventSource,
options,
);
await deltaCalculator.getDelta({reset: true, shallow: options.shallow});
const graph = deltaCalculator.getGraph();
deltaCalculator.end();
return graph.dependencies;
}
// Note: the graph returned by this function needs to be ended when finished
// so that we don't leak graphs that are not reachable.
// To get just the dependencies, use getDependencies which will not leak graphs.
async buildGraph(
entryPoints: $ReadOnlyArray<string>,
options: Options<T>,
): Promise<Graph<T>> {
const deltaCalculator = new DeltaCalculator(
new Set(entryPoints),
this._changeEventSource,
options,
);
await deltaCalculator.getDelta({reset: true, shallow: options.shallow});
const graph = deltaCalculator.getGraph();
this._deltaCalculators.set(graph, deltaCalculator);
return graph;
}
async getDelta(
graph: Graph<T>,
{
reset,
shallow,
}: {
reset: boolean,
shallow: boolean,
...
},
): Promise<DeltaResult<T>> {
const deltaCalculator = this._deltaCalculators.get(graph);
if (!deltaCalculator) {
throw new Error('Graph not found');
}
return await deltaCalculator.getDelta({reset, shallow});
}
listen(graph: Graph<T>, callback: () => Promise<void>): () => void {
const deltaCalculator = this._deltaCalculators.get(graph);
if (!deltaCalculator) {
throw new Error('Graph not found');
}
deltaCalculator.on('change', callback);
return () => {
deltaCalculator.removeListener('change', callback);
};
}
endGraph(graph: Graph<T>): void {
const deltaCalculator = this._deltaCalculators.get(graph);
if (!deltaCalculator) {
throw new Error('Graph not found');
}
deltaCalculator.end();
this._deltaCalculators.delete(graph);
}
}
module.exports = DeltaBundler;

View File

@@ -0,0 +1,205 @@
"use strict";
var _Graph = require("./Graph");
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const debug = require("debug")("Metro:DeltaCalculator");
const { EventEmitter } = require("events");
class DeltaCalculator extends EventEmitter {
_deletedFiles = new Set();
_modifiedFiles = new Set();
_addedFiles = new Set();
_requiresReset = false;
constructor(entryPoints, changeEventSource, options) {
super();
this._options = options;
this._changeEventSource = changeEventSource;
this._graph = new _Graph.Graph({
entryPoints,
transformOptions: this._options.transformOptions,
});
this._changeEventSource.on("change", this._handleMultipleFileChanges);
}
end() {
this._changeEventSource.removeListener(
"change",
this._handleMultipleFileChanges
);
this.removeAllListeners();
this._graph = new _Graph.Graph({
entryPoints: this._graph.entryPoints,
transformOptions: this._options.transformOptions,
});
this._modifiedFiles = new Set();
this._deletedFiles = new Set();
this._addedFiles = new Set();
}
async getDelta({ reset, shallow }) {
debug("Calculating delta (reset: %s, shallow: %s)", reset, shallow);
if (this._currentBuildPromise) {
await this._currentBuildPromise;
}
const modifiedFiles = this._modifiedFiles;
this._modifiedFiles = new Set();
const deletedFiles = this._deletedFiles;
this._deletedFiles = new Set();
const addedFiles = this._addedFiles;
this._addedFiles = new Set();
const requiresReset = this._requiresReset;
this._requiresReset = false;
if (requiresReset) {
const markModified = (file) => {
if (!addedFiles.has(file) && !deletedFiles.has(file)) {
modifiedFiles.add(file);
}
};
this._graph.dependencies.forEach((_, key) => markModified(key));
this._graph.entryPoints.forEach(markModified);
}
this._currentBuildPromise = this._getChangedDependencies(
modifiedFiles,
deletedFiles,
addedFiles
);
let result;
try {
result = await this._currentBuildPromise;
} catch (error) {
modifiedFiles.forEach((file) => this._modifiedFiles.add(file));
deletedFiles.forEach((file) => this._deletedFiles.add(file));
addedFiles.forEach((file) => this._addedFiles.add(file));
throw error;
} finally {
this._currentBuildPromise = null;
}
if (reset) {
this._graph.reorderGraph({
shallow,
});
return {
added: this._graph.dependencies,
modified: new Map(),
deleted: new Set(),
reset: true,
};
}
return result;
}
getGraph() {
return this._graph;
}
_handleMultipleFileChanges = (changeEvent) => {
changeEvent.eventsQueue.forEach((eventInfo) => {
this._handleFileChange(eventInfo, changeEvent.logger);
});
};
_handleFileChange = ({ type, filePath, metadata }, logger) => {
debug("Handling %s: %s (type: %s)", type, filePath, metadata.type);
if (
metadata.type === "l" ||
(this._options.unstable_enablePackageExports &&
filePath.endsWith(_path.default.sep + "package.json"))
) {
this._requiresReset = true;
this.emit("change", {
logger,
});
}
let state;
if (this._deletedFiles.has(filePath)) {
state = "deleted";
} else if (this._modifiedFiles.has(filePath)) {
state = "modified";
} else if (this._addedFiles.has(filePath)) {
state = "added";
}
let nextState;
if (type === "delete") {
nextState = "deleted";
} else if (type === "add") {
nextState = state === "deleted" ? "modified" : "added";
} else {
nextState = state === "added" ? "added" : "modified";
}
switch (nextState) {
case "deleted":
this._deletedFiles.add(filePath);
this._modifiedFiles.delete(filePath);
this._addedFiles.delete(filePath);
break;
case "added":
this._addedFiles.add(filePath);
this._deletedFiles.delete(filePath);
this._modifiedFiles.delete(filePath);
break;
case "modified":
this._modifiedFiles.add(filePath);
this._deletedFiles.delete(filePath);
this._addedFiles.delete(filePath);
break;
default:
nextState;
}
this.emit("change", {
logger,
});
};
async _getChangedDependencies(modifiedFiles, deletedFiles, addedFiles) {
if (!this._graph.dependencies.size) {
const { added } = await this._graph.initialTraverseDependencies(
this._options
);
return {
added,
modified: new Map(),
deleted: new Set(),
reset: true,
};
}
deletedFiles.forEach((filePath) => {
for (const modifiedModulePath of this._graph.getModifiedModulesForDeletedPath(
filePath
)) {
if (!deletedFiles.has(modifiedModulePath)) {
modifiedFiles.add(modifiedModulePath);
}
}
});
if (this._options.unstable_allowRequireContext) {
addedFiles.forEach((filePath) => {
this._graph.markModifiedContextModules(filePath, modifiedFiles);
});
}
const modifiedDependencies = Array.from(modifiedFiles).filter((filePath) =>
this._graph.dependencies.has(filePath)
);
if (modifiedDependencies.length === 0) {
return {
added: new Map(),
modified: new Map(),
deleted: new Set(),
reset: false,
};
}
debug("Traversing dependencies for %s paths", modifiedDependencies.length);
const { added, modified, deleted } = await this._graph.traverseDependencies(
modifiedDependencies,
this._options
);
debug(
"Calculated graph delta {added: %s, modified: %d, deleted: %d}",
added.size,
modified.size,
deleted.size
);
return {
added,
modified,
deleted,
reset: false,
};
}
}
module.exports = DeltaCalculator;

View File

@@ -0,0 +1,339 @@
/**
* 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 {DeltaResult, Options} from './types.flow';
import type {RootPerfLogger} from 'metro-config';
import type {ChangeEventMetadata} from 'metro-file-map';
import {Graph} from './Graph';
import path from 'path';
const debug = require('debug')('Metro:DeltaCalculator');
const {EventEmitter} = require('events');
/**
* This class is in charge of calculating the delta of changed modules that
* happen between calls. To do so, it subscribes to file changes, so it can
* traverse the files that have been changed between calls and avoid having to
* traverse the whole dependency tree for trivial small changes.
*/
class DeltaCalculator<T> extends EventEmitter {
_changeEventSource: EventEmitter;
_options: Options<T>;
_currentBuildPromise: ?Promise<DeltaResult<T>>;
_deletedFiles: Set<string> = new Set();
_modifiedFiles: Set<string> = new Set();
_addedFiles: Set<string> = new Set();
_requiresReset = false;
_graph: Graph<T>;
constructor(
entryPoints: $ReadOnlySet<string>,
changeEventSource: EventEmitter,
options: Options<T>,
) {
super();
this._options = options;
this._changeEventSource = changeEventSource;
this._graph = new Graph({
entryPoints,
transformOptions: this._options.transformOptions,
});
this._changeEventSource.on('change', this._handleMultipleFileChanges);
}
/**
* Stops listening for file changes and clears all the caches.
*/
end(): void {
this._changeEventSource.removeListener(
'change',
this._handleMultipleFileChanges,
);
this.removeAllListeners();
// Clean up all the cache data structures to deallocate memory.
this._graph = new Graph({
entryPoints: this._graph.entryPoints,
transformOptions: this._options.transformOptions,
});
this._modifiedFiles = new Set();
this._deletedFiles = new Set();
this._addedFiles = new Set();
}
/**
* Main method to calculate the delta of modules. It returns a DeltaResult,
* which contain the modified/added modules and the removed modules.
*/
async getDelta({
reset,
shallow,
}: {
reset: boolean,
shallow: boolean,
...
}): Promise<DeltaResult<T>> {
debug('Calculating delta (reset: %s, shallow: %s)', reset, shallow);
// If there is already a build in progress, wait until it finish to start
// processing a new one (delta server doesn't support concurrent builds).
if (this._currentBuildPromise) {
await this._currentBuildPromise;
}
// We don't want the modified files Set to be modified while building the
// bundle, so we isolate them by using the current instance for the bundling
// and creating a new instance for the file watcher.
const modifiedFiles = this._modifiedFiles;
this._modifiedFiles = new Set();
const deletedFiles = this._deletedFiles;
this._deletedFiles = new Set();
const addedFiles = this._addedFiles;
this._addedFiles = new Set();
const requiresReset = this._requiresReset;
this._requiresReset = false;
// Revisit all files if changes require a graph reset - resolutions may be
// invalidated but we don't yet know which. This should be optimized in the
// future.
if (requiresReset) {
const markModified = (file: string) => {
if (!addedFiles.has(file) && !deletedFiles.has(file)) {
modifiedFiles.add(file);
}
};
this._graph.dependencies.forEach((_, key) => markModified(key));
this._graph.entryPoints.forEach(markModified);
}
// Concurrent requests should reuse the same bundling process. To do so,
// this method stores the promise as an instance variable, and then it's
// removed after it gets resolved.
this._currentBuildPromise = this._getChangedDependencies(
modifiedFiles,
deletedFiles,
addedFiles,
);
let result;
try {
result = await this._currentBuildPromise;
} catch (error) {
// In case of error, we don't want to mark the modified files as
// processed (since we haven't actually created any delta). If we do not
// do so, asking for a delta after an error will produce an empty Delta,
// which is not correct.
modifiedFiles.forEach((file: string) => this._modifiedFiles.add(file));
deletedFiles.forEach((file: string) => this._deletedFiles.add(file));
addedFiles.forEach((file: string) => this._addedFiles.add(file));
throw error;
} finally {
this._currentBuildPromise = null;
}
// Return all the modules if the client requested a reset delta.
if (reset) {
this._graph.reorderGraph({shallow});
return {
added: this._graph.dependencies,
modified: new Map(),
deleted: new Set(),
reset: true,
};
}
return result;
}
/**
* Returns the graph with all the dependencies. Each module contains the
* needed information to do the traversing (dependencies, inverseDependencies)
* plus some metadata.
*/
getGraph(): Graph<T> {
return this._graph;
}
/* $FlowFixMe[missing-local-annot] The type annotation(s) required by Flow's
* LTI update could not be added via codemod */
_handleMultipleFileChanges = changeEvent => {
changeEvent.eventsQueue.forEach(eventInfo => {
this._handleFileChange(eventInfo, changeEvent.logger);
});
};
/**
* Handles a single file change. To avoid doing any work before it's needed,
* the listener only stores the modified file, which will then be used later
* when the delta needs to be calculated.
*/
_handleFileChange = (
{
type,
filePath,
metadata,
}: {
type: string,
filePath: string,
metadata: ChangeEventMetadata,
...
},
logger: ?RootPerfLogger,
): mixed => {
debug('Handling %s: %s (type: %s)', type, filePath, metadata.type);
if (
metadata.type === 'l' ||
(this._options.unstable_enablePackageExports &&
filePath.endsWith(path.sep + 'package.json'))
) {
this._requiresReset = true;
this.emit('change', {logger});
}
let state: void | 'deleted' | 'modified' | 'added';
if (this._deletedFiles.has(filePath)) {
state = 'deleted';
} else if (this._modifiedFiles.has(filePath)) {
state = 'modified';
} else if (this._addedFiles.has(filePath)) {
state = 'added';
}
let nextState: 'deleted' | 'modified' | 'added';
if (type === 'delete') {
nextState = 'deleted';
} else if (type === 'add') {
// A deleted+added file is modified
nextState = state === 'deleted' ? 'modified' : 'added';
} else {
// type === 'change'
// An added+modified file is added
nextState = state === 'added' ? 'added' : 'modified';
}
switch (nextState) {
case 'deleted':
this._deletedFiles.add(filePath);
this._modifiedFiles.delete(filePath);
this._addedFiles.delete(filePath);
break;
case 'added':
this._addedFiles.add(filePath);
this._deletedFiles.delete(filePath);
this._modifiedFiles.delete(filePath);
break;
case 'modified':
this._modifiedFiles.add(filePath);
this._deletedFiles.delete(filePath);
this._addedFiles.delete(filePath);
break;
default:
(nextState: empty);
}
// Notify users that there is a change in some of the bundle files. This
// way the client can choose to refetch the bundle.
this.emit('change', {
logger,
});
};
async _getChangedDependencies(
modifiedFiles: Set<string>,
deletedFiles: Set<string>,
addedFiles: Set<string>,
): Promise<DeltaResult<T>> {
if (!this._graph.dependencies.size) {
const {added} = await this._graph.initialTraverseDependencies(
this._options,
);
return {
added,
modified: new Map(),
deleted: new Set(),
reset: true,
};
}
// If a file has been deleted, we want to invalidate any other file that
// depends on it, so we can process it and correctly return an error.
deletedFiles.forEach((filePath: string) => {
for (const modifiedModulePath of this._graph.getModifiedModulesForDeletedPath(
filePath,
)) {
// Only mark the inverse dependency as modified if it's not already
// marked as deleted (in that case we can just ignore it).
if (!deletedFiles.has(modifiedModulePath)) {
modifiedFiles.add(modifiedModulePath);
}
}
});
// NOTE(EvanBacon): This check adds extra complexity so we feature gate it
// to enable users to opt out.
if (this._options.unstable_allowRequireContext) {
// Check if any added or removed files are matched in a context module.
// We only need to do this for added files because (1) deleted files will have a context
// module as an inverse dependency, (2) modified files don't invalidate the contents
// of the context module.
addedFiles.forEach(filePath => {
this._graph.markModifiedContextModules(filePath, modifiedFiles);
});
}
// We only want to process files that are in the bundle.
const modifiedDependencies = Array.from(modifiedFiles).filter(
(filePath: string) => this._graph.dependencies.has(filePath),
);
// No changes happened. Return empty delta.
if (modifiedDependencies.length === 0) {
return {
added: new Map(),
modified: new Map(),
deleted: new Set(),
reset: false,
};
}
debug('Traversing dependencies for %s paths', modifiedDependencies.length);
const {added, modified, deleted} = await this._graph.traverseDependencies(
modifiedDependencies,
this._options,
);
debug(
'Calculated graph delta {added: %s, modified: %d, deleted: %d}',
added.size,
modified.size,
deleted.size,
);
return {
added,
modified,
deleted,
reset: false,
};
}
}
module.exports = DeltaCalculator;

View File

@@ -0,0 +1,40 @@
/**
* 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
*/
import type {
Dependencies,
GraphInputOptions,
MixedOutput,
Module,
Options,
TransformInputOptions,
} from './types';
export interface Result<T> {
added: Map<string, Module<T>>;
modified: Map<string, Module<T>>;
deleted: Set<string>;
}
export class Graph<T = MixedOutput> {
entryPoints: ReadonlySet<string>;
transformOptions: TransformInputOptions;
dependencies: Dependencies<T>;
constructor(options: GraphInputOptions);
traverseDependencies(
paths: ReadonlyArray<string>,
options: Options<T>,
): Promise<Result<T>>;
initialTraverseDependencies(options: Options<T>): Promise<Result<T>>;
markModifiedContextModules(
filePath: string,
modifiedPaths: Set<string>,
): void;
}

View File

@@ -0,0 +1,580 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.Graph = void 0;
var _contextModule = require("../lib/contextModule");
var _CountingSet = _interopRequireDefault(require("../lib/CountingSet"));
var _buildSubgraph = require("./buildSubgraph");
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const invariant = require("invariant");
const nullthrows = require("nullthrows");
function getInternalOptions({ transform, resolve, onProgress, lazy, shallow }) {
let numProcessed = 0;
let total = 0;
return {
lazy,
transform,
resolve,
onDependencyAdd: () => onProgress && onProgress(numProcessed, ++total),
onDependencyAdded: () => onProgress && onProgress(++numProcessed, total),
shallow,
};
}
function isWeakOrLazy(dependency, options) {
const asyncType = dependency.data.data.asyncType;
return asyncType === "weak" || (asyncType != null && options.lazy);
}
class Graph {
dependencies = new Map();
#importBundleNodes = new Map();
#gc = {
color: new Map(),
possibleCycleRoots: new Set(),
};
#resolvedContexts = new Map();
constructor(options) {
this.entryPoints = options.entryPoints;
this.transformOptions = options.transformOptions;
}
async traverseDependencies(paths, options) {
const internalOptions = getInternalOptions(options);
const modifiedPathsInBaseGraph = new Set(
paths.filter((path) => this.dependencies.has(path))
);
const allModifiedPaths = new Set(paths);
const delta = await this._buildDelta(
modifiedPathsInBaseGraph,
internalOptions,
(absolutePath) =>
!this.dependencies.has(absolutePath) ||
allModifiedPaths.has(absolutePath)
);
if (delta.errors.size > 0) {
for (const modified of modifiedPathsInBaseGraph) {
delta.baseModuleData.set(
modified,
this._moduleSnapshot(nullthrows(this.dependencies.get(modified)))
);
}
}
for (const modified of modifiedPathsInBaseGraph) {
if (delta.errors.has(modified)) {
continue;
}
const module = this.dependencies.get(modified);
if (module == null) {
continue;
}
this._recursivelyCommitModule(modified, delta, internalOptions, {
onlyRemove: true,
});
}
this._collectCycles(delta, internalOptions);
try {
for (const modified of modifiedPathsInBaseGraph) {
const module = this.dependencies.get(modified);
if (module == null) {
continue;
}
this._recursivelyCommitModule(modified, delta, internalOptions);
}
} catch (error) {
const rollbackDelta = {
added: delta.added,
deleted: delta.deleted,
touched: new Set(),
updatedModuleData: delta.baseModuleData,
baseModuleData: new Map(),
errors: new Map(),
};
for (const modified of modifiedPathsInBaseGraph) {
const module = this.dependencies.get(modified);
if (module == null) {
continue;
}
this._recursivelyCommitModule(modified, rollbackDelta, internalOptions);
}
this._collectCycles(delta, internalOptions);
invariant(
rollbackDelta.added.size === 0 && rollbackDelta.deleted.size === 0,
"attempted to roll back a graph commit but there were still changes"
);
throw error;
}
const added = new Map();
for (const path of delta.added) {
added.set(path, nullthrows(this.dependencies.get(path)));
}
const modified = new Map();
for (const path of modifiedPathsInBaseGraph) {
if (
delta.touched.has(path) &&
!delta.deleted.has(path) &&
!delta.added.has(path)
) {
modified.set(path, nullthrows(this.dependencies.get(path)));
}
}
return {
added,
modified,
deleted: delta.deleted,
};
}
async initialTraverseDependencies(options) {
const internalOptions = getInternalOptions(options);
invariant(
this.dependencies.size === 0,
"initialTraverseDependencies called on nonempty graph"
);
this.#gc.color.clear();
this.#gc.possibleCycleRoots.clear();
this.#importBundleNodes.clear();
for (const path of this.entryPoints) {
this.#gc.color.set(path, "black");
}
const delta = await this._buildDelta(this.entryPoints, internalOptions);
if (delta.errors.size > 0) {
throw delta.errors.values().next().value;
}
for (const path of this.entryPoints) {
this._recursivelyCommitModule(path, delta, internalOptions);
}
this.reorderGraph({
shallow: options.shallow,
});
return {
added: this.dependencies,
modified: new Map(),
deleted: new Set(),
};
}
async _buildDelta(pathsToVisit, options, moduleFilter) {
const subGraph = await (0, _buildSubgraph.buildSubgraph)(
pathsToVisit,
this.#resolvedContexts,
{
resolve: options.resolve,
transform: async (absolutePath, requireContext) => {
options.onDependencyAdd();
const result = await options.transform(absolutePath, requireContext);
options.onDependencyAdded();
return result;
},
shouldTraverse: (dependency) => {
if (options.shallow || isWeakOrLazy(dependency, options)) {
return false;
}
return moduleFilter == null || moduleFilter(dependency.absolutePath);
},
}
);
return {
added: new Set(),
touched: new Set(),
deleted: new Set(),
updatedModuleData: subGraph.moduleData,
baseModuleData: new Map(),
errors: subGraph.errors,
};
}
_recursivelyCommitModule(
path,
delta,
options,
commitOptions = {
onlyRemove: false,
}
) {
if (delta.errors.has(path)) {
throw delta.errors.get(path);
}
const previousModule = this.dependencies.get(path);
const currentModule = nullthrows(
delta.updatedModuleData.get(path) ?? delta.baseModuleData.get(path)
);
const previousDependencies = previousModule?.dependencies ?? new Map();
const {
dependencies: currentDependencies,
resolvedContexts,
...transformResult
} = currentModule;
const nextModule = {
...(previousModule ?? {
inverseDependencies: new _CountingSet.default(),
path,
}),
...transformResult,
dependencies: new Map(previousDependencies),
};
this.dependencies.set(nextModule.path, nextModule);
if (previousModule == null) {
if (delta.deleted.has(path)) {
delta.deleted.delete(path);
} else {
delta.added.add(path);
}
}
let dependenciesRemoved = false;
for (const [key, prevDependency] of previousDependencies) {
const curDependency = currentDependencies.get(key);
if (
!curDependency ||
!dependenciesEqual(prevDependency, curDependency, options)
) {
dependenciesRemoved = true;
this._removeDependency(nextModule, key, prevDependency, delta, options);
}
}
let dependenciesAdded = false;
if (!commitOptions.onlyRemove) {
for (const [key, curDependency] of currentDependencies) {
const prevDependency = previousDependencies.get(key);
if (
!prevDependency ||
!dependenciesEqual(prevDependency, curDependency, options)
) {
dependenciesAdded = true;
this._addDependency(
nextModule,
key,
curDependency,
resolvedContexts.get(key),
delta,
options
);
}
}
}
const previousDependencyKeys = [...previousDependencies.keys()];
const dependencyKeysChangedOrReordered =
currentDependencies.size !== previousDependencies.size ||
[...currentDependencies.keys()].some(
(currentKey, index) => currentKey !== previousDependencyKeys[index]
);
if (
previousModule != null &&
!transformOutputMayDiffer(previousModule, nextModule) &&
!dependenciesRemoved &&
!dependenciesAdded &&
!dependencyKeysChangedOrReordered
) {
this.dependencies.set(previousModule.path, previousModule);
return previousModule;
}
delta.touched.add(path);
if (commitOptions.onlyRemove) {
return nextModule;
}
invariant(
nextModule.dependencies.size === currentDependencies.size,
"Failed to add the correct dependencies"
);
nextModule.dependencies = new Map(currentDependencies);
return nextModule;
}
_addDependency(
parentModule,
key,
dependency,
requireContext,
delta,
options
) {
const path = dependency.absolutePath;
let module = this.dependencies.get(path);
if (options.shallow) {
} else if (dependency.data.data.asyncType === "weak") {
} else if (options.lazy && dependency.data.data.asyncType != null) {
this._incrementImportBundleReference(dependency, parentModule);
} else {
if (!module) {
try {
module = this._recursivelyCommitModule(path, delta, options);
} catch (error) {
const module = this.dependencies.get(path);
if (module) {
if (module.inverseDependencies.size > 0) {
this._markAsPossibleCycleRoot(module);
} else {
this._releaseModule(module, delta, options);
}
}
throw error;
}
}
module.inverseDependencies.add(parentModule.path);
this._markModuleInUse(module);
}
if (requireContext) {
this.#resolvedContexts.set(path, requireContext);
} else {
this.#resolvedContexts.delete(path);
}
parentModule.dependencies.set(key, dependency);
}
_removeDependency(parentModule, key, dependency, delta, options) {
parentModule.dependencies.delete(key);
const { absolutePath } = dependency;
if (dependency.data.data.asyncType === "weak") {
return;
}
const module = this.dependencies.get(absolutePath);
if (options.lazy && dependency.data.data.asyncType != null) {
this._decrementImportBundleReference(dependency, parentModule);
} else if (module) {
module.inverseDependencies.delete(parentModule.path);
}
if (!module) {
return;
}
if (
module.inverseDependencies.size > 0 ||
this.entryPoints.has(absolutePath)
) {
this._markAsPossibleCycleRoot(module);
} else {
this._releaseModule(module, delta, options);
}
}
markModifiedContextModules(filePath, modifiedPaths) {
for (const [absolutePath, context] of this.#resolvedContexts) {
if (
!modifiedPaths.has(absolutePath) &&
(0, _contextModule.fileMatchesContext)(filePath, context)
) {
modifiedPaths.add(absolutePath);
}
}
}
*getModifiedModulesForDeletedPath(filePath) {
yield* this.dependencies.get(filePath)?.inverseDependencies ?? [];
yield* this.#importBundleNodes.get(filePath)?.inverseDependencies ?? [];
}
reorderGraph(options) {
const orderedDependencies = new Map();
this.entryPoints.forEach((entryPoint) => {
const mainModule = this.dependencies.get(entryPoint);
if (!mainModule) {
throw new ReferenceError(
"Module not registered in graph: " + entryPoint
);
}
this._reorderDependencies(mainModule, orderedDependencies, options);
});
this.dependencies.clear();
for (const [key, dep] of orderedDependencies) {
this.dependencies.set(key, dep);
}
}
_reorderDependencies(module, orderedDependencies, options) {
if (module.path) {
if (orderedDependencies.has(module.path)) {
return;
}
orderedDependencies.set(module.path, module);
}
module.dependencies.forEach((dependency) => {
const path = dependency.absolutePath;
const childModule = this.dependencies.get(path);
if (!childModule) {
if (dependency.data.data.asyncType != null || options.shallow) {
return;
} else {
throw new ReferenceError("Module not registered in graph: " + path);
}
}
this._reorderDependencies(childModule, orderedDependencies, options);
});
}
_incrementImportBundleReference(dependency, parentModule) {
const { absolutePath } = dependency;
const importBundleNode = this.#importBundleNodes.get(absolutePath) ?? {
inverseDependencies: new _CountingSet.default(),
};
importBundleNode.inverseDependencies.add(parentModule.path);
this.#importBundleNodes.set(absolutePath, importBundleNode);
}
_decrementImportBundleReference(dependency, parentModule) {
const { absolutePath } = dependency;
const importBundleNode = nullthrows(
this.#importBundleNodes.get(absolutePath)
);
invariant(
importBundleNode.inverseDependencies.has(parentModule.path),
"lazy: import bundle inverse references"
);
importBundleNode.inverseDependencies.delete(parentModule.path);
if (importBundleNode.inverseDependencies.size === 0) {
this.#importBundleNodes.delete(absolutePath);
}
}
_markModuleInUse(module) {
this.#gc.color.set(module.path, "black");
}
*_children(module, options) {
for (const dependency of module.dependencies.values()) {
if (isWeakOrLazy(dependency, options)) {
continue;
}
yield nullthrows(this.dependencies.get(dependency.absolutePath));
}
}
_moduleSnapshot(module) {
const { dependencies, getSource, output, unstable_transformResultKey } =
module;
const resolvedContexts = new Map();
for (const [key, dependency] of dependencies) {
const resolvedContext = this.#resolvedContexts.get(
dependency.absolutePath
);
if (resolvedContext != null) {
resolvedContexts.set(key, resolvedContext);
}
}
return {
dependencies: new Map(dependencies),
resolvedContexts,
getSource,
output,
unstable_transformResultKey,
};
}
_releaseModule(module, delta, options) {
if (
!delta.updatedModuleData.has(module.path) &&
!delta.baseModuleData.has(module.path)
) {
delta.baseModuleData.set(module.path, this._moduleSnapshot(module));
}
for (const [key, dependency] of module.dependencies) {
this._removeDependency(module, key, dependency, delta, options);
}
this.#gc.color.set(module.path, "black");
this._freeModule(module, delta);
}
_freeModule(module, delta) {
if (delta.added.has(module.path)) {
delta.added.delete(module.path);
} else {
delta.deleted.add(module.path);
}
this.dependencies.delete(module.path);
this.#gc.possibleCycleRoots.delete(module.path);
this.#gc.color.delete(module.path);
this.#resolvedContexts.delete(module.path);
}
_markAsPossibleCycleRoot(module) {
if (this.#gc.color.get(module.path) !== "purple") {
this.#gc.color.set(module.path, "purple");
this.#gc.possibleCycleRoots.add(module.path);
}
}
_collectCycles(delta, options) {
for (const path of this.#gc.possibleCycleRoots) {
const module = nullthrows(this.dependencies.get(path));
const color = nullthrows(this.#gc.color.get(path));
if (color === "purple") {
this._markGray(module, options);
} else {
this.#gc.possibleCycleRoots.delete(path);
if (
color === "black" &&
module.inverseDependencies.size === 0 &&
!this.entryPoints.has(path)
) {
this._freeModule(module, delta);
}
}
}
for (const path of this.#gc.possibleCycleRoots) {
const module = nullthrows(this.dependencies.get(path));
this._scan(module, options);
}
for (const path of this.#gc.possibleCycleRoots) {
this.#gc.possibleCycleRoots.delete(path);
const module = nullthrows(this.dependencies.get(path));
this._collectWhite(module, delta);
}
}
_markGray(module, options) {
const color = nullthrows(this.#gc.color.get(module.path));
if (color !== "gray") {
this.#gc.color.set(module.path, "gray");
for (const childModule of this._children(module, options)) {
childModule.inverseDependencies.delete(module.path);
this._markGray(childModule, options);
}
}
}
_scan(module, options) {
const color = nullthrows(this.#gc.color.get(module.path));
if (color === "gray") {
if (
module.inverseDependencies.size > 0 ||
this.entryPoints.has(module.path)
) {
this._scanBlack(module, options);
} else {
this.#gc.color.set(module.path, "white");
for (const childModule of this._children(module, options)) {
this._scan(childModule, options);
}
}
}
}
_scanBlack(module, options) {
this.#gc.color.set(module.path, "black");
for (const childModule of this._children(module, options)) {
childModule.inverseDependencies.add(module.path);
const childColor = nullthrows(this.#gc.color.get(childModule.path));
if (childColor !== "black") {
this._scanBlack(childModule, options);
}
}
}
_collectWhite(module, delta) {
const color = nullthrows(this.#gc.color.get(module.path));
if (color === "white" && !this.#gc.possibleCycleRoots.has(module.path)) {
this.#gc.color.set(module.path, "black");
for (const dependency of module.dependencies.values()) {
const childModule = this.dependencies.get(dependency.absolutePath);
if (childModule) {
this._collectWhite(childModule, delta);
}
}
this._freeModule(module, delta);
}
}
}
exports.Graph = Graph;
function dependenciesEqual(a, b, options) {
return (
a === b ||
(a.absolutePath === b.absolutePath &&
(!options.lazy || a.data.data.asyncType === b.data.data.asyncType) &&
contextParamsEqual(a.data.data.contextParams, b.data.data.contextParams))
);
}
function contextParamsEqual(a, b) {
return (
a === b ||
(a == null && b == null) ||
(a != null &&
b != null &&
a.recursive === b.recursive &&
a.filter.pattern === b.filter.pattern &&
a.filter.flags === b.filter.flags &&
a.mode === b.mode)
);
}
function transformOutputMayDiffer(a, b) {
return (
a.unstable_transformResultKey == null ||
a.unstable_transformResultKey !== b.unstable_transformResultKey
);
}

View File

@@ -0,0 +1,944 @@
/**
* 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
*/
/**
* Portions of this code are based on the Synchronous Cycle Collection
* algorithm described in:
*
* David F. Bacon and V. T. Rajan. 2001. Concurrent Cycle Collection in
* Reference Counted Systems. In Proceedings of the 15th European Conference on
* Object-Oriented Programming (ECOOP '01). Springer-Verlag, Berlin,
* Heidelberg, 207235.
*
* Notable differences from the algorithm in the paper:
* 1. Our implementation uses the inverseDependencies set (which we already
* have to maintain) instead of a separate refcount variable. A module's
* reference count is equal to the size of its inverseDependencies set, plus
* 1 if it's an entry point of the graph.
* 2. We keep the "root buffer" (possibleCycleRoots) free of duplicates by
* making it a Set, instead of storing a "buffered" flag on each node.
* 3. On top of tracking edges between nodes, we also count references between
* nodes and entries in the importBundleNodes set.
*/
import type {RequireContext} from '../lib/contextModule';
import type {RequireContextParams} from '../ModuleGraph/worker/collectDependencies';
import type {
Dependencies,
Dependency,
GraphInputOptions,
MixedOutput,
Module,
ModuleData,
Options,
TransformInputOptions,
} from './types.flow';
import {fileMatchesContext} from '../lib/contextModule';
import CountingSet from '../lib/CountingSet';
import {buildSubgraph} from './buildSubgraph';
const invariant = require('invariant');
const nullthrows = require('nullthrows');
// TODO: Convert to a Flow enum
type NodeColor =
// In use or free
| 'black'
// Possible member of cycle
| 'gray'
// Member of garbage cycle
| 'white'
// Possible root of cycle
| 'purple'
// Inherently acyclic node (Not currently used)
| 'green';
export type Result<T> = {
added: Map<string, Module<T>>,
modified: Map<string, Module<T>>,
deleted: Set<string>,
};
/**
* Internal data structure that the traversal logic uses to know which of the
* files have been modified. This allows to return the added modules before the
* modified ones (which is useful for things like Hot Module Reloading).
**/
type Delta<T> = $ReadOnly<{
// `added` and `deleted` are mutually exclusive.
// Internally, a module can be in both `touched` and (either) `added` or
// `deleted`. Before returning the result, we'll calculate
// modified = touched - added - deleted.
added: Set<string>,
touched: Set<string>,
deleted: Set<string>,
updatedModuleData: $ReadOnlyMap<string, ModuleData<T>>,
baseModuleData: Map<string, ModuleData<T>>,
errors: $ReadOnlyMap<string, Error>,
}>;
type InternalOptions<T> = $ReadOnly<{
lazy: boolean,
onDependencyAdd: () => mixed,
onDependencyAdded: () => mixed,
resolve: Options<T>['resolve'],
transform: Options<T>['transform'],
shallow: boolean,
}>;
function getInternalOptions<T>({
transform,
resolve,
onProgress,
lazy,
shallow,
}: Options<T>): InternalOptions<T> {
let numProcessed = 0;
let total = 0;
return {
lazy,
transform,
resolve,
onDependencyAdd: () => onProgress && onProgress(numProcessed, ++total),
onDependencyAdded: () => onProgress && onProgress(++numProcessed, total),
shallow,
};
}
function isWeakOrLazy<T>(
dependency: Dependency,
options: InternalOptions<T>,
): boolean {
const asyncType = dependency.data.data.asyncType;
return asyncType === 'weak' || (asyncType != null && options.lazy);
}
export class Graph<T = MixedOutput> {
+entryPoints: $ReadOnlySet<string>;
+transformOptions: TransformInputOptions;
+dependencies: Dependencies<T> = new Map();
+#importBundleNodes: Map<
string,
$ReadOnly<{
inverseDependencies: CountingSet<string>,
}>,
> = new Map();
/// GC state for nodes in the graph (this.dependencies)
+#gc: {
+color: Map<string, NodeColor>,
+possibleCycleRoots: Set<string>,
} = {
color: new Map(),
possibleCycleRoots: new Set(),
};
/** Resolved context parameters from `require.context`. */
#resolvedContexts: Map<string, RequireContext> = new Map();
constructor(options: GraphInputOptions) {
this.entryPoints = options.entryPoints;
this.transformOptions = options.transformOptions;
}
/**
* Dependency Traversal logic for the Delta Bundler. This method calculates
* the modules that should be included in the bundle by traversing the
* dependency graph.
* Instead of traversing the whole graph each time, it just calculates the
* difference between runs by only traversing the added/removed dependencies.
* To do so, it uses the passed graph dependencies and it mutates it.
* The paths parameter contains the absolute paths of the root files that the
* method should traverse. Normally, these paths should be the modified files
* since the last traversal.
*/
async traverseDependencies(
paths: $ReadOnlyArray<string>,
options: Options<T>,
): Promise<Result<T>> {
const internalOptions = getInternalOptions(options);
const modifiedPathsInBaseGraph = new Set(
paths.filter(path => this.dependencies.has(path)),
);
const allModifiedPaths = new Set(paths);
const delta = await this._buildDelta(
modifiedPathsInBaseGraph,
internalOptions,
// Traverse new or modified paths
absolutePath =>
!this.dependencies.has(absolutePath) ||
allModifiedPaths.has(absolutePath),
);
// If we have errors we might need to roll back any changes - take
// snapshots of all modified modules at the base state. We'll also snapshot
// unmodified modules that become unreachable as they are released, so that
// we have everything we need to restore the graph to base.
if (delta.errors.size > 0) {
for (const modified of modifiedPathsInBaseGraph) {
delta.baseModuleData.set(
modified,
this._moduleSnapshot(nullthrows(this.dependencies.get(modified))),
);
}
}
// Commit changes in a subtractive pass and then an additive pass - this
// ensures that any errors encountered on the additive pass would also be
// encountered on a fresh build (implying legitimate errors in the graph,
// rather than an error in a module that's no longer reachable).
for (const modified of modifiedPathsInBaseGraph) {
// Skip this module if it has errors. Hopefully it will be removed -
// if not, we'll throw during the additive pass.
if (delta.errors.has(modified)) {
continue;
}
const module = this.dependencies.get(modified);
// The module may have already been released from the graph - we'll readd
// it if necessary later.
if (module == null) {
continue;
}
// Process the transform result and dependency removals. This should
// never encounter an error.
this._recursivelyCommitModule(modified, delta, internalOptions, {
onlyRemove: true,
});
}
// Ensure we have released any unreachable modules before the additive
// pass.
this._collectCycles(delta, internalOptions);
// Additive pass - any errors we encounter here should be thrown after
// rolling back the commit.
try {
for (const modified of modifiedPathsInBaseGraph) {
const module = this.dependencies.get(modified);
// The module may have already been released from the graph (it may yet
// be readded via another dependency).
if (module == null) {
continue;
}
this._recursivelyCommitModule(modified, delta, internalOptions);
}
} catch (error) {
// Roll back to base before re-throwing.
const rollbackDelta: Delta<T> = {
added: delta.added,
deleted: delta.deleted,
touched: new Set(),
updatedModuleData: delta.baseModuleData,
baseModuleData: new Map(),
errors: new Map(),
};
for (const modified of modifiedPathsInBaseGraph) {
const module = this.dependencies.get(modified);
// The module may have already been released from the graph (it may yet
// be readded via another dependency).
if (module == null) {
continue;
}
// Set the module and descendants back to base state.
this._recursivelyCommitModule(modified, rollbackDelta, internalOptions);
}
// Collect cycles again after rolling back. There's no need if we're
// not rolling back, because we have not removed any edges.
this._collectCycles(delta, internalOptions);
// Cheap check to validate the rollback.
invariant(
rollbackDelta.added.size === 0 && rollbackDelta.deleted.size === 0,
'attempted to roll back a graph commit but there were still changes',
);
// Re-throw the transform or resolution error originally seen by
// `buildSubgraph`.
throw error;
}
const added = new Map<string, Module<T>>();
for (const path of delta.added) {
added.set(path, nullthrows(this.dependencies.get(path)));
}
const modified = new Map<string, Module<T>>();
for (const path of modifiedPathsInBaseGraph) {
if (
delta.touched.has(path) &&
!delta.deleted.has(path) &&
!delta.added.has(path)
) {
modified.set(path, nullthrows(this.dependencies.get(path)));
}
}
return {
added,
modified,
deleted: delta.deleted,
};
}
async initialTraverseDependencies(options: Options<T>): Promise<Result<T>> {
const internalOptions = getInternalOptions(options);
invariant(
this.dependencies.size === 0,
'initialTraverseDependencies called on nonempty graph',
);
this.#gc.color.clear();
this.#gc.possibleCycleRoots.clear();
this.#importBundleNodes.clear();
for (const path of this.entryPoints) {
// Each entry point implicitly has a refcount of 1, so mark them all black.
this.#gc.color.set(path, 'black');
}
const delta = await this._buildDelta(this.entryPoints, internalOptions);
if (delta.errors.size > 0) {
// If we encountered any errors during traversal, throw one of them.
// Since errors are encountered in a non-deterministic order, even on
// fresh builds, it's valid to arbitrarily pick the first.
throw delta.errors.values().next().value;
}
for (const path of this.entryPoints) {
// We have already thrown on userland errors in the delta, so any error
// encountered here would be exceptional and fatal.
this._recursivelyCommitModule(path, delta, internalOptions);
}
this.reorderGraph({
shallow: options.shallow,
});
return {
added: this.dependencies,
modified: new Map(),
deleted: new Set(),
};
}
async _buildDelta(
pathsToVisit: $ReadOnlySet<string>,
options: InternalOptions<T>,
moduleFilter?: (path: string) => boolean,
): Promise<Delta<T>> {
const subGraph = await buildSubgraph(pathsToVisit, this.#resolvedContexts, {
resolve: options.resolve,
transform: async (absolutePath, requireContext) => {
options.onDependencyAdd();
const result = await options.transform(absolutePath, requireContext);
options.onDependencyAdded();
return result;
},
shouldTraverse: (dependency: Dependency) => {
if (options.shallow || isWeakOrLazy(dependency, options)) {
return false;
}
return moduleFilter == null || moduleFilter(dependency.absolutePath);
},
});
return {
added: new Set(),
touched: new Set(),
deleted: new Set(),
updatedModuleData: subGraph.moduleData,
baseModuleData: new Map(),
errors: subGraph.errors,
};
}
_recursivelyCommitModule(
path: string,
delta: Delta<T>,
options: InternalOptions<T>,
commitOptions: $ReadOnly<{
onlyRemove: boolean,
}> = {onlyRemove: false},
): Module<T> {
if (delta.errors.has(path)) {
throw delta.errors.get(path);
}
const previousModule = this.dependencies.get(path);
const currentModule: ModuleData<T> = nullthrows(
delta.updatedModuleData.get(path) ?? delta.baseModuleData.get(path),
);
const previousDependencies = previousModule?.dependencies ?? new Map();
const {
dependencies: currentDependencies,
resolvedContexts,
...transformResult
} = currentModule;
const nextModule = {
...(previousModule ?? {
inverseDependencies: new CountingSet(),
path,
}),
...transformResult,
dependencies: new Map(previousDependencies),
};
// Update the module information.
this.dependencies.set(nextModule.path, nextModule);
if (previousModule == null) {
// If the module is not currently in the graph, it is either new or was
// released earlier in the commit.
if (delta.deleted.has(path)) {
// Mark the addition by clearing a prior deletion.
delta.deleted.delete(path);
} else {
// Mark the addition in the added set.
delta.added.add(path);
}
}
// Diff dependencies (1/3): remove dependencies that have changed or been removed.
let dependenciesRemoved = false;
for (const [key, prevDependency] of previousDependencies) {
const curDependency = currentDependencies.get(key);
if (
!curDependency ||
!dependenciesEqual(prevDependency, curDependency, options)
) {
dependenciesRemoved = true;
this._removeDependency(nextModule, key, prevDependency, delta, options);
}
}
// Diff dependencies (2/3): add dependencies that have changed or been added.
let dependenciesAdded = false;
if (!commitOptions.onlyRemove) {
for (const [key, curDependency] of currentDependencies) {
const prevDependency = previousDependencies.get(key);
if (
!prevDependency ||
!dependenciesEqual(prevDependency, curDependency, options)
) {
dependenciesAdded = true;
this._addDependency(
nextModule,
key,
curDependency,
resolvedContexts.get(key),
delta,
options,
);
}
}
}
// Diff dependencies (3/3): detect changes in the ordering of dependency
// keys, which must be committed even if no other changes were made.
const previousDependencyKeys = [...previousDependencies.keys()];
const dependencyKeysChangedOrReordered =
currentDependencies.size !== previousDependencies.size ||
[...currentDependencies.keys()].some(
(currentKey, index) => currentKey !== previousDependencyKeys[index],
);
if (
previousModule != null &&
!transformOutputMayDiffer(previousModule, nextModule) &&
!dependenciesRemoved &&
!dependenciesAdded &&
!dependencyKeysChangedOrReordered
) {
// We have not operated on nextModule, so restore previousModule
// to aid diffing. Don't add this path to delta.touched.
this.dependencies.set(previousModule.path, previousModule);
return previousModule;
}
delta.touched.add(path);
// Replace dependencies with the correctly-ordered version, matching the
// transform output. Because this assignment does not add or remove edges,
// it does NOT invalidate any of the garbage collection state.
// A subtractive pass only partially commits modules, so our dependencies
// are not generally complete yet. We'll address ordering in the next pass
// after processing additions.
if (commitOptions.onlyRemove) {
return nextModule;
}
// Catch obvious errors with a cheap assertion.
invariant(
nextModule.dependencies.size === currentDependencies.size,
'Failed to add the correct dependencies',
);
nextModule.dependencies = new Map(currentDependencies);
return nextModule;
}
_addDependency(
parentModule: Module<T>,
key: string,
dependency: Dependency,
requireContext: ?RequireContext,
delta: Delta<T>,
options: InternalOptions<T>,
): void {
const path = dependency.absolutePath;
// The module may already exist, in which case we just need to update some
// bookkeeping instead of adding a new node to the graph.
let module = this.dependencies.get(path);
if (options.shallow) {
// Don't add a node for the module if the graph is shallow (single-module).
} else if (dependency.data.data.asyncType === 'weak') {
// Exclude weak dependencies from the bundle.
} else if (options.lazy && dependency.data.data.asyncType != null) {
// Don't add a node for the module if we are traversing async dependencies
// lazily (and this is an async dependency). Instead, record it in
// importBundleNodes.
this._incrementImportBundleReference(dependency, parentModule);
} else {
if (!module) {
try {
module = this._recursivelyCommitModule(path, delta, options);
} catch (error) {
// If we couldn't add this module but it was added to the graph
// before failing on a sub-dependency, it may be orphaned. Mark it as
// a possible garbage root.
const module = this.dependencies.get(path);
if (module) {
if (module.inverseDependencies.size > 0) {
this._markAsPossibleCycleRoot(module);
} else {
this._releaseModule(module, delta, options);
}
}
throw error;
}
}
// We either added a new node to the graph, or we're updating an existing one.
module.inverseDependencies.add(parentModule.path);
this._markModuleInUse(module);
}
if (requireContext) {
this.#resolvedContexts.set(path, requireContext);
} else {
// This dependency may have existed previously as a require.context -
// clean it up.
this.#resolvedContexts.delete(path);
}
// Update the parent's dependency map unless we failed to add a dependency.
// This means the parent's dependencies can get desynced from
// inverseDependencies and the other fields in the case of lazy edges.
// Not an optimal representation :(
parentModule.dependencies.set(key, dependency);
}
_removeDependency(
parentModule: Module<T>,
key: string,
dependency: Dependency,
delta: Delta<T>,
options: InternalOptions<T>,
): void {
parentModule.dependencies.delete(key);
const {absolutePath} = dependency;
if (dependency.data.data.asyncType === 'weak') {
// Weak dependencies are excluded from the bundle.
return;
}
const module = this.dependencies.get(absolutePath);
if (options.lazy && dependency.data.data.asyncType != null) {
this._decrementImportBundleReference(dependency, parentModule);
} else if (module) {
// Decrement inverseDependencies only if the dependency is not async,
// mirroring the increment conditions in _addDependency.
module.inverseDependencies.delete(parentModule.path);
}
if (!module) {
return;
}
if (
module.inverseDependencies.size > 0 ||
this.entryPoints.has(absolutePath)
) {
// The reference count has decreased, but not to zero.
// NOTE: Each entry point implicitly has a refcount of 1.
this._markAsPossibleCycleRoot(module);
} else {
// The reference count has decreased to zero.
this._releaseModule(module, delta, options);
}
}
/**
* Collect a list of context modules which include a given file.
*/
markModifiedContextModules(
filePath: string,
modifiedPaths: Set<string> | CountingSet<string>,
) {
for (const [absolutePath, context] of this.#resolvedContexts) {
if (
!modifiedPaths.has(absolutePath) &&
fileMatchesContext(filePath, context)
) {
modifiedPaths.add(absolutePath);
}
}
}
/**
* Gets the list of modules affected by the deletion of a given file. The
* caller is expected to mark these modules as modified in the next call to
* traverseDependencies. Note that the list may contain duplicates.
*/
*getModifiedModulesForDeletedPath(filePath: string): Iterable<string> {
yield* this.dependencies.get(filePath)?.inverseDependencies ?? [];
yield* this.#importBundleNodes.get(filePath)?.inverseDependencies ?? [];
}
/**
* Re-traverse the dependency graph in DFS order to reorder the modules and
* guarantee the same order between runs. This method mutates the passed graph.
*/
reorderGraph(options: {shallow: boolean, ...}): void {
const orderedDependencies = new Map<string, Module<T>>();
this.entryPoints.forEach((entryPoint: string) => {
const mainModule = this.dependencies.get(entryPoint);
if (!mainModule) {
throw new ReferenceError(
'Module not registered in graph: ' + entryPoint,
);
}
this._reorderDependencies(mainModule, orderedDependencies, options);
});
this.dependencies.clear();
for (const [key, dep] of orderedDependencies) {
this.dependencies.set(key, dep);
}
}
_reorderDependencies(
module: Module<T>,
orderedDependencies: Map<string, Module<T>>,
options: {shallow: boolean, ...},
): void {
if (module.path) {
if (orderedDependencies.has(module.path)) {
return;
}
orderedDependencies.set(module.path, module);
}
module.dependencies.forEach((dependency: Dependency) => {
const path = dependency.absolutePath;
const childModule = this.dependencies.get(path);
if (!childModule) {
if (dependency.data.data.asyncType != null || options.shallow) {
return;
} else {
throw new ReferenceError('Module not registered in graph: ' + path);
}
}
this._reorderDependencies(childModule, orderedDependencies, options);
});
}
/** Garbage collection functions */
// Add an entry to importBundleNodes (or record an inverse dependency of an existing one)
_incrementImportBundleReference(
dependency: Dependency,
parentModule: Module<T>,
) {
const {absolutePath} = dependency;
const importBundleNode = this.#importBundleNodes.get(absolutePath) ?? {
inverseDependencies: new CountingSet(),
};
importBundleNode.inverseDependencies.add(parentModule.path);
this.#importBundleNodes.set(absolutePath, importBundleNode);
}
// Decrease the reference count of an entry in importBundleNodes (and delete it if necessary)
_decrementImportBundleReference(
dependency: Dependency,
parentModule: Module<T>,
) {
const {absolutePath} = dependency;
const importBundleNode = nullthrows(
this.#importBundleNodes.get(absolutePath),
);
invariant(
importBundleNode.inverseDependencies.has(parentModule.path),
'lazy: import bundle inverse references',
);
importBundleNode.inverseDependencies.delete(parentModule.path);
if (importBundleNode.inverseDependencies.size === 0) {
this.#importBundleNodes.delete(absolutePath);
}
}
// Mark a module as in use (ref count >= 1)
_markModuleInUse(module: Module<T>) {
this.#gc.color.set(module.path, 'black');
}
// Iterate "children" of the given module - i.e. non-weak / async
// dependencies having a corresponding inverse dependency.
*_children(
module: Module<T>,
options: InternalOptions<T>,
): Iterator<Module<T>> {
for (const dependency of module.dependencies.values()) {
if (isWeakOrLazy(dependency, options)) {
continue;
}
yield nullthrows(this.dependencies.get(dependency.absolutePath));
}
}
_moduleSnapshot(module: Module<T>): ModuleData<T> {
const {dependencies, getSource, output, unstable_transformResultKey} =
module;
const resolvedContexts: Map<string, RequireContext> = new Map();
for (const [key, dependency] of dependencies) {
const resolvedContext = this.#resolvedContexts.get(
dependency.absolutePath,
);
if (resolvedContext != null) {
resolvedContexts.set(key, resolvedContext);
}
}
return {
dependencies: new Map(dependencies),
resolvedContexts,
getSource,
output,
unstable_transformResultKey,
};
}
// Delete an unreachable module (and its outbound edges) from the graph
// immediately.
// Called when the reference count of a module has reached 0.
_releaseModule(
module: Module<T>,
delta: Delta<T>,
options: InternalOptions<T>,
) {
if (
!delta.updatedModuleData.has(module.path) &&
!delta.baseModuleData.has(module.path)
) {
// Before releasing a module, take a snapshot of the data we might need
// to reintroduce it to the graph later in this commit. As it is not
// already present in updatedModuleData we can infer it has not been modified,
// so the transform output and dependencies we copy here are current.
delta.baseModuleData.set(module.path, this._moduleSnapshot(module));
}
for (const [key, dependency] of module.dependencies) {
this._removeDependency(module, key, dependency, delta, options);
}
this.#gc.color.set(module.path, 'black');
this._freeModule(module, delta);
}
// Delete an unreachable module from the graph.
_freeModule(module: Module<T>, delta: Delta<T>) {
if (delta.added.has(module.path)) {
// Mark the deletion by clearing a prior addition.
delta.added.delete(module.path);
} else {
// Mark the deletion in the deleted set.
delta.deleted.add(module.path);
}
// This module is not used anywhere else! We can clear it from the bundle.
// Clean up all the state associated with this module in order to correctly
// re-add it if we encounter it again.
this.dependencies.delete(module.path);
this.#gc.possibleCycleRoots.delete(module.path);
this.#gc.color.delete(module.path);
this.#resolvedContexts.delete(module.path);
}
// Mark a module as a possible cycle root
_markAsPossibleCycleRoot(module: Module<T>) {
if (this.#gc.color.get(module.path) !== 'purple') {
this.#gc.color.set(module.path, 'purple');
this.#gc.possibleCycleRoots.add(module.path);
}
}
// Collect any unreachable cycles in the graph.
_collectCycles(delta: Delta<T>, options: InternalOptions<T>) {
// Mark recursively from roots (trial deletion)
for (const path of this.#gc.possibleCycleRoots) {
const module = nullthrows(this.dependencies.get(path));
const color = nullthrows(this.#gc.color.get(path));
if (color === 'purple') {
this._markGray(module, options);
} else {
this.#gc.possibleCycleRoots.delete(path);
if (
color === 'black' &&
module.inverseDependencies.size === 0 &&
!this.entryPoints.has(path)
) {
this._freeModule(module, delta);
}
}
}
// Scan recursively from roots (undo unsuccessful trial deletions)
for (const path of this.#gc.possibleCycleRoots) {
const module = nullthrows(this.dependencies.get(path));
this._scan(module, options);
}
// Collect recursively from roots (free unreachable cycles)
for (const path of this.#gc.possibleCycleRoots) {
this.#gc.possibleCycleRoots.delete(path);
const module = nullthrows(this.dependencies.get(path));
this._collectWhite(module, delta);
}
}
_markGray(module: Module<T>, options: InternalOptions<T>) {
const color = nullthrows(this.#gc.color.get(module.path));
if (color !== 'gray') {
this.#gc.color.set(module.path, 'gray');
for (const childModule of this._children(module, options)) {
// The inverse dependency will be restored during the scan phase if this module remains live.
childModule.inverseDependencies.delete(module.path);
this._markGray(childModule, options);
}
}
}
_scan(module: Module<T>, options: InternalOptions<T>) {
const color = nullthrows(this.#gc.color.get(module.path));
if (color === 'gray') {
if (
module.inverseDependencies.size > 0 ||
this.entryPoints.has(module.path)
) {
this._scanBlack(module, options);
} else {
this.#gc.color.set(module.path, 'white');
for (const childModule of this._children(module, options)) {
this._scan(childModule, options);
}
}
}
}
_scanBlack(module: Module<T>, options: InternalOptions<T>) {
this.#gc.color.set(module.path, 'black');
for (const childModule of this._children(module, options)) {
// The inverse dependency must have been deleted during the mark phase.
childModule.inverseDependencies.add(module.path);
const childColor = nullthrows(this.#gc.color.get(childModule.path));
if (childColor !== 'black') {
this._scanBlack(childModule, options);
}
}
}
_collectWhite(module: Module<T>, delta: Delta<T>) {
const color = nullthrows(this.#gc.color.get(module.path));
if (color === 'white' && !this.#gc.possibleCycleRoots.has(module.path)) {
this.#gc.color.set(module.path, 'black');
for (const dependency of module.dependencies.values()) {
const childModule = this.dependencies.get(dependency.absolutePath);
// The child may already have been collected.
if (childModule) {
this._collectWhite(childModule, delta);
}
}
this._freeModule(module, delta);
}
}
/** End of garbage collection functions */
}
function dependenciesEqual(
a: Dependency,
b: Dependency,
options: $ReadOnly<{lazy: boolean, ...}>,
): boolean {
return (
a === b ||
(a.absolutePath === b.absolutePath &&
(!options.lazy || a.data.data.asyncType === b.data.data.asyncType) &&
contextParamsEqual(a.data.data.contextParams, b.data.data.contextParams))
);
}
function contextParamsEqual(
a: ?RequireContextParams,
b: ?RequireContextParams,
): boolean {
return (
a === b ||
(a == null && b == null) ||
(a != null &&
b != null &&
a.recursive === b.recursive &&
a.filter.pattern === b.filter.pattern &&
a.filter.flags === b.filter.flags &&
a.mode === b.mode)
);
}
function transformOutputMayDiffer<T>(a: Module<T>, b: Module<T>): boolean {
return (
a.unstable_transformResultKey == null ||
a.unstable_transformResultKey !== b.unstable_transformResultKey
);
}

View File

@@ -0,0 +1,53 @@
"use strict";
const getAppendScripts = require("../../lib/getAppendScripts");
const processModules = require("./helpers/processModules");
function baseJSBundle(entryPoint, preModules, graph, options) {
for (const module of graph.dependencies.values()) {
options.createModuleId(module.path);
}
const processModulesOptions = {
filter: options.processModuleFilter,
createModuleId: options.createModuleId,
dev: options.dev,
includeAsyncPaths: options.includeAsyncPaths,
projectRoot: options.projectRoot,
serverRoot: options.serverRoot,
sourceUrl: options.sourceUrl,
};
if (options.modulesOnly) {
preModules = [];
}
const preCode = processModules(preModules, processModulesOptions)
.map(([_, code]) => code)
.join("\n");
const modules = [...graph.dependencies.values()].sort(
(a, b) => options.createModuleId(a.path) - options.createModuleId(b.path)
);
const postCode = processModules(
getAppendScripts(entryPoint, [...preModules, ...modules], {
asyncRequireModulePath: options.asyncRequireModulePath,
createModuleId: options.createModuleId,
getRunModuleStatement: options.getRunModuleStatement,
inlineSourceMap: options.inlineSourceMap,
runBeforeMainModule: options.runBeforeMainModule,
runModule: options.runModule,
shouldAddToIgnoreList: options.shouldAddToIgnoreList,
sourceMapUrl: options.sourceMapUrl,
sourceUrl: options.sourceUrl,
getSourceUrl: options.getSourceUrl,
}),
processModulesOptions
)
.map(([_, code]) => code)
.join("\n");
return {
pre: preCode,
post: postCode,
modules: processModules(
[...graph.dependencies.values()],
processModulesOptions
).map(([module, code]) => [options.createModuleId(module.path), code]),
};
}
module.exports = baseJSBundle;

View File

@@ -0,0 +1,87 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {
MixedOutput,
Module,
ReadOnlyGraph,
SerializerOptions,
} from '../types.flow';
import type {Bundle} from 'metro-runtime/src/modules/types.flow';
const getAppendScripts = require('../../lib/getAppendScripts');
const processModules = require('./helpers/processModules');
function baseJSBundle(
entryPoint: string,
preModules: $ReadOnlyArray<Module<>>,
graph: ReadOnlyGraph<>,
options: SerializerOptions,
): Bundle {
for (const module of graph.dependencies.values()) {
options.createModuleId(module.path);
}
const processModulesOptions = {
filter: options.processModuleFilter,
createModuleId: options.createModuleId,
dev: options.dev,
includeAsyncPaths: options.includeAsyncPaths,
projectRoot: options.projectRoot,
serverRoot: options.serverRoot,
sourceUrl: options.sourceUrl,
};
// Do not prepend polyfills or the require runtime when only modules are requested
if (options.modulesOnly) {
preModules = [];
}
const preCode = processModules(preModules, processModulesOptions)
.map(([_, code]) => code)
.join('\n');
const modules = [...graph.dependencies.values()].sort(
(a: Module<MixedOutput>, b: Module<MixedOutput>) =>
options.createModuleId(a.path) - options.createModuleId(b.path),
);
const postCode = processModules(
getAppendScripts(entryPoint, [...preModules, ...modules], {
asyncRequireModulePath: options.asyncRequireModulePath,
createModuleId: options.createModuleId,
getRunModuleStatement: options.getRunModuleStatement,
inlineSourceMap: options.inlineSourceMap,
runBeforeMainModule: options.runBeforeMainModule,
runModule: options.runModule,
shouldAddToIgnoreList: options.shouldAddToIgnoreList,
sourceMapUrl: options.sourceMapUrl,
sourceUrl: options.sourceUrl,
getSourceUrl: options.getSourceUrl,
}),
processModulesOptions,
)
.map(([_, code]) => code)
.join('\n');
return {
pre: preCode,
post: postCode,
modules: processModules(
[...graph.dependencies.values()],
processModulesOptions,
).map(([module, code]) => [options.createModuleId(module.path), code]),
};
}
module.exports = baseJSBundle;

View File

@@ -0,0 +1,31 @@
"use strict";
const { getAssetFiles } = require("../../Assets");
const { getJsOutput, isJsModule } = require("./helpers/js");
async function getAllFiles(pre, graph, options) {
const modules = graph.dependencies;
const { processModuleFilter } = options;
const promises = [];
for (const module of pre) {
if (processModuleFilter(module)) {
promises.push([module.path]);
}
}
for (const module of modules.values()) {
if (!isJsModule(module) || !processModuleFilter(module)) {
continue;
}
if (getJsOutput(module).type === "js/module/asset") {
promises.push(getAssetFiles(module.path, options.platform));
} else {
promises.push([module.path]);
}
}
const dependencies = await Promise.all(promises);
const output = [];
for (const dependencyArray of dependencies) {
output.push(...dependencyArray);
}
return output;
}
module.exports = getAllFiles;

View File

@@ -0,0 +1,62 @@
/**
* 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} from '../types.flow';
const {getAssetFiles} = require('../../Assets');
const {getJsOutput, isJsModule} = require('./helpers/js');
type Options = {
platform: ?string,
+processModuleFilter: (module: Module<>) => boolean,
};
async function getAllFiles(
pre: $ReadOnlyArray<Module<>>,
graph: ReadOnlyGraph<>,
options: Options,
): Promise<$ReadOnlyArray<string>> {
const modules = graph.dependencies;
const {processModuleFilter} = options;
const promises: Array<Promise<Array<string>> | Array<string>> = [];
for (const module of pre) {
if (processModuleFilter(module)) {
promises.push([module.path]);
}
}
for (const module of modules.values()) {
if (!isJsModule(module) || !processModuleFilter(module)) {
continue;
}
if (getJsOutput(module).type === 'js/module/asset') {
promises.push(getAssetFiles(module.path, options.platform));
} else {
promises.push([module.path]);
}
}
const dependencies = await Promise.all(promises);
const output: Array<string> = [];
for (const dependencyArray of dependencies) {
output.push(...dependencyArray);
}
return output;
}
module.exports = getAllFiles;

View File

@@ -0,0 +1,29 @@
"use strict";
const { getAssetData } = require("../../Assets");
const { getJsOutput, isJsModule } = require("./helpers/js");
const path = require("path");
async function getAssets(dependencies, options) {
const promises = [];
const { processModuleFilter } = options;
for (const module of dependencies.values()) {
if (
isJsModule(module) &&
processModuleFilter(module) &&
getJsOutput(module).type === "js/module/asset" &&
path.relative(options.projectRoot, module.path) !== "package.json"
) {
promises.push(
getAssetData(
module.path,
path.relative(options.projectRoot, module.path),
options.assetPlugins,
options.platform,
options.publicPath
)
);
}
}
return await Promise.all(promises);
}
module.exports = getAssets;

View File

@@ -0,0 +1,58 @@
/**
* 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 {AssetData} from '../../Assets';
import type {Module, ReadOnlyDependencies} from '../types.flow';
const {getAssetData} = require('../../Assets');
const {getJsOutput, isJsModule} = require('./helpers/js');
const path = require('path');
type Options = {
+processModuleFilter: (module: Module<>) => boolean,
assetPlugins: $ReadOnlyArray<string>,
platform: ?string,
projectRoot: string,
publicPath: string,
};
async function getAssets(
dependencies: ReadOnlyDependencies<>,
options: Options,
): Promise<$ReadOnlyArray<AssetData>> {
const promises = [];
const {processModuleFilter} = options;
for (const module of dependencies.values()) {
if (
isJsModule(module) &&
processModuleFilter(module) &&
getJsOutput(module).type === 'js/module/asset' &&
path.relative(options.projectRoot, module.path) !== 'package.json'
) {
promises.push(
getAssetData(
module.path,
path.relative(options.projectRoot, module.path),
options.assetPlugins,
options.platform,
options.publicPath,
),
);
}
}
return await Promise.all(promises);
}
module.exports = getAssets;

View File

@@ -0,0 +1,25 @@
"use strict";
const { getJsOutput, isJsModule } = require("./helpers/js");
function getExplodedSourceMap(modules, options) {
const modulesToProcess = modules
.filter(isJsModule)
.filter(options.processModuleFilter);
const result = [];
let firstLine1Based = 1;
for (const module of modulesToProcess) {
const { path } = module;
const { lineCount, functionMap, map } = getJsOutput(module).data;
result.push({
firstLine1Based,
functionMap,
path,
map,
});
firstLine1Based += lineCount;
}
return result;
}
module.exports = {
getExplodedSourceMap,
};

View File

@@ -0,0 +1,53 @@
/**
* 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} from '../types.flow';
import type {
FBSourceFunctionMap,
MetroSourceMapSegmentTuple,
} from 'metro-source-map';
const {getJsOutput, isJsModule} = require('./helpers/js');
export type ExplodedSourceMap = $ReadOnlyArray<{
+map: Array<MetroSourceMapSegmentTuple>,
+firstLine1Based: number,
+functionMap: ?FBSourceFunctionMap,
+path: string,
}>;
function getExplodedSourceMap(
modules: $ReadOnlyArray<Module<>>,
options: {
+processModuleFilter: (module: Module<>) => boolean,
},
): ExplodedSourceMap {
const modulesToProcess = modules
.filter(isJsModule)
.filter(options.processModuleFilter);
const result = [];
let firstLine1Based = 1;
for (const module of modulesToProcess) {
const {path} = module;
const {lineCount, functionMap, map} = getJsOutput(module).data;
result.push({firstLine1Based, functionMap, path, map});
firstLine1Based += lineCount;
}
return result;
}
module.exports = {
getExplodedSourceMap,
};

View File

@@ -0,0 +1,18 @@
/**
* 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
*/
import {ModuleTransportLike} from '../../shared/types';
export interface RamBundleInfo {
getDependencies: (filePath: string) => Set<string>;
startupModules: Readonly<ModuleTransportLike>;
lazyModules: Readonly<ModuleTransportLike>;
groups: Map<number, Set<number>>;
}

View File

@@ -0,0 +1,104 @@
"use strict";
const { createRamBundleGroups } = require("../../Bundler/util");
const getAppendScripts = require("../../lib/getAppendScripts");
const getTransitiveDependencies = require("./helpers/getTransitiveDependencies");
const { isJsModule, wrapModule } = require("./helpers/js");
const { sourceMapObject } = require("./sourceMapObject");
const nullthrows = require("nullthrows");
const path = require("path");
async function getRamBundleInfo(entryPoint, pre, graph, options) {
let modules = [...pre, ...graph.dependencies.values()];
modules = modules.concat(getAppendScripts(entryPoint, modules, options));
modules.forEach((module) => options.createModuleId(module.path));
const ramModules = modules
.filter(isJsModule)
.filter(options.processModuleFilter)
.map((module) => ({
id: options.createModuleId(module.path),
code: wrapModule(module, options),
map: sourceMapObject([module], {
excludeSource: options.excludeSource,
processModuleFilter: options.processModuleFilter,
shouldAddToIgnoreList: options.shouldAddToIgnoreList,
getSourceUrl: options.getSourceUrl,
}),
name: path.basename(module.path),
sourcePath: module.path,
source: module.getSource().toString(),
type: nullthrows(module.output.find(({ type }) => type.startsWith("js")))
.type,
}));
const { preloadedModules, ramGroups } = await _getRamOptions(
entryPoint,
{
dev: options.dev,
platform: options.platform,
},
(filePath) => getTransitiveDependencies(filePath, graph),
options.getTransformOptions
);
const startupModules = [];
const lazyModules = [];
ramModules.forEach((module) => {
if (preloadedModules.hasOwnProperty(module.sourcePath)) {
startupModules.push(module);
return;
}
if (module.type.startsWith("js/script")) {
startupModules.push(module);
return;
}
if (module.type.startsWith("js/module")) {
lazyModules.push(module);
}
});
const groups = createRamBundleGroups(
ramGroups,
lazyModules,
(module, dependenciesByPath) => {
const deps = getTransitiveDependencies(module.sourcePath, graph);
const output = new Set();
for (const dependency of deps) {
const module = dependenciesByPath.get(dependency);
if (module) {
output.add(module.id);
}
}
return output;
}
);
return {
getDependencies: (filePath) => getTransitiveDependencies(filePath, graph),
groups,
lazyModules,
startupModules,
};
}
async function _getRamOptions(
entryFile,
options,
getDependencies,
getTransformOptions
) {
if (getTransformOptions == null) {
return {
preloadedModules: {},
ramGroups: [],
};
}
const { preloadedModules, ramGroups } = await getTransformOptions(
[entryFile],
{
dev: options.dev,
hot: true,
platform: options.platform,
},
async (x) => Array.from(getDependencies)
);
return {
preloadedModules: preloadedModules || {},
ramGroups: ramGroups || [],
};
}
module.exports = getRamBundleInfo;

View File

@@ -0,0 +1,175 @@
/**
* 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 {
ModuleTransportLike,
RamModuleTransport,
} from '../../shared/types.flow';
import type {Module, ReadOnlyGraph, SerializerOptions} from '../types.flow';
import type {SourceMapGeneratorOptions} from './sourceMapGenerator';
import type {GetTransformOptions} from 'metro-config/src/configTypes.flow.js';
const {createRamBundleGroups} = require('../../Bundler/util');
const getAppendScripts = require('../../lib/getAppendScripts');
const getTransitiveDependencies = require('./helpers/getTransitiveDependencies');
const {isJsModule, wrapModule} = require('./helpers/js');
const {sourceMapObject} = require('./sourceMapObject');
const nullthrows = require('nullthrows');
const path = require('path');
type Options = $ReadOnly<{
...SerializerOptions,
...SourceMapGeneratorOptions,
getTransformOptions: ?GetTransformOptions,
platform: ?string,
}>;
export type RamBundleInfo = {
getDependencies: string => Set<string>,
startupModules: $ReadOnlyArray<ModuleTransportLike>,
lazyModules: $ReadOnlyArray<ModuleTransportLike>,
groups: Map<number, Set<number>>,
};
async function getRamBundleInfo(
entryPoint: string,
pre: $ReadOnlyArray<Module<>>,
graph: ReadOnlyGraph<>,
options: Options,
): Promise<RamBundleInfo> {
let modules: $ReadOnlyArray<Module<>> = [
...pre,
...graph.dependencies.values(),
];
modules = modules.concat(getAppendScripts(entryPoint, modules, options));
modules.forEach((module: Module<>) => options.createModuleId(module.path));
const ramModules: Array<RamModuleTransport> = modules
.filter(isJsModule)
.filter(options.processModuleFilter)
.map((module: Module<>): RamModuleTransport => ({
id: options.createModuleId(module.path),
code: wrapModule(module, options),
map: sourceMapObject([module], {
excludeSource: options.excludeSource,
processModuleFilter: options.processModuleFilter,
shouldAddToIgnoreList: options.shouldAddToIgnoreList,
getSourceUrl: options.getSourceUrl,
}),
name: path.basename(module.path),
sourcePath: module.path,
source: module.getSource().toString(),
type: nullthrows(module.output.find(({type}) => type.startsWith('js')))
.type,
}));
const {preloadedModules, ramGroups} = await _getRamOptions(
entryPoint,
{
dev: options.dev,
platform: options.platform,
},
(filePath: string) => getTransitiveDependencies(filePath, graph),
options.getTransformOptions,
);
const startupModules = [];
const lazyModules = [];
ramModules.forEach((module: RamModuleTransport) => {
if (preloadedModules.hasOwnProperty(module.sourcePath)) {
startupModules.push(module);
return;
}
if (module.type.startsWith('js/script')) {
startupModules.push(module);
return;
}
if (module.type.startsWith('js/module')) {
lazyModules.push(module);
}
});
const groups = createRamBundleGroups(
ramGroups,
lazyModules,
(
module: ModuleTransportLike,
dependenciesByPath: Map<string, ModuleTransportLike>,
): Set<number> => {
const deps = getTransitiveDependencies(module.sourcePath, graph);
const output = new Set<number>();
for (const dependency of deps) {
const module = dependenciesByPath.get(dependency);
if (module) {
output.add(module.id);
}
}
return output;
},
);
return {
getDependencies: (filePath: string): Set<string> =>
getTransitiveDependencies(filePath, graph),
groups,
lazyModules,
startupModules,
};
}
/**
* Returns the options needed to create a RAM bundle.
*/
async function _getRamOptions(
entryFile: string,
options: {
dev: boolean,
platform: ?string,
...
},
getDependencies: string => Iterable<string>,
getTransformOptions: ?GetTransformOptions,
): Promise<{
+preloadedModules: {[string]: true, ...},
+ramGroups: Array<string>,
}> {
if (getTransformOptions == null) {
return {
preloadedModules: {},
ramGroups: [],
};
}
const {preloadedModules, ramGroups} = await getTransformOptions(
[entryFile],
{dev: options.dev, hot: true, platform: options.platform},
/* $FlowFixMe(>=0.99.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.99 was deployed. To see the error, delete this
* comment and run Flow. */
async (x: string) => Array.from(getDependencies),
);
return {
preloadedModules: preloadedModules || {},
ramGroups: ramGroups || [],
};
}
module.exports = getRamBundleInfo;

View File

@@ -0,0 +1,7 @@
"use strict";
function getInlineSourceMappingURL(sourceMap) {
const base64 = Buffer.from(sourceMap).toString("base64");
return `data:application/json;charset=utf-8;base64,${base64}`;
}
module.exports = getInlineSourceMappingURL;

View File

@@ -0,0 +1,19 @@
/**
* 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';
function getInlineSourceMappingURL(sourceMap: string): string {
const base64 = Buffer.from(sourceMap).toString('base64');
return `data:application/json;charset=utf-8;base64,${base64}`;
}
module.exports = getInlineSourceMappingURL;

View File

@@ -0,0 +1,18 @@
"use strict";
const { getJsOutput } = require("./js");
function getSourceMapInfo(module, options) {
return {
...getJsOutput(module).data,
isIgnored: options.shouldAddToIgnoreList(module),
path: options?.getSourceUrl?.(module) ?? module.path,
source: options.excludeSource ? "" : getModuleSource(module),
};
}
function getModuleSource(module) {
if (getJsOutput(module).type === "js/module/asset") {
return "";
}
return module.getSource().toString();
}
module.exports = getSourceMapInfo;

View File

@@ -0,0 +1,54 @@
/**
* 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} from '../../types.flow';
import type {
FBSourceFunctionMap,
MetroSourceMapSegmentTuple,
} from 'metro-source-map';
const {getJsOutput} = require('./js');
function getSourceMapInfo(
module: Module<>,
options: {
+excludeSource: boolean,
+shouldAddToIgnoreList: (Module<>) => boolean,
getSourceUrl: ?(module: Module<>) => string,
},
): {
+map: Array<MetroSourceMapSegmentTuple>,
+functionMap: ?FBSourceFunctionMap,
+code: string,
+path: string,
+source: string,
+lineCount: number,
+isIgnored: boolean,
} {
return {
...getJsOutput(module).data,
isIgnored: options.shouldAddToIgnoreList(module),
path: options?.getSourceUrl?.(module) ?? module.path,
source: options.excludeSource ? '' : getModuleSource(module),
};
}
function getModuleSource(module: Module<>): string {
if (getJsOutput(module).type === 'js/module/asset') {
return '';
}
return module.getSource().toString();
}
module.exports = getSourceMapInfo;

View File

@@ -0,0 +1,22 @@
"use strict";
function getTransitiveDependencies(path, graph) {
const dependencies = _getDeps(path, graph, new Set());
dependencies.delete(path);
return dependencies;
}
function _getDeps(path, graph, deps) {
if (deps.has(path)) {
return deps;
}
const module = graph.dependencies.get(path);
if (!module) {
return deps;
}
deps.add(path);
for (const dependency of module.dependencies.values()) {
_getDeps(dependency.absolutePath, graph, deps);
}
return deps;
}
module.exports = getTransitiveDependencies;

View File

@@ -0,0 +1,53 @@
/**
* 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 {ReadOnlyGraph} from '../../types.flow';
function getTransitiveDependencies<T>(
path: string,
graph: ReadOnlyGraph<T>,
): Set<string> {
const dependencies = _getDeps(path, graph, new Set());
// Remove the main entry point, since this method only returns the
// dependencies.
dependencies.delete(path);
return dependencies;
}
function _getDeps<T>(
path: string,
graph: ReadOnlyGraph<T>,
deps: Set<string>,
): Set<string> {
if (deps.has(path)) {
return deps;
}
const module = graph.dependencies.get(path);
if (!module) {
return deps;
}
deps.add(path);
for (const dependency of module.dependencies.values()) {
_getDeps(dependency.absolutePath, graph, deps);
}
return deps;
}
module.exports = getTransitiveDependencies;

View File

@@ -0,0 +1,91 @@
"use strict";
const invariant = require("invariant");
const jscSafeUrl = require("jsc-safe-url");
const { addParamsToDefineCall } = require("metro-transform-plugins");
const path = require("path");
function wrapModule(module, options) {
const output = getJsOutput(module);
if (output.type.startsWith("js/script")) {
return output.data.code;
}
const params = getModuleParams(module, options);
return addParamsToDefineCall(output.data.code, ...params);
}
function getModuleParams(module, options) {
const moduleId = options.createModuleId(module.path);
const paths = {};
let hasPaths = false;
const dependencyMapArray = Array.from(module.dependencies.values()).map(
(dependency) => {
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"
);
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(
path.dirname(bundlePath),
path.basename(bundlePath, path.extname(bundlePath))
) +
".bundle?" +
searchParams.toString();
}
return id;
}
);
const params = [
moduleId,
hasPaths
? {
...dependencyMapArray,
paths,
}
: dependencyMapArray,
];
if (options.dev) {
params.push(path.relative(options.projectRoot, module.path));
}
return params;
}
function getJsOutput(module) {
const jsModules = module.output.filter(({ type }) => type.startsWith("js/"));
invariant(
jsModules.length === 1,
`Modules must have exactly one JS output, but ${
module.path ?? "unknown module"
} has ${jsModules.length} JS outputs.`
);
const jsOutput = jsModules[0];
invariant(
Number.isFinite(jsOutput.data.lineCount),
`JS output must populate lineCount, but ${
module.path ?? "unknown module"
} has ${jsOutput.type} output with lineCount '${jsOutput.data.lineCount}'`
);
return jsOutput;
}
function isJsModule(module) {
return module.output.filter(isJsOutput).length > 0;
}
function isJsOutput(output) {
return output.type.startsWith("js/");
}
module.exports = {
getJsOutput,
getModuleParams,
isJsModule,
wrapModule,
};

View File

@@ -0,0 +1,148 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {MixedOutput, Module} from '../../types.flow';
import type {JsOutput} from 'metro-transform-worker';
const invariant = require('invariant');
const jscSafeUrl = require('jsc-safe-url');
const {addParamsToDefineCall} = require('metro-transform-plugins');
const path = require('path');
export type Options = $ReadOnly<{
createModuleId: string => number | string,
dev: boolean,
includeAsyncPaths: boolean,
projectRoot: string,
serverRoot: string,
sourceUrl: ?string,
...
}>;
function wrapModule(module: Module<>, options: Options): string {
const output = getJsOutput(module);
if (output.type.startsWith('js/script')) {
return output.data.code;
}
const params = getModuleParams(module, options);
return addParamsToDefineCall(output.data.code, ...params);
}
function getModuleParams(module: Module<>, options: Options): Array<mixed> {
const moduleId = options.createModuleId(module.path);
const paths: {[moduleID: number | string]: mixed} = {};
let hasPaths = false;
const dependencyMapArray = Array.from(module.dependencies.values()).map(
dependency => {
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(
path.dirname(bundlePath),
// Strip the file extension
path.basename(bundlePath, path.extname(bundlePath)),
) +
'.bundle?' +
searchParams.toString();
}
return id;
},
);
const params = [
moduleId,
hasPaths
? {
// $FlowIgnore[not-an-object] Intentionally spreading an array into an object
...dependencyMapArray,
paths,
}
: dependencyMapArray,
];
if (options.dev) {
// Add the relative path of the module to make debugging easier.
// This is mapped to `module.verboseName` in `require.js`.
params.push(path.relative(options.projectRoot, module.path));
}
return params;
}
function getJsOutput(
module: $ReadOnly<{
output: $ReadOnlyArray<MixedOutput>,
path?: string,
...
}>,
): JsOutput {
const jsModules = module.output.filter(({type}) => type.startsWith('js/'));
invariant(
jsModules.length === 1,
`Modules must have exactly one JS output, but ${
module.path ?? 'unknown module'
} has ${jsModules.length} JS outputs.`,
);
const jsOutput: JsOutput = (jsModules[0]: any);
invariant(
Number.isFinite(jsOutput.data.lineCount),
`JS output must populate lineCount, but ${
module.path ?? 'unknown module'
} has ${jsOutput.type} output with lineCount '${jsOutput.data.lineCount}'`,
);
return jsOutput;
}
function isJsModule(module: Module<>): boolean {
return module.output.filter(isJsOutput).length > 0;
}
function isJsOutput(output: MixedOutput): boolean {
return output.type.startsWith('js/');
}
module.exports = {
getJsOutput,
getModuleParams,
isJsModule,
wrapModule,
};

View File

@@ -0,0 +1,31 @@
"use strict";
const { isJsModule, wrapModule } = require("./js");
function processModules(
modules,
{
filter = () => true,
createModuleId,
dev,
includeAsyncPaths,
projectRoot,
serverRoot,
sourceUrl,
}
) {
return [...modules]
.filter(isJsModule)
.filter(filter)
.map((module) => [
module,
wrapModule(module, {
createModuleId,
dev,
includeAsyncPaths,
projectRoot,
serverRoot,
sourceUrl,
}),
]);
}
module.exports = processModules;

View File

@@ -0,0 +1,54 @@
/**
* 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} from '../../types.flow';
const {isJsModule, wrapModule} = require('./js');
function processModules(
modules: $ReadOnlyArray<Module<>>,
{
filter = () => true,
createModuleId,
dev,
includeAsyncPaths,
projectRoot,
serverRoot,
sourceUrl,
}: $ReadOnly<{
filter?: (module: Module<>) => boolean,
createModuleId: string => number,
dev: boolean,
includeAsyncPaths: boolean,
projectRoot: string,
serverRoot: string,
sourceUrl: ?string,
}>,
): $ReadOnlyArray<[Module<>, string]> {
return [...modules]
.filter(isJsModule)
.filter(filter)
.map((module: Module<>) => [
module,
wrapModule(module, {
createModuleId,
dev,
includeAsyncPaths,
projectRoot,
serverRoot,
sourceUrl,
}),
]);
}
module.exports = processModules;

View File

@@ -0,0 +1,76 @@
"use strict";
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");
function generateModules(sourceModules, graph, options) {
const modules = [];
for (const module of sourceModules) {
if (isJsModule(module)) {
const getURL = (extension) => {
options.clientUrl.pathname = path.relative(
options.serverRoot ?? options.projectRoot,
path.join(
path.dirname(module.path),
path.basename(module.path, path.extname(module.path)) +
"." +
extension
)
);
return url.format(options.clientUrl);
};
const sourceMappingURL = getURL("map");
const sourceURL = jscSafeUrl.toJscSafeUrl(getURL("bundle"));
const code =
prepareModule(module, graph, options) +
`\n//# sourceMappingURL=${sourceMappingURL}\n` +
`//# sourceURL=${sourceURL}\n`;
modules.push({
module: [options.createModuleId(module.path), code],
sourceMappingURL,
sourceURL,
});
}
}
return modules;
}
function prepareModule(module, graph, options) {
const code = wrapModule(module, {
...options,
sourceUrl: url.format(options.clientUrl),
dev: true,
});
const inverseDependencies = getInverseDependencies(module.path, graph);
const inverseDependenciesById = Object.create(null);
Object.keys(inverseDependencies).forEach((path) => {
inverseDependenciesById[options.createModuleId(path)] = inverseDependencies[
path
].map(options.createModuleId);
});
return addParamsToDefineCall(code, inverseDependenciesById);
}
function getInverseDependencies(path, graph, inverseDependencies = {}) {
if (path in inverseDependencies) {
return inverseDependencies;
}
const module = graph.dependencies.get(path);
if (!module) {
return inverseDependencies;
}
inverseDependencies[path] = [];
for (const inverse of module.inverseDependencies) {
inverseDependencies[path].push(inverse);
getInverseDependencies(inverse, graph, inverseDependencies);
}
return inverseDependencies;
}
function hmrJSBundle(delta, graph, options) {
return {
added: generateModules(delta.added.values(), graph, options),
modified: generateModules(delta.modified.values(), graph, options),
deleted: [...delta.deleted].map((path) => options.createModuleId(path)),
};
}
module.exports = hmrJSBundle;

View File

@@ -0,0 +1,146 @@
/**
* 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 {EntryPointURL} from '../../HmrServer';
import type {DeltaResult, Module, ReadOnlyGraph} from '../types.flow';
import type {HmrModule} from 'metro-runtime/src/modules/types.flow';
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,
createModuleId: string => number,
includeAsyncPaths: boolean,
projectRoot: string,
serverRoot: string,
...
}>;
function generateModules(
sourceModules: Iterable<Module<>>,
graph: ReadOnlyGraph<>,
options: Options,
): $ReadOnlyArray<HmrModule> {
const modules = [];
for (const module of sourceModules) {
if (isJsModule(module)) {
// Construct a bundle URL for this specific module only
const getURL = (extension: 'bundle' | 'map') => {
options.clientUrl.pathname = path.relative(
options.serverRoot ?? options.projectRoot,
path.join(
path.dirname(module.path),
path.basename(module.path, path.extname(module.path)) +
'.' +
extension,
),
);
return url.format(options.clientUrl);
};
const sourceMappingURL = getURL('map');
const sourceURL = jscSafeUrl.toJscSafeUrl(getURL('bundle'));
const code =
prepareModule(module, graph, options) +
`\n//# sourceMappingURL=${sourceMappingURL}\n` +
`//# sourceURL=${sourceURL}\n`;
modules.push({
module: [options.createModuleId(module.path), code],
sourceMappingURL,
sourceURL,
});
}
}
return modules;
}
function prepareModule(
module: Module<>,
graph: ReadOnlyGraph<>,
options: Options,
): string {
const code = wrapModule(module, {
...options,
sourceUrl: url.format(options.clientUrl),
dev: true,
});
const inverseDependencies = getInverseDependencies(module.path, graph);
// Transform the inverse dependency paths to ids.
const inverseDependenciesById = Object.create(null);
Object.keys(inverseDependencies).forEach((path: string) => {
// $FlowFixMe[prop-missing]
// $FlowFixMe[invalid-computed-prop]
inverseDependenciesById[options.createModuleId(path)] = inverseDependencies[
path
].map(options.createModuleId);
});
return addParamsToDefineCall(code, inverseDependenciesById);
}
/**
* Instead of adding the whole inverseDependncies object into each changed
* module (which can be really huge if the dependency graph is big), we only
* add the needed inverseDependencies for each changed module (we do this by
* traversing upwards the dependency graph).
*/
function getInverseDependencies(
path: string,
graph: ReadOnlyGraph<>,
inverseDependencies: {[key: string]: Array<string>, ...} = {},
): {[key: string]: Array<string>, ...} {
// Dependency alredy traversed.
if (path in inverseDependencies) {
return inverseDependencies;
}
const module = graph.dependencies.get(path);
if (!module) {
return inverseDependencies;
}
inverseDependencies[path] = [];
for (const inverse of module.inverseDependencies) {
inverseDependencies[path].push(inverse);
getInverseDependencies(inverse, graph, inverseDependencies);
}
return inverseDependencies;
}
function hmrJSBundle(
delta: DeltaResult<>,
graph: ReadOnlyGraph<>,
options: Options,
): {
+added: $ReadOnlyArray<HmrModule>,
+deleted: $ReadOnlyArray<number>,
+modified: $ReadOnlyArray<HmrModule>,
} {
return {
added: generateModules(delta.added.values(), graph, options),
modified: generateModules(delta.modified.values(), graph, options),
deleted: [...delta.deleted].map((path: string) =>
options.createModuleId(path),
),
};
}
module.exports = hmrJSBundle;

View File

@@ -0,0 +1,73 @@
"use strict";
const getSourceMapInfo = require("./helpers/getSourceMapInfo");
const { isJsModule } = require("./helpers/js");
const {
fromRawMappings,
fromRawMappingsNonBlocking,
} = require("metro-source-map");
function getSourceMapInfosImpl(isBlocking, onDone, modules, options) {
const sourceMapInfos = [];
const modulesToProcess = modules
.filter(isJsModule)
.filter(options.processModuleFilter);
function processNextModule() {
if (modulesToProcess.length === 0) {
return true;
}
const mod = modulesToProcess.shift();
const info = getSourceMapInfo(mod, {
excludeSource: options.excludeSource,
shouldAddToIgnoreList: options.shouldAddToIgnoreList,
getSourceUrl: options.getSourceUrl,
});
sourceMapInfos.push(info);
return false;
}
function workLoop() {
const time = process.hrtime();
while (true) {
const isDone = processNextModule();
if (isDone) {
onDone(sourceMapInfos);
break;
}
if (!isBlocking) {
const diff = process.hrtime(time);
const NS_IN_MS = 1000000;
if (diff[1] > 50 * NS_IN_MS) {
setImmediate(workLoop);
break;
}
}
}
}
workLoop();
}
function sourceMapGenerator(modules, options) {
let sourceMapInfos;
getSourceMapInfosImpl(
true,
(infos) => {
sourceMapInfos = infos;
},
modules,
options
);
if (sourceMapInfos == null) {
throw new Error(
"Expected getSourceMapInfosImpl() to finish synchronously."
);
}
return fromRawMappings(sourceMapInfos);
}
async function sourceMapGeneratorNonBlocking(modules, options) {
const sourceMapInfos = await new Promise((resolve) => {
getSourceMapInfosImpl(false, resolve, modules, options);
});
return fromRawMappingsNonBlocking(sourceMapInfos);
}
module.exports = {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
};

View File

@@ -0,0 +1,119 @@
/**
* 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} from '../types.flow';
const getSourceMapInfo = require('./helpers/getSourceMapInfo');
const {isJsModule} = require('./helpers/js');
const {
fromRawMappings,
fromRawMappingsNonBlocking,
} = require('metro-source-map');
export type SourceMapGeneratorOptions = $ReadOnly<{
excludeSource: boolean,
processModuleFilter: (module: Module<>) => boolean,
shouldAddToIgnoreList: (module: Module<>) => boolean,
getSourceUrl: ?(module: Module<>) => string,
}>;
function getSourceMapInfosImpl(
isBlocking: boolean,
onDone: ($ReadOnlyArray<ReturnType<typeof getSourceMapInfo>>) => void,
modules: $ReadOnlyArray<Module<>>,
options: SourceMapGeneratorOptions,
): void {
const sourceMapInfos = [];
const modulesToProcess = modules
.filter(isJsModule)
.filter(options.processModuleFilter);
function processNextModule() {
if (modulesToProcess.length === 0) {
return true;
}
const mod = modulesToProcess.shift();
// $FlowFixMe[incompatible-call]
const info = getSourceMapInfo(mod, {
excludeSource: options.excludeSource,
shouldAddToIgnoreList: options.shouldAddToIgnoreList,
getSourceUrl: options.getSourceUrl,
});
sourceMapInfos.push(info);
return false;
}
function workLoop() {
const time = process.hrtime();
while (true) {
const isDone = processNextModule();
if (isDone) {
onDone(sourceMapInfos);
break;
}
if (!isBlocking) {
// Keep the loop running but try to avoid blocking
// for too long because this is not in a worker yet.
const diff = process.hrtime(time);
const NS_IN_MS = 1000000;
if (diff[1] > 50 * NS_IN_MS) {
// We've blocked for more than 50ms.
// This code currently runs on the main thread,
// so let's give Metro an opportunity to handle requests.
setImmediate(workLoop);
break;
}
}
}
}
workLoop();
}
function sourceMapGenerator(
modules: $ReadOnlyArray<Module<>>,
options: SourceMapGeneratorOptions,
): ReturnType<typeof fromRawMappings> {
let sourceMapInfos;
getSourceMapInfosImpl(
true,
infos => {
sourceMapInfos = infos;
},
modules,
options,
);
if (sourceMapInfos == null) {
throw new Error(
'Expected getSourceMapInfosImpl() to finish synchronously.',
);
}
return fromRawMappings(sourceMapInfos);
}
async function sourceMapGeneratorNonBlocking(
modules: $ReadOnlyArray<Module<>>,
options: SourceMapGeneratorOptions,
): ReturnType<typeof fromRawMappingsNonBlocking> {
const sourceMapInfos = await new Promise<
$ReadOnlyArray<ReturnType<typeof getSourceMapInfo>>,
>(resolve => {
getSourceMapInfosImpl(false, resolve, modules, options);
});
return fromRawMappingsNonBlocking(sourceMapInfos);
}
module.exports = {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
};

View File

@@ -0,0 +1,22 @@
"use strict";
const {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
} = require("./sourceMapGenerator");
function sourceMapObject(modules, options) {
const generator = sourceMapGenerator(modules, options);
return generator.toMap(undefined, {
excludeSource: options.excludeSource,
});
}
async function sourceMapObjectNonBlocking(modules, options) {
const generator = await sourceMapGeneratorNonBlocking(modules, options);
return generator.toMap(undefined, {
excludeSource: options.excludeSource,
});
}
module.exports = {
sourceMapObject,
sourceMapObjectNonBlocking,
};

View File

@@ -0,0 +1,46 @@
/**
* 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} from '../types.flow';
import type {SourceMapGeneratorOptions} from './sourceMapGenerator';
import type {MixedSourceMap} from 'metro-source-map';
const {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
} = require('./sourceMapGenerator');
function sourceMapObject(
modules: $ReadOnlyArray<Module<>>,
options: SourceMapGeneratorOptions,
): MixedSourceMap {
const generator = sourceMapGenerator(modules, options);
return generator.toMap(undefined, {
excludeSource: options.excludeSource,
});
}
async function sourceMapObjectNonBlocking(
modules: $ReadOnlyArray<Module<>>,
options: SourceMapGeneratorOptions,
): Promise<MixedSourceMap> {
const generator = await sourceMapGeneratorNonBlocking(modules, options);
return generator.toMap(undefined, {
excludeSource: options.excludeSource,
});
}
module.exports = {
sourceMapObject,
sourceMapObjectNonBlocking,
};

View File

@@ -0,0 +1,21 @@
"use strict";
const {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
} = require("./sourceMapGenerator");
function sourceMapString(modules, options) {
return sourceMapGenerator(modules, options).toString(undefined, {
excludeSource: options.excludeSource,
});
}
async function sourceMapStringNonBlocking(modules, options) {
const generator = await sourceMapGeneratorNonBlocking(modules, options);
return generator.toString(undefined, {
excludeSource: options.excludeSource,
});
}
module.exports = {
sourceMapString,
sourceMapStringNonBlocking,
};

View File

@@ -0,0 +1,44 @@
/**
* 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} from '../types.flow';
import type {SourceMapGeneratorOptions} from './sourceMapGenerator';
const {
sourceMapGenerator,
sourceMapGeneratorNonBlocking,
} = require('./sourceMapGenerator');
function sourceMapString(
modules: $ReadOnlyArray<Module<>>,
options: SourceMapGeneratorOptions,
): string {
return sourceMapGenerator(modules, options).toString(undefined, {
excludeSource: options.excludeSource,
});
}
async function sourceMapStringNonBlocking(
modules: $ReadOnlyArray<Module<>>,
options: SourceMapGeneratorOptions,
): Promise<string> {
const generator = await sourceMapGeneratorNonBlocking(modules, options);
return generator.toString(undefined, {
excludeSource: options.excludeSource,
});
}
module.exports = {
sourceMapString,
sourceMapStringNonBlocking,
};

View File

@@ -0,0 +1,138 @@
"use strict";
var _crypto = _interopRequireDefault(require("crypto"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const getTransformCacheKey = require("./getTransformCacheKey");
const WorkerFarm = require("./WorkerFarm");
const assert = require("assert");
const fs = require("fs");
const { Cache, stableHash } = require("metro-cache");
const path = require("path");
class Transformer {
constructor(config, getSha1Fn) {
this._config = config;
this._config.watchFolders.forEach(verifyRootExists);
this._cache = new Cache(config.cacheStores);
this._getSha1 = getSha1Fn;
const {
getTransformOptions: _getTransformOptions,
transformVariants: _transformVariants,
workerPath: _workerPath,
unstable_workerThreads: _workerThreads,
...transformerConfig
} = this._config.transformer;
const transformerOptions = {
transformerPath: this._config.transformerPath,
transformerConfig,
};
this._workerFarm = new WorkerFarm(config, transformerOptions);
const globalCacheKey = this._cache.isDisabled
? ""
: getTransformCacheKey({
cacheVersion: this._config.cacheVersion,
projectRoot: this._config.projectRoot,
transformerConfig: transformerOptions,
});
this._baseHash = stableHash([globalCacheKey]).toString("binary");
}
async transformFile(filePath, transformerOptions, fileBuffer) {
const cache = this._cache;
const {
customTransformOptions,
dev,
experimentalImportSupport,
hot,
inlinePlatform,
inlineRequires,
minify,
nonInlinedRequires,
platform,
type,
unstable_disableES6Transforms,
unstable_transformProfile,
...extra
} = transformerOptions;
for (const key in extra) {
if (hasOwnProperty.call(extra, key)) {
throw new Error(
"Extra keys detected: " + Object.keys(extra).join(", ")
);
}
}
const localPath = path.relative(this._config.projectRoot, filePath);
const partialKey = stableHash([
this._baseHash,
path.sep === "/" ? localPath : localPath.replaceAll(path.sep, "/"),
customTransformOptions,
dev,
experimentalImportSupport,
hot,
inlinePlatform,
inlineRequires,
minify,
nonInlinedRequires,
platform,
type,
unstable_disableES6Transforms,
unstable_transformProfile,
]);
let sha1;
if (fileBuffer) {
sha1 = _crypto.default
.createHash("sha1")
.update(fileBuffer)
.digest("hex");
} else {
sha1 = this._getSha1(filePath);
}
let fullKey = Buffer.concat([partialKey, Buffer.from(sha1, "hex")]);
let result;
try {
result = await cache.get(fullKey);
} catch (error) {
this._config.reporter.update({
type: "cache_read_error",
error,
});
throw error;
}
const data = result
? {
result,
sha1,
}
: await this._workerFarm.transform(
localPath,
transformerOptions,
fileBuffer
);
if (sha1 !== data.sha1) {
fullKey = Buffer.concat([partialKey, Buffer.from(data.sha1, "hex")]);
}
cache.set(fullKey, data.result).catch((error) => {
this._config.reporter.update({
type: "cache_write_error",
error,
});
});
return {
...data.result,
unstable_transformResultKey: fullKey.toString(),
getSource() {
if (fileBuffer) {
return fileBuffer;
}
return fs.readFileSync(filePath);
},
};
}
end() {
this._workerFarm.kill();
}
}
function verifyRootExists(root) {
assert(fs.statSync(root).isDirectory(), "Root has to be a valid directory");
}
module.exports = Transformer;

View File

@@ -0,0 +1,198 @@
/**
* 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 {TransformResult, TransformResultWithSource} from '../DeltaBundler';
import type {TransformerConfig, TransformOptions} from './Worker';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
import crypto from 'crypto';
const getTransformCacheKey = require('./getTransformCacheKey');
const WorkerFarm = require('./WorkerFarm');
const assert = require('assert');
const fs = require('fs');
const {Cache, stableHash} = require('metro-cache');
const path = require('path');
class Transformer {
_config: ConfigT;
_cache: Cache<TransformResult<>>;
_baseHash: string;
_getSha1: string => string;
_workerFarm: WorkerFarm;
constructor(config: ConfigT, getSha1Fn: string => string) {
this._config = config;
this._config.watchFolders.forEach(verifyRootExists);
this._cache = new Cache(config.cacheStores);
this._getSha1 = getSha1Fn;
// Remove the transformer config params that we don't want to pass to the
// transformer. We should change the config object and move them away so we
// can treat the transformer config params as opaque.
const {
getTransformOptions: _getTransformOptions,
transformVariants: _transformVariants,
workerPath: _workerPath,
unstable_workerThreads: _workerThreads,
...transformerConfig
} = this._config.transformer;
const transformerOptions: TransformerConfig = {
transformerPath: this._config.transformerPath,
transformerConfig,
};
this._workerFarm = new WorkerFarm(config, transformerOptions);
const globalCacheKey = this._cache.isDisabled
? ''
: getTransformCacheKey({
cacheVersion: this._config.cacheVersion,
projectRoot: this._config.projectRoot,
transformerConfig: transformerOptions,
});
this._baseHash = stableHash([globalCacheKey]).toString('binary');
}
async transformFile(
filePath: string,
transformerOptions: TransformOptions,
fileBuffer?: Buffer,
): Promise<TransformResultWithSource<>> {
const cache = this._cache;
const {
customTransformOptions,
dev,
experimentalImportSupport,
hot,
inlinePlatform,
inlineRequires,
minify,
nonInlinedRequires,
platform,
type,
unstable_disableES6Transforms,
unstable_transformProfile,
...extra
} = transformerOptions;
for (const key in extra) {
// $FlowFixMe[cannot-resolve-name]
if (hasOwnProperty.call(extra, key)) {
throw new Error(
'Extra keys detected: ' + Object.keys(extra).join(', '),
);
}
}
const localPath = path.relative(this._config.projectRoot, filePath);
const partialKey = stableHash([
// This is the hash related to the global Bundler config.
this._baseHash,
// Project-relative, posix-separated path for portability. Necessary in
// addition to content hash because transformers receive path as an
// input, and may apply e.g. extension-based logic.
path.sep === '/' ? localPath : localPath.replaceAll(path.sep, '/'),
customTransformOptions,
dev,
experimentalImportSupport,
hot,
inlinePlatform,
inlineRequires,
minify,
nonInlinedRequires,
platform,
type,
unstable_disableES6Transforms,
unstable_transformProfile,
]);
let sha1: string;
if (fileBuffer) {
// Shortcut for virtual modules which provide the contents with the filename.
sha1 = crypto.createHash('sha1').update(fileBuffer).digest('hex');
} else {
sha1 = this._getSha1(filePath);
}
let fullKey = Buffer.concat([partialKey, Buffer.from(sha1, 'hex')]);
let result;
try {
result = await cache.get(fullKey);
} catch (error) {
this._config.reporter.update({
type: 'cache_read_error',
error,
});
throw error;
}
// A valid result from the cache is used directly; otherwise we call into
// the transformer to computed the corresponding result.
const data: $ReadOnly<{
result: TransformResult<>,
sha1: string,
}> = result
? {result, sha1}
: await this._workerFarm.transform(
localPath,
transformerOptions,
fileBuffer,
);
// Only re-compute the full key if the SHA-1 changed. This is because
// references are used by the cache implementation in a weak map to keep
// track of the cache that returned the result.
if (sha1 !== data.sha1) {
fullKey = Buffer.concat([partialKey, Buffer.from(data.sha1, 'hex')]);
}
// Fire-and-forget cache set promise.
cache.set(fullKey, data.result).catch(error => {
this._config.reporter.update({
type: 'cache_write_error',
error,
});
});
return {
...data.result,
unstable_transformResultKey: fullKey.toString(),
getSource(): Buffer {
if (fileBuffer) {
return fileBuffer;
}
return fs.readFileSync(filePath);
},
};
}
end(): void {
// $FlowFixMe[unused-promise]
this._workerFarm.kill();
}
}
function verifyRootExists(root: string): void {
// Verify that the root exists.
assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory');
}
module.exports = Transformer;

View File

@@ -0,0 +1,47 @@
/**
* 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
*/
import type {TransformResult} from './types';
import type {
JsTransformerConfig,
JsTransformOptions,
} from 'metro-transform-worker';
type LogEntry = unknown;
export type TransformOptions = JsTransformOptions;
declare function transform(
filename: string,
transformOptions: JsTransformOptions,
projectRoot: string,
transformerConfig: TransformerConfig,
fileBuffer?: Buffer,
): Promise<Data>;
export interface Worker {
readonly transform: typeof transform;
}
export interface TransformerConfig {
transformerPath: string;
transformerConfig: JsTransformerConfig;
}
interface Data {
readonly result: TransformResult<void>;
readonly sha1: string;
readonly transformFileStartLogEntry: LogEntry;
readonly transformFileEndLogEntry: LogEntry;
}
declare const worker: Worker;
export default worker;

View File

@@ -0,0 +1,86 @@
"use strict";
const traverse = require("@babel/traverse").default;
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
function asDeserializedBuffer(value) {
if (Buffer.isBuffer(value)) {
return value;
}
if (value && value.type === "Buffer") {
return Buffer.from(value.data);
}
return null;
}
async function transform(
filename,
transformOptions,
projectRoot,
transformerConfig,
fileBuffer
) {
let data;
const fileBufferObject = asDeserializedBuffer(fileBuffer);
if (fileBufferObject) {
data = fileBufferObject;
} else {
data = fs.readFileSync(path.resolve(projectRoot, filename));
}
return transformFile(
filename,
data,
transformOptions,
projectRoot,
transformerConfig
);
}
async function transformFile(
filename,
data,
transformOptions,
projectRoot,
transformerConfig
) {
const Transformer = require.call(null, transformerConfig.transformerPath);
const transformFileStartLogEntry = {
action_name: "Transforming file",
action_phase: "start",
file_name: filename,
log_entry_label: "Transforming file",
start_timestamp: process.hrtime(),
};
const sha1 = crypto.createHash("sha1").update(data).digest("hex");
const result = await Transformer.transform(
transformerConfig.transformerConfig,
projectRoot,
filename,
data,
transformOptions
);
traverse.cache.clear();
const transformFileEndLogEntry = getEndLogEntry(
transformFileStartLogEntry,
filename
);
return {
result,
sha1,
transformFileStartLogEntry,
transformFileEndLogEntry,
};
}
function getEndLogEntry(startLogEntry, filename) {
const timeDelta = process.hrtime(startLogEntry.start_timestamp);
const duration_ms = Math.round((timeDelta[0] * 1e9 + timeDelta[1]) / 1e6);
return {
action_name: "Transforming file",
action_phase: "end",
file_name: filename,
duration_ms,
log_entry_label: "Transforming file",
};
}
module.exports = {
transform,
};

View File

@@ -0,0 +1,163 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {TransformResult} from './types.flow';
import type {LogEntry} from 'metro-core/src/Logger';
import type {
JsTransformerConfig,
JsTransformOptions,
} from 'metro-transform-worker';
const traverse = require('@babel/traverse').default;
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
export type {JsTransformOptions as TransformOptions} from 'metro-transform-worker';
export type Worker = {
+transform: typeof transform,
};
type TransformerInterface = {
transform(
JsTransformerConfig,
string,
string,
Buffer,
JsTransformOptions,
): Promise<TransformResult<>>,
};
export type TransformerConfig = {
transformerPath: string,
transformerConfig: JsTransformerConfig,
...
};
type Data = $ReadOnly<{
result: TransformResult<>,
sha1: string,
transformFileStartLogEntry: LogEntry,
transformFileEndLogEntry: LogEntry,
}>;
/**
* When the `Buffer` is sent over the worker thread it gets serialized into a JSON object.
* This helper method will deserialize it if needed.
*
* @returns `Buffer` representation of the JSON object.
* @returns `null` if the given object is nullish or not a serialized `Buffer` object.
*/
function asDeserializedBuffer(value: any): Buffer | null {
if (Buffer.isBuffer(value)) {
return value;
}
if (value && value.type === 'Buffer') {
return Buffer.from(value.data);
}
return null;
}
async function transform(
filename: string,
transformOptions: JsTransformOptions,
projectRoot: string,
transformerConfig: TransformerConfig,
fileBuffer?: Buffer,
): Promise<Data> {
let data;
const fileBufferObject = asDeserializedBuffer(fileBuffer);
if (fileBufferObject) {
data = fileBufferObject;
} else {
data = fs.readFileSync(path.resolve(projectRoot, filename));
}
return transformFile(
filename,
data,
transformOptions,
projectRoot,
transformerConfig,
);
}
async function transformFile(
filename: string,
data: Buffer,
transformOptions: JsTransformOptions,
projectRoot: string,
transformerConfig: TransformerConfig,
): Promise<Data> {
// eslint-disable-next-line no-useless-call
const Transformer: TransformerInterface = require.call(
null,
transformerConfig.transformerPath,
);
const transformFileStartLogEntry = {
action_name: 'Transforming file',
action_phase: 'start',
file_name: filename,
log_entry_label: 'Transforming file',
start_timestamp: process.hrtime(),
};
const sha1 = crypto.createHash('sha1').update(data).digest('hex');
const result = await Transformer.transform(
transformerConfig.transformerConfig,
projectRoot,
filename,
data,
transformOptions,
);
// The babel cache caches scopes and pathes for already traversed AST nodes.
// Clearing the cache here since the nodes of the transformed file are no longer referenced.
// This isn't stritcly necessary since the cache uses a WeakMap. However, WeakMap only permit
// that unreferenced keys are collected but the values still hold references to the Scope and NodePaths.
// Manually clearing the cache allows the GC to collect the Scope and NodePaths without checking if there
// exist any other references to the keys.
traverse.cache.clear();
const transformFileEndLogEntry = getEndLogEntry(
transformFileStartLogEntry,
filename,
);
return {
result,
sha1,
transformFileStartLogEntry,
transformFileEndLogEntry,
};
}
function getEndLogEntry(startLogEntry: LogEntry, filename: string): LogEntry {
const timeDelta = process.hrtime(startLogEntry.start_timestamp);
const duration_ms = Math.round((timeDelta[0] * 1e9 + timeDelta[1]) / 1e6);
return {
action_name: 'Transforming file',
action_phase: 'end',
file_name: filename,
duration_ms,
log_entry_label: 'Transforming file',
};
}
module.exports = ({
transform,
}: Worker);

View File

@@ -0,0 +1,6 @@
"use strict";
try {
require("metro-babel-register").unstable_registerForMetroMonorepo();
} catch {}
module.exports = require("./Worker.flow");

View File

@@ -0,0 +1,22 @@
/**
* 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';
/*::
export type * from './Worker.flow';
*/
try {
require('metro-babel-register').unstable_registerForMetroMonorepo();
} catch {}
module.exports = require('./Worker.flow');

View File

@@ -0,0 +1,113 @@
"use strict";
const { Worker: JestWorker } = require("jest-worker");
const { Logger } = require("metro-core");
class WorkerFarm {
constructor(config, transformerConfig) {
this._config = config;
this._transformerConfig = transformerConfig;
const absoluteWorkerPath = require.resolve(config.transformer.workerPath);
if (this._config.maxWorkers > 1) {
const worker = this._makeFarm(
absoluteWorkerPath,
["transform"],
this._config.maxWorkers
);
worker.getStdout().on("data", (chunk) => {
this._config.reporter.update({
type: "worker_stdout_chunk",
chunk: chunk.toString("utf8"),
});
});
worker.getStderr().on("data", (chunk) => {
this._config.reporter.update({
type: "worker_stderr_chunk",
chunk: chunk.toString("utf8"),
});
});
this._worker = worker;
} else {
this._worker = require.call(null, this._config.transformer.workerPath);
}
}
async kill() {
if (this._worker && typeof this._worker.end === "function") {
await this._worker.end();
}
}
async transform(filename, options, fileBuffer) {
try {
const data = await this._worker.transform(
filename,
options,
this._config.projectRoot,
this._transformerConfig,
fileBuffer
);
Logger.log(data.transformFileStartLogEntry);
Logger.log(data.transformFileEndLogEntry);
return {
result: data.result,
sha1: data.sha1,
};
} catch (err) {
if (err.loc) {
throw this._formatBabelError(err, filename);
} else {
throw this._formatGenericError(err, filename);
}
}
}
_makeFarm(absoluteWorkerPath, exposedMethods, numWorkers) {
const env = {
...process.env,
FORCE_COLOR: 1,
};
return new JestWorker(absoluteWorkerPath, {
computeWorkerKey: this._config.stickyWorkers
? this._computeWorkerKey
: undefined,
exposedMethods,
enableWorkerThreads: this._config.transformer.unstable_workerThreads,
forkOptions: {
env,
},
numWorkers,
});
}
_computeWorkerKey(method, filename) {
if (method === "transform") {
return filename;
}
return null;
}
_formatGenericError(err, filename) {
const error = new TransformError(`${filename}: ${err.message}`);
return Object.assign(error, {
stack: (err.stack || "").split("\n").slice(0, -1).join("\n"),
lineNumber: 0,
});
}
_formatBabelError(err, filename) {
const error = new TransformError(
`${err.type || "Error"}${
err.message.includes(filename) ? "" : " in " + filename
}: ${err.message}`
);
return Object.assign(error, {
stack: err.stack,
snippet: err.codeFrame,
lineNumber: err.loc.line,
column: err.loc.column,
filename,
});
}
}
class TransformError extends SyntaxError {
type = "TransformError";
constructor(message) {
super(message);
Error.captureStackTrace && Error.captureStackTrace(this, TransformError);
}
}
module.exports = WorkerFarm;

View File

@@ -0,0 +1,181 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {TransformResult} from '../DeltaBundler';
import type {TransformerConfig, TransformOptions, Worker} from './Worker';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
import type {Readable} from 'stream';
const {Worker: JestWorker} = require('jest-worker');
const {Logger} = require('metro-core');
type WorkerInterface = {
getStdout(): Readable,
getStderr(): Readable,
end(): void,
...Worker,
};
type TransformerResult = $ReadOnly<{
result: TransformResult<>,
sha1: string,
}>;
class WorkerFarm {
_config: ConfigT;
_transformerConfig: TransformerConfig;
_worker: WorkerInterface | Worker;
constructor(config: ConfigT, transformerConfig: TransformerConfig) {
this._config = config;
this._transformerConfig = transformerConfig;
const absoluteWorkerPath = require.resolve(config.transformer.workerPath);
if (this._config.maxWorkers > 1) {
const worker = this._makeFarm(
absoluteWorkerPath,
['transform'],
this._config.maxWorkers,
);
worker.getStdout().on('data', chunk => {
this._config.reporter.update({
type: 'worker_stdout_chunk',
chunk: chunk.toString('utf8'),
});
});
worker.getStderr().on('data', chunk => {
this._config.reporter.update({
type: 'worker_stderr_chunk',
chunk: chunk.toString('utf8'),
});
});
this._worker = worker;
} else {
// eslint-disable-next-line no-useless-call
this._worker = (require.call(
null,
this._config.transformer.workerPath,
): Worker);
}
}
async kill(): Promise<void> {
if (this._worker && typeof this._worker.end === 'function') {
await this._worker.end();
}
}
async transform(
filename: string,
options: TransformOptions,
fileBuffer?: Buffer,
): Promise<TransformerResult> {
try {
const data = await this._worker.transform(
filename,
options,
this._config.projectRoot,
this._transformerConfig,
fileBuffer,
);
Logger.log(data.transformFileStartLogEntry);
Logger.log(data.transformFileEndLogEntry);
return {
result: data.result,
sha1: data.sha1,
};
} catch (err) {
if (err.loc) {
throw this._formatBabelError(err, filename);
} else {
throw this._formatGenericError(err, filename);
}
}
}
_makeFarm(
absoluteWorkerPath: string,
exposedMethods: $ReadOnlyArray<string>,
numWorkers: number,
): any {
const env = {
...process.env,
// Force color to print syntax highlighted code frames.
FORCE_COLOR: 1,
};
return new JestWorker(absoluteWorkerPath, {
computeWorkerKey: this._config.stickyWorkers
? // $FlowFixMe[method-unbinding] added when improving typing for this parameters
// $FlowFixMe[incompatible-call]
this._computeWorkerKey
: undefined,
exposedMethods,
enableWorkerThreads: this._config.transformer.unstable_workerThreads,
forkOptions: {env},
numWorkers,
});
}
_computeWorkerKey(method: string, filename: string): ?string {
// Only when transforming a file we want to stick to the same worker; and
// we'll shard by file path. If not; we return null, which tells the worker
// to pick the first available one.
if (method === 'transform') {
return filename;
}
return null;
}
_formatGenericError(err: any, filename: string): TransformError {
const error = new TransformError(`${filename}: ${err.message}`);
return Object.assign(error, {
stack: (err.stack || '').split('\n').slice(0, -1).join('\n'),
lineNumber: 0,
});
}
_formatBabelError(err: any, filename: string): TransformError {
const error = new TransformError(
`${err.type || 'Error'}${
err.message.includes(filename) ? '' : ' in ' + filename
}: ${err.message}`,
);
// $FlowExpectedError: TODO(t67543470): Change this to properly extend the error.
return Object.assign(error, {
stack: err.stack,
snippet: err.codeFrame,
lineNumber: err.loc.line,
column: err.loc.column,
filename,
});
}
}
class TransformError extends SyntaxError {
type: string = 'TransformError';
constructor(message: string) {
super(message);
Error.captureStackTrace && Error.captureStackTrace(this, TransformError);
}
}
module.exports = WorkerFarm;

View File

@@ -0,0 +1,17 @@
"use strict";
const fs = require("fs");
module.exports = {
getHasteName(filename) {
const matches = fs
.readFileSync(filename, "utf8")
.match(/@providesModule ([^\n]+)/);
if (!matches) {
return undefined;
}
return matches[1];
},
getCacheKey() {
return "hasteImplFixture";
},
};

View File

@@ -0,0 +1,114 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.buildSubgraph = buildSubgraph;
var _contextModule = require("../lib/contextModule");
var _path = _interopRequireDefault(require("path"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
function resolveDependencies(parentPath, dependencies, resolve) {
const maybeResolvedDeps = new Map();
const resolvedContexts = new Map();
for (const dep of dependencies) {
let resolvedDep;
const key = dep.data.key;
const { contextParams } = dep.data;
if (contextParams) {
const from = _path.default.join(parentPath, "..", dep.name);
const absolutePath = (0, _contextModule.deriveAbsolutePathFromContext)(
from,
contextParams
);
const resolvedContext = {
from,
mode: contextParams.mode,
recursive: contextParams.recursive,
filter: new RegExp(
contextParams.filter.pattern,
contextParams.filter.flags
),
};
resolvedContexts.set(key, resolvedContext);
resolvedDep = {
absolutePath,
data: dep,
};
} else {
try {
resolvedDep = {
absolutePath: resolve(parentPath, dep).filePath,
data: dep,
};
} catch (error) {
if (dep.data.isOptional !== true) {
throw error;
}
}
}
if (maybeResolvedDeps.has(key)) {
throw new Error(
`resolveDependencies: Found duplicate dependency key '${key}' in ${parentPath}`
);
}
maybeResolvedDeps.set(key, resolvedDep);
}
const resolvedDeps = new Map();
for (const [key, resolvedDep] of maybeResolvedDeps) {
if (resolvedDep) {
resolvedDeps.set(key, resolvedDep);
}
}
return {
dependencies: resolvedDeps,
resolvedContexts,
};
}
async function buildSubgraph(
entryPaths,
resolvedContexts,
{ resolve, transform, shouldTraverse }
) {
const moduleData = new Map();
const errors = new Map();
const visitedPaths = new Set();
async function visit(absolutePath, requireContext) {
if (visitedPaths.has(absolutePath)) {
return;
}
visitedPaths.add(absolutePath);
const transformResult = await transform(absolutePath, requireContext);
const resolutionResult = resolveDependencies(
absolutePath,
transformResult.dependencies,
resolve
);
moduleData.set(absolutePath, {
...transformResult,
...resolutionResult,
});
await Promise.all(
[...resolutionResult.dependencies]
.filter(([key, dependency]) => shouldTraverse(dependency))
.map(([key, dependency]) =>
visit(
dependency.absolutePath,
resolutionResult.resolvedContexts.get(dependency.data.data.key)
).catch((error) => errors.set(dependency.absolutePath, error))
)
);
}
await Promise.all(
[...entryPaths].map((absolutePath) =>
visit(absolutePath, resolvedContexts.get(absolutePath)).catch((error) =>
errors.set(absolutePath, error)
)
)
);
return {
moduleData,
errors,
};
}

View File

@@ -0,0 +1,161 @@
/**
* 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
*/
import type {RequireContext} from '../lib/contextModule';
import type {
Dependency,
ModuleData,
ResolveFn,
TransformFn,
TransformResultDependency,
} from './types.flow';
import {deriveAbsolutePathFromContext} from '../lib/contextModule';
import path from 'path';
type Parameters<T> = $ReadOnly<{
resolve: ResolveFn,
transform: TransformFn<T>,
shouldTraverse: Dependency => boolean,
}>;
function resolveDependencies(
parentPath: string,
dependencies: $ReadOnlyArray<TransformResultDependency>,
resolve: ResolveFn,
): {
dependencies: Map<string, Dependency>,
resolvedContexts: Map<string, RequireContext>,
} {
const maybeResolvedDeps = new Map<string, void | Dependency>();
const resolvedContexts = new Map<string, RequireContext>();
for (const dep of dependencies) {
let resolvedDep;
const key = dep.data.key;
// `require.context`
const {contextParams} = dep.data;
if (contextParams) {
// Ensure the filepath has uniqueness applied to ensure multiple `require.context`
// statements can be used to target the same file with different properties.
const from = path.join(parentPath, '..', dep.name);
const absolutePath = deriveAbsolutePathFromContext(from, contextParams);
const resolvedContext: RequireContext = {
from,
mode: contextParams.mode,
recursive: contextParams.recursive,
filter: new RegExp(
contextParams.filter.pattern,
contextParams.filter.flags,
),
};
resolvedContexts.set(key, resolvedContext);
resolvedDep = {
absolutePath,
data: dep,
};
} else {
try {
resolvedDep = {
absolutePath: resolve(parentPath, dep).filePath,
data: dep,
};
} catch (error) {
// Ignore unavailable optional dependencies. They are guarded
// with a try-catch block and will be handled during runtime.
if (dep.data.isOptional !== true) {
throw error;
}
}
}
if (maybeResolvedDeps.has(key)) {
throw new Error(
`resolveDependencies: Found duplicate dependency key '${key}' in ${parentPath}`,
);
}
maybeResolvedDeps.set(key, resolvedDep);
}
const resolvedDeps = new Map<string, Dependency>();
// Return just the dependencies we successfully resolved.
// FIXME: This has a bad bug affecting all dependencies *after* an unresolved
// optional dependency. We'll need to propagate the nulls all the way to the
// serializer and the require() runtime to keep the dependency map from being
// desynced from the contents of the module.
for (const [key, resolvedDep] of maybeResolvedDeps) {
if (resolvedDep) {
resolvedDeps.set(key, resolvedDep);
}
}
return {dependencies: resolvedDeps, resolvedContexts};
}
export async function buildSubgraph<T>(
entryPaths: $ReadOnlySet<string>,
resolvedContexts: $ReadOnlyMap<string, ?RequireContext>,
{resolve, transform, shouldTraverse}: Parameters<T>,
): Promise<{
moduleData: Map<string, ModuleData<T>>,
errors: Map<string, Error>,
}> {
const moduleData: Map<string, ModuleData<T>> = new Map();
const errors: Map<string, Error> = new Map();
const visitedPaths: Set<string> = new Set();
async function visit(
absolutePath: string,
requireContext: ?RequireContext,
): Promise<void> {
if (visitedPaths.has(absolutePath)) {
return;
}
visitedPaths.add(absolutePath);
const transformResult = await transform(absolutePath, requireContext);
// Get the absolute path of all sub-dependencies (some of them could have been
// moved but maintain the same relative path).
const resolutionResult = resolveDependencies(
absolutePath,
transformResult.dependencies,
resolve,
);
moduleData.set(absolutePath, {
...transformResult,
...resolutionResult,
});
await Promise.all(
[...resolutionResult.dependencies]
.filter(([key, dependency]) => shouldTraverse(dependency))
.map(([key, dependency]) =>
visit(
dependency.absolutePath,
resolutionResult.resolvedContexts.get(dependency.data.data.key),
).catch(error => errors.set(dependency.absolutePath, error)),
),
);
}
await Promise.all(
[...entryPaths].map(absolutePath =>
visit(absolutePath, resolvedContexts.get(absolutePath)).catch(error =>
errors.set(absolutePath, error),
),
),
);
return {moduleData, errors};
}

View File

@@ -0,0 +1,26 @@
"use strict";
const VERSION = require("../../package.json").version;
const crypto = require("crypto");
const getCacheKey = require("metro-cache-key");
function getTransformCacheKey(opts) {
const { transformerPath, transformerConfig } = opts.transformerConfig;
const Transformer = require.call(null, transformerPath);
const transformerKey = Transformer.getCacheKey
? Transformer.getCacheKey(transformerConfig)
: "";
return crypto
.createHash("sha1")
.update(
[
"metro-cache",
VERSION,
opts.cacheVersion,
getCacheKey([require.resolve(transformerPath)]),
transformerKey,
transformerConfig.globalPrefix,
].join("$")
)
.digest("hex");
}
module.exports = getTransformCacheKey;

View File

@@ -0,0 +1,53 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {TransformerConfig} from './Worker';
import type {JsTransformerConfig} from 'metro-transform-worker';
const VERSION = require('../../package.json').version;
const crypto = require('crypto');
const getCacheKey = require('metro-cache-key');
type CacheKeyProvider = {
getCacheKey?: JsTransformerConfig => string,
};
function getTransformCacheKey(opts: {
+cacheVersion: string,
+projectRoot: string,
+transformerConfig: TransformerConfig,
}): string {
const {transformerPath, transformerConfig} = opts.transformerConfig;
// eslint-disable-next-line no-useless-call
const Transformer: CacheKeyProvider = require.call(null, transformerPath);
const transformerKey = Transformer.getCacheKey
? Transformer.getCacheKey(transformerConfig)
: '';
return crypto
.createHash('sha1')
.update(
[
'metro-cache',
VERSION,
opts.cacheVersion,
getCacheKey([require.resolve(transformerPath)]),
transformerKey,
transformerConfig.globalPrefix,
].join('$'),
)
.digest('hex');
}
module.exports = getTransformCacheKey;

View File

@@ -0,0 +1,53 @@
"use strict";
function mergeDeltas(delta1, delta2) {
const added1 = new Map(delta1.added);
const modified1 = new Map(delta1.modified);
const deleted1 = new Set(delta1.deleted);
const added2 = new Map(delta2.added);
const modified2 = new Map(delta2.modified);
const deleted2 = new Set(delta2.deleted);
const added = new Map();
const modified = new Map();
const deleted = new Set();
for (const [id, code] of added1) {
if (!deleted2.has(id) && !modified2.has(id)) {
added.set(id, code);
}
}
for (const [id, code] of modified1) {
if (!deleted2.has(id) && !modified2.has(id)) {
modified.set(id, code);
}
}
for (const id of deleted1) {
if (!added2.has(id)) {
deleted.add(id);
}
}
for (const [id, code] of added2) {
if (deleted1.has(id)) {
modified.set(id, code);
} else {
added.set(id, code);
}
}
for (const [id, code] of modified2) {
if (added1.has(id)) {
added.set(id, code);
} else {
modified.set(id, code);
}
}
for (const id of deleted2) {
if (!added1.has(id)) {
deleted.add(id);
}
}
return {
added: [...added.entries()],
modified: [...modified.entries()],
deleted: [...deleted],
};
}
module.exports = mergeDeltas;

View File

@@ -0,0 +1,74 @@
/**
* 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 {DeltaBundle} from 'metro-runtime/src/modules/types.flow';
function mergeDeltas(delta1: DeltaBundle, delta2: DeltaBundle): DeltaBundle {
const added1 = new Map(delta1.added);
const modified1 = new Map(delta1.modified);
const deleted1 = new Set(delta1.deleted);
const added2 = new Map(delta2.added);
const modified2 = new Map(delta2.modified);
const deleted2 = new Set(delta2.deleted);
const added = new Map<number, string>();
const modified = new Map<number, string>();
const deleted = new Set<number>();
for (const [id, code] of added1) {
if (!deleted2.has(id) && !modified2.has(id)) {
added.set(id, code);
}
}
for (const [id, code] of modified1) {
if (!deleted2.has(id) && !modified2.has(id)) {
modified.set(id, code);
}
}
for (const id of deleted1) {
if (!added2.has(id)) {
deleted.add(id);
}
}
for (const [id, code] of added2) {
if (deleted1.has(id)) {
modified.set(id, code);
} else {
added.set(id, code);
}
}
for (const [id, code] of modified2) {
if (added1.has(id)) {
added.set(id, code);
} else {
modified.set(id, code);
}
}
for (const id of deleted2) {
if (!added1.has(id)) {
deleted.add(id);
}
}
return {
added: [...added.entries()],
modified: [...modified.entries()],
deleted: [...deleted],
};
}
module.exports = mergeDeltas;

View File

@@ -0,0 +1,163 @@
/**
* 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
*/
import type {RequireContext} from '../lib/contextModule';
import type CountingSet from '../lib/CountingSet';
import type {RequireContextParams} from '../ModuleGraph/worker/collectDependencies';
import type {Graph} from './Graph';
import type {JsTransformOptions} from 'metro-transform-worker';
export interface MixedOutput {
readonly data: {code: string};
readonly type: string;
}
export type AsyncDependencyType = 'async' | 'prefetch' | 'weak';
export interface TransformResultDependency {
/**
* The literal name provided to a require or import call. For example 'foo' in
* case of `require('foo')`.
*/
readonly name: string;
/**
* Extra data returned by the dependency extractor.
*/
readonly data: {
/**
* A locally unique key for this dependency within the current module.
*/
readonly key: string;
/**
* If not null, this dependency is due to a dynamic `import()` or `__prefetchImport()` call.
*/
readonly asyncType: AsyncDependencyType | null;
/**
* The dependency is enclosed in a try/catch block.
*/
readonly isOptional?: boolean;
readonly locs: ReadonlyArray<{
readonly start: {readonly line: number; readonly column: number};
readonly end: {readonly line: number; readonly column: number};
}>;
/** Context for requiring a collection of modules. */
readonly contextParams?: RequireContextParams;
};
}
export interface Dependency {
readonly absolutePath: string;
readonly data: TransformResultDependency;
[key: string]: unknown;
}
export interface Module<T = MixedOutput> {
readonly dependencies: Map<string, Dependency>;
readonly inverseDependencies: CountingSet<string>;
readonly output: ReadonlyArray<T>;
readonly path: string;
readonly getSource: () => Buffer;
}
export type Dependencies<T = MixedOutput> = Map<string, Module<T>>;
export type ReadOnlyDependencies<T = MixedOutput> = ReadonlyMap<
string,
Module<T>
>;
export type TransformInputOptions = Omit<
JsTransformOptions,
'inlinePlatform' | 'inlineRequires'
>;
export type GraphInputOptions = Readonly<{
entryPoints: ReadonlySet<string>;
// Unused in core but useful for custom serializers / experimentalSerializerHook
transformOptions: TransformInputOptions;
}>;
export interface ReadOnlyGraph<T = MixedOutput> {
readonly entryPoints: ReadonlySet<string>;
// Unused in core but useful for custom serializers / experimentalSerializerHook
readonly transformOptions: Readonly<TransformInputOptions>;
readonly dependencies: ReadOnlyDependencies<T>;
}
export type {Graph};
export interface TransformResult<T = MixedOutput> {
dependencies: ReadonlyArray<TransformResultDependency>;
output: ReadonlyArray<T>;
}
export interface TransformResultWithSource<T = MixedOutput>
extends TransformResult<T> {
getSource: () => Buffer;
}
export type TransformFn<T = MixedOutput> = (
modulePath: string,
requireContext: RequireContext | null,
) => Promise<TransformResultWithSource<T>>;
export interface AllowOptionalDependenciesWithOptions {
readonly exclude: string[];
}
export type AllowOptionalDependencies =
| boolean
| AllowOptionalDependenciesWithOptions;
export interface BundlerResolution {
readonly type: 'sourceFile';
readonly filePath: string;
}
export interface Options<T = MixedOutput> {
readonly resolve: (from: string, to: TransformResultDependency) => string;
readonly transform: TransformFn<T>;
readonly transformOptions: TransformInputOptions;
readonly onProgress:
| ((numProcessed: number, total: number) => unknown)
| null;
readonly lazy: boolean;
readonly unstable_allowRequireContext: boolean;
readonly shallow: boolean;
}
export interface DeltaResult<T = MixedOutput> {
readonly added: Map<string, Module<T>>;
readonly modified: Map<string, Module<T>>;
readonly deleted: Set<string>;
readonly reset: boolean;
}
export interface SerializerOptions<T = MixedOutput> {
readonly asyncRequireModulePath: string;
readonly createModuleId: (filePath: string) => number;
readonly dev: boolean;
readonly getRunModuleStatement: (moduleId: string | number) => string;
readonly includeAsyncPaths: boolean;
readonly inlineSourceMap?: boolean;
readonly modulesOnly: boolean;
readonly processModuleFilter: (module: Module<T>) => boolean;
readonly projectRoot: string;
readonly runBeforeMainModule: ReadonlyArray<string>;
readonly runModule: boolean;
readonly serverRoot: string;
readonly shouldAddToIgnoreList: (module: Module<T>) => boolean;
readonly sourceMapUrl?: string;
readonly sourceUrl?: string;
}

View File

@@ -0,0 +1,6 @@
"use strict";
var _CountingSet = _interopRequireDefault(require("../lib/CountingSet"));
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

View File

@@ -0,0 +1,179 @@
/**
* 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 {RequireContext} from '../lib/contextModule';
import type {RequireContextParams} from '../ModuleGraph/worker/collectDependencies';
import type {Graph} from './Graph';
import type {JsTransformOptions} from 'metro-transform-worker';
import CountingSet from '../lib/CountingSet';
export type MixedOutput = {
+data: mixed,
+type: string,
};
export type AsyncDependencyType = 'async' | 'maybeSync' | 'prefetch' | 'weak';
export type TransformResultDependency = $ReadOnly<{
/**
* The literal name provided to a require or import call. For example 'foo' in
* case of `require('foo')`.
*/
name: string,
/**
* Extra data returned by the dependency extractor.
*/
data: $ReadOnly<{
/**
* A locally unique key for this dependency within the current module.
*/
key: string,
/**
* If not null, this dependency is due to a dynamic `import()` or `__prefetchImport()` call.
*/
asyncType: AsyncDependencyType | null,
/**
* The dependency is enclosed in a try/catch block.
*/
isOptional?: boolean,
locs: $ReadOnlyArray<BabelSourceLocation>,
/** Context for requiring a collection of modules. */
contextParams?: RequireContextParams,
}>,
}>;
export type Dependency = $ReadOnly<{
absolutePath: string,
data: TransformResultDependency,
}>;
export type Module<T = MixedOutput> = $ReadOnly<{
dependencies: Map<string, Dependency>,
inverseDependencies: CountingSet<string>,
output: $ReadOnlyArray<T>,
path: string,
getSource: () => Buffer,
unstable_transformResultKey?: ?string,
}>;
export type ModuleData<T = MixedOutput> = $ReadOnly<{
dependencies: $ReadOnlyMap<string, Dependency>,
resolvedContexts: $ReadOnlyMap<string, RequireContext>,
output: $ReadOnlyArray<T>,
getSource: () => Buffer,
unstable_transformResultKey?: ?string,
}>;
export type Dependencies<T = MixedOutput> = Map<string, Module<T>>;
export type ReadOnlyDependencies<T = MixedOutput> = $ReadOnlyMap<
string,
Module<T>,
>;
export type TransformInputOptions = $Diff<
JsTransformOptions,
{
inlinePlatform: boolean,
inlineRequires: boolean,
...
},
>;
export type GraphInputOptions = $ReadOnly<{
entryPoints: $ReadOnlySet<string>,
// Unused in core but useful for custom serializers / experimentalSerializerHook
transformOptions: TransformInputOptions,
}>;
export interface ReadOnlyGraph<T = MixedOutput> {
+entryPoints: $ReadOnlySet<string>;
// Unused in core but useful for custom serializers / experimentalSerializerHook
+transformOptions: $ReadOnly<TransformInputOptions>;
+dependencies: ReadOnlyDependencies<T>;
}
export type {Graph};
export type TransformResult<T = MixedOutput> = $ReadOnly<{
dependencies: $ReadOnlyArray<TransformResultDependency>,
output: $ReadOnlyArray<T>,
unstable_transformResultKey?: ?string,
}>;
export type TransformResultWithSource<T = MixedOutput> = $ReadOnly<{
...TransformResult<T>,
getSource: () => Buffer,
}>;
export type TransformFn<T = MixedOutput> = (
string,
?RequireContext,
) => Promise<TransformResultWithSource<T>>;
export type ResolveFn = (
from: string,
dependency: TransformResultDependency,
) => BundlerResolution;
export type AllowOptionalDependenciesWithOptions = {
+exclude: Array<string>,
};
export type AllowOptionalDependencies =
| boolean
| AllowOptionalDependenciesWithOptions;
export type BundlerResolution = $ReadOnly<{
type: 'sourceFile',
filePath: string,
}>;
export type Options<T = MixedOutput> = {
+resolve: ResolveFn,
+transform: TransformFn<T>,
+transformOptions: TransformInputOptions,
+onProgress: ?(numProcessed: number, total: number) => mixed,
+lazy: boolean,
+unstable_allowRequireContext: boolean,
+unstable_enablePackageExports: boolean,
+shallow: boolean,
};
export type DeltaResult<T = MixedOutput> = {
+added: Map<string, Module<T>>,
+modified: Map<string, Module<T>>,
+deleted: Set<string>,
+reset: boolean,
};
export type SerializerOptions = $ReadOnly<{
asyncRequireModulePath: string,
createModuleId: string => number,
dev: boolean,
getRunModuleStatement: (number | string) => string,
includeAsyncPaths: boolean,
inlineSourceMap: ?boolean,
modulesOnly: boolean,
processModuleFilter: (module: Module<>) => boolean,
projectRoot: string,
runBeforeMainModule: $ReadOnlyArray<string>,
runModule: boolean,
serverRoot: string,
shouldAddToIgnoreList: (Module<>) => boolean,
sourceMapUrl: ?string,
sourceUrl: ?string,
getSourceUrl: ?(Module<>) => string,
}>;

View File

@@ -0,0 +1,285 @@
"use strict";
const hmrJSBundle = require("./DeltaBundler/Serializers/hmrJSBundle");
const GraphNotFoundError = require("./IncrementalBundler/GraphNotFoundError");
const RevisionNotFoundError = require("./IncrementalBundler/RevisionNotFoundError");
const debounceAsyncQueue = require("./lib/debounceAsyncQueue");
const formatBundlingError = require("./lib/formatBundlingError");
const getGraphId = require("./lib/getGraphId");
const parseOptionsFromUrl = require("./lib/parseOptionsFromUrl");
const splitBundleOptions = require("./lib/splitBundleOptions");
const transformHelpers = require("./lib/transformHelpers");
const {
Logger: { createActionStartEntry, createActionEndEntry, log },
} = require("metro-core");
const nullthrows = require("nullthrows");
const url = require("url");
function send(sendFns, message) {
const strMessage = JSON.stringify(message);
sendFns.forEach((sendFn) => sendFn(strMessage));
}
class HmrServer {
constructor(bundler, createModuleId, config) {
this._config = config;
this._bundler = bundler;
this._createModuleId = createModuleId;
this._clientGroups = new Map();
}
onClientConnect = async (requestUrl, sendFn) => {
return {
sendFn,
revisionIds: [],
optedIntoHMR: false,
};
};
async _registerEntryPoint(client, requestUrl, sendFn) {
requestUrl = this._config.server.rewriteRequestUrl(requestUrl);
const clientUrl = nullthrows(url.parse(requestUrl, true));
const options = parseOptionsFromUrl(
requestUrl,
new Set(this._config.resolver.platforms)
);
const { entryFile, resolverOptions, transformOptions, graphOptions } =
splitBundleOptions(options);
const resolutionFn = await transformHelpers.getResolveDependencyFn(
this._bundler.getBundler(),
transformOptions.platform,
resolverOptions
);
const resolvedEntryFilePath = resolutionFn(
(this._config.server.unstable_serverRoot ?? this._config.projectRoot) +
"/.",
{
name: entryFile,
data: {
key: entryFile,
asyncType: null,
locs: [],
},
}
).filePath;
const graphId = getGraphId(resolvedEntryFilePath, transformOptions, {
resolverOptions,
shallow: graphOptions.shallow,
lazy: graphOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
});
const revPromise = this._bundler.getRevisionByGraphId(graphId);
if (!revPromise) {
send([sendFn], {
type: "error",
body: formatBundlingError(new GraphNotFoundError(graphId)),
});
return;
}
const { graph, id } = await revPromise;
client.revisionIds.push(id);
let clientGroup = this._clientGroups.get(id);
if (clientGroup != null) {
clientGroup.clients.add(client);
} else {
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",
};
clientUrl.search = "";
clientGroup = {
clients: new Set([client]),
clientUrl,
revisionId: id,
graphOptions,
unlisten: () => unlisten(),
};
this._clientGroups.set(id, clientGroup);
let latestEventArgs = [];
const debounceCallHandleFileChange = debounceAsyncQueue(async () => {
await this._handleFileChange(
nullthrows(clientGroup),
{
isInitialUpdate: false,
},
...latestEventArgs
);
}, 50);
const unlisten = this._bundler
.getDeltaBundler()
.listen(graph, async (...args) => {
latestEventArgs = args;
await debounceCallHandleFileChange();
});
}
await this._handleFileChange(clientGroup, {
isInitialUpdate: true,
});
send([sendFn], {
type: "bundle-registered",
});
}
onClientMessage = async (client, message, sendFn) => {
let data;
try {
data = JSON.parse(String(message));
} catch (error) {
send([sendFn], {
type: "error",
body: formatBundlingError(error),
});
return Promise.resolve();
}
if (data && data.type) {
switch (data.type) {
case "register-entrypoints":
return Promise.all(
data.entryPoints.map((entryPoint) =>
this._registerEntryPoint(client, entryPoint, sendFn)
)
);
case "log":
if (this._config.server.forwardClientLogs) {
this._config.reporter.update({
type: "client_log",
level: data.level,
data: data.data,
mode: data.mode,
});
}
break;
case "log-opt-in":
client.optedIntoHMR = true;
break;
default:
break;
}
}
return Promise.resolve();
};
onClientError = (client, e) => {
this._config.reporter.update({
type: "hmr_client_error",
error: e.error,
});
this.onClientDisconnect(client);
};
onClientDisconnect = (client) => {
client.revisionIds.forEach((revisionId) => {
const group = this._clientGroups.get(revisionId);
if (group != null) {
if (group.clients.size === 1) {
this._clientGroups.delete(revisionId);
group.unlisten();
} else {
group.clients.delete(client);
}
}
});
};
async _handleFileChange(group, options, changeEvent) {
const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
if (logger) {
logger.point("fileChange_end");
logger.point("hmrPrepareAndSendMessage_start");
}
const optedIntoHMR = [...group.clients].some(
(client) => client.optedIntoHMR
);
const processingHmrChange = log(
createActionStartEntry({
action_name: optedIntoHMR
? "Processing HMR change"
: "Processing HMR change (no client opt-in)",
})
);
const sendFns = [...group.clients].map((client) => client.sendFn);
send(sendFns, {
type: "update-start",
body: options,
});
const message = await this._prepareMessage(group, options, changeEvent);
send(sendFns, message);
send(sendFns, {
type: "update-done",
});
log({
...createActionEndEntry(processingHmrChange),
outdated_modules:
message.type === "update"
? message.body.added.length + message.body.modified.length
: undefined,
});
if (logger) {
logger.point("hmrPrepareAndSendMessage_end");
logger.end("SUCCESS");
}
}
async _prepareMessage(group, options, changeEvent) {
const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
try {
const revPromise = this._bundler.getRevision(group.revisionId);
if (!revPromise) {
return {
type: "error",
body: formatBundlingError(
new RevisionNotFoundError(group.revisionId)
),
};
}
logger?.point("updateGraph_start");
const { revision, delta } = await this._bundler.updateGraph(
await revPromise,
false
);
logger?.point("updateGraph_end");
this._clientGroups.delete(group.revisionId);
group.revisionId = revision.id;
for (const client of group.clients) {
client.revisionIds = client.revisionIds.filter(
(revisionId) => revisionId !== group.revisionId
);
client.revisionIds.push(revision.id);
}
this._clientGroups.set(group.revisionId, group);
logger?.point("serialize_start");
const hmrUpdate = hmrJSBundle(delta, revision.graph, {
clientUrl: group.clientUrl,
createModuleId: this._createModuleId,
includeAsyncPaths: group.graphOptions.lazy,
projectRoot: this._config.projectRoot,
serverRoot:
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
});
logger?.point("serialize_end");
return {
type: "update",
body: {
revisionId: revision.id,
isInitialUpdate: options.isInitialUpdate,
...hmrUpdate,
},
};
} catch (error) {
const formattedError = formatBundlingError(error);
this._config.reporter.update({
type: "bundling_error",
error,
});
return {
type: "error",
body: formattedError,
};
}
}
}
module.exports = HmrServer;

View File

@@ -0,0 +1,396 @@
/**
* 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
* @flow
*/
'use strict';
import type IncrementalBundler, {RevisionId} from './IncrementalBundler';
import type {GraphOptions} from './shared/types.flow';
import type {ConfigT, RootPerfLogger} from 'metro-config';
import type {
HmrClientMessage,
HmrErrorMessage,
HmrMessage,
HmrUpdateMessage,
} from 'metro-runtime/src/modules/types.flow';
import type {UrlWithParsedQuery} from 'url';
const hmrJSBundle = require('./DeltaBundler/Serializers/hmrJSBundle');
const GraphNotFoundError = require('./IncrementalBundler/GraphNotFoundError');
const RevisionNotFoundError = require('./IncrementalBundler/RevisionNotFoundError');
const debounceAsyncQueue = require('./lib/debounceAsyncQueue');
const formatBundlingError = require('./lib/formatBundlingError');
const getGraphId = require('./lib/getGraphId');
const parseOptionsFromUrl = require('./lib/parseOptionsFromUrl');
const splitBundleOptions = require('./lib/splitBundleOptions');
const transformHelpers = require('./lib/transformHelpers');
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,
revisionIds: Array<RevisionId>,
+sendFn: string => void,
};
type ClientGroup = {
+clients: Set<Client>,
clientUrl: EntryPointURL,
revisionId: RevisionId,
+unlisten: () => void,
+graphOptions: GraphOptions,
};
function send(sendFns: Array<(string) => void>, message: HmrMessage): void {
const strMessage = JSON.stringify(message);
sendFns.forEach((sendFn: string => void) => sendFn(strMessage));
}
/**
* The HmrServer (Hot Module Reloading) implements a lightweight interface
* to communicate easily to the logic in the React Native repository (which
* is the one that handles the Web Socket connections).
*
* This interface allows the HmrServer to hook its own logic to WS clients
* getting connected, disconnected or having errors (through the
* `onClientConnect`, `onClientDisconnect` and `onClientError` methods).
*/
class HmrServer<TClient: Client> {
_config: ConfigT;
_bundler: IncrementalBundler;
_createModuleId: (path: string) => number;
_clientGroups: Map<RevisionId, ClientGroup>;
constructor(
bundler: IncrementalBundler,
createModuleId: (path: string) => number,
config: ConfigT,
) {
this._config = config;
this._bundler = bundler;
this._createModuleId = createModuleId;
this._clientGroups = new Map();
}
onClientConnect: (
requestUrl: string,
sendFn: (data: string) => void,
) => Promise<Client> = async (requestUrl, sendFn) => {
return {
sendFn,
revisionIds: [],
optedIntoHMR: false,
};
};
async _registerEntryPoint(
client: Client,
requestUrl: string,
sendFn: (data: string) => void,
): Promise<void> {
requestUrl = this._config.server.rewriteRequestUrl(requestUrl);
const clientUrl = nullthrows(url.parse(requestUrl, true));
const options = parseOptionsFromUrl(
requestUrl,
new Set(this._config.resolver.platforms),
);
const {entryFile, resolverOptions, transformOptions, graphOptions} =
splitBundleOptions(options);
/**
* `entryFile` is relative to projectRoot, we need to use resolution function
* to find the appropriate file with supported extensions.
*/
const resolutionFn = await transformHelpers.getResolveDependencyFn(
this._bundler.getBundler(),
transformOptions.platform,
resolverOptions,
);
const resolvedEntryFilePath = resolutionFn(
(this._config.server.unstable_serverRoot ?? this._config.projectRoot) +
'/.',
{
name: entryFile,
data: {
key: entryFile,
asyncType: null,
locs: [],
},
},
).filePath;
const graphId = getGraphId(resolvedEntryFilePath, transformOptions, {
resolverOptions,
shallow: graphOptions.shallow,
lazy: graphOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
});
const revPromise = this._bundler.getRevisionByGraphId(graphId);
if (!revPromise) {
send([sendFn], {
type: 'error',
body: formatBundlingError(new GraphNotFoundError(graphId)),
});
return;
}
const {graph, id} = await revPromise;
client.revisionIds.push(id);
let clientGroup: ?ClientGroup = this._clientGroups.get(id);
if (clientGroup != null) {
clientGroup.clients.add(client);
} 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',
};
clientUrl.search = '';
clientGroup = {
clients: new Set([client]),
clientUrl,
revisionId: id,
graphOptions,
unlisten: (): void => unlisten(),
};
this._clientGroups.set(id, clientGroup);
let latestEventArgs: Array<any> = [];
const debounceCallHandleFileChange = debounceAsyncQueue(async () => {
await this._handleFileChange(
nullthrows(clientGroup),
{isInitialUpdate: false},
...latestEventArgs,
);
}, 50);
const unlisten = this._bundler
.getDeltaBundler()
// $FlowFixMe[missing-local-annot]
.listen(graph, async (...args) => {
latestEventArgs = args;
await debounceCallHandleFileChange();
});
}
await this._handleFileChange(clientGroup, {isInitialUpdate: true});
send([sendFn], {type: 'bundle-registered'});
}
onClientMessage: (
client: TClient,
message: string | Buffer | ArrayBuffer | Array<Buffer>,
sendFn: (data: string) => void,
) => Promise<void> = async (client, message, sendFn) => {
let data: HmrClientMessage;
try {
data = JSON.parse(String(message));
} catch (error) {
send([sendFn], {
type: 'error',
body: formatBundlingError(error),
});
return Promise.resolve();
}
if (data && data.type) {
switch (data.type) {
case 'register-entrypoints':
return Promise.all(
data.entryPoints.map(entryPoint =>
this._registerEntryPoint(client, entryPoint, sendFn),
),
);
case 'log':
if (this._config.server.forwardClientLogs) {
this._config.reporter.update({
type: 'client_log',
level: data.level,
data: data.data,
mode: data.mode,
});
}
break;
case 'log-opt-in':
client.optedIntoHMR = true;
break;
default:
break;
}
}
return Promise.resolve();
};
onClientError: (client: TClient, e: ErrorEvent) => void = (client, e) => {
this._config.reporter.update({
type: 'hmr_client_error',
error: e.error,
});
this.onClientDisconnect(client);
};
onClientDisconnect: (client: TClient) => void = client => {
client.revisionIds.forEach(revisionId => {
const group = this._clientGroups.get(revisionId);
if (group != null) {
if (group.clients.size === 1) {
this._clientGroups.delete(revisionId);
group.unlisten();
} else {
group.clients.delete(client);
}
}
});
};
async _handleFileChange(
group: ClientGroup,
options: {isInitialUpdate: boolean},
changeEvent: ?{
logger: ?RootPerfLogger,
},
): Promise<void> {
const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
if (logger) {
logger.point('fileChange_end');
logger.point('hmrPrepareAndSendMessage_start');
}
const optedIntoHMR = [...group.clients].some(
(client: Client) => client.optedIntoHMR,
);
const processingHmrChange = log(
createActionStartEntry({
// Even when HMR is disabled on the client, this function still
// runs so we can stash updates while it's off and apply them later.
// However, this would mess up our internal analytics because we track
// HMR as being used even for people who have it disabled.
// As a workaround, we use a different event name for clients
// that didn't explicitly opt into HMR.
action_name: optedIntoHMR
? 'Processing HMR change'
: 'Processing HMR change (no client opt-in)',
}),
);
const sendFns = [...group.clients].map((client: Client) => client.sendFn);
send(sendFns, {
type: 'update-start',
body: options,
});
const message = await this._prepareMessage(group, options, changeEvent);
send(sendFns, message);
send(sendFns, {type: 'update-done'});
log({
...createActionEndEntry(processingHmrChange),
outdated_modules:
message.type === 'update'
? message.body.added.length + message.body.modified.length
: undefined,
});
if (logger) {
logger.point('hmrPrepareAndSendMessage_end');
logger.end('SUCCESS');
}
}
async _prepareMessage(
group: ClientGroup,
options: {isInitialUpdate: boolean},
changeEvent: ?{
logger: ?RootPerfLogger,
},
): Promise<HmrUpdateMessage | HmrErrorMessage> {
const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
try {
const revPromise = this._bundler.getRevision(group.revisionId);
if (!revPromise) {
return {
type: 'error',
body: formatBundlingError(
new RevisionNotFoundError(group.revisionId),
),
};
}
logger?.point('updateGraph_start');
const {revision, delta} = await this._bundler.updateGraph(
await revPromise,
false,
);
logger?.point('updateGraph_end');
this._clientGroups.delete(group.revisionId);
group.revisionId = revision.id;
for (const client of group.clients) {
client.revisionIds = client.revisionIds.filter(
revisionId => revisionId !== group.revisionId,
);
client.revisionIds.push(revision.id);
}
this._clientGroups.set(group.revisionId, group);
logger?.point('serialize_start');
const hmrUpdate = hmrJSBundle(delta, revision.graph, {
clientUrl: group.clientUrl,
createModuleId: this._createModuleId,
includeAsyncPaths: group.graphOptions.lazy,
projectRoot: this._config.projectRoot,
serverRoot:
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
});
logger?.point('serialize_end');
return {
type: 'update',
body: {
revisionId: revision.id,
isInitialUpdate: options.isInitialUpdate,
...hmrUpdate,
},
};
} catch (error) {
const formattedError = formatBundlingError(error);
this._config.reporter.update({type: 'bundling_error', error});
return {type: 'error', body: formattedError};
}
}
}
module.exports = HmrServer;

View File

@@ -0,0 +1,98 @@
/**
* 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
*/
import type Bundler from './Bundler';
import type {
Options as DeltaBundlerOptions,
ReadOnlyDependencies,
TransformInputOptions,
} from './DeltaBundler/types';
import type {GraphId} from './lib/getGraphId';
import type {ConfigT} from 'metro-config';
import DeltaBundler, {DeltaResult, Graph, Module} from './DeltaBundler';
import {ResolverInputOptions} from './shared/types';
export type RevisionId = string;
export type OutputGraph = Graph<void>;
export interface OtherOptions {
readonly onProgress: DeltaBundlerOptions<void>['onProgress'];
readonly shallow: boolean;
}
export interface GraphRevision {
readonly id: RevisionId;
readonly date: Date;
readonly graphId: GraphId;
readonly graph: OutputGraph;
readonly prepend: ReadonlyArray<Module<void>>;
}
export interface IncrementalBundlerOptions {
readonly hasReducedPerformance?: boolean;
readonly watch?: boolean;
}
export default class IncrementalBundler {
static revisionIdFromString: (str: string) => RevisionId;
constructor(config: ConfigT, options?: IncrementalBundlerOptions);
end(): void;
getBundler(): Bundler;
getDeltaBundler(): DeltaBundler<void>;
getRevision(revisionId: RevisionId): Promise<GraphRevision> | null;
getRevisionByGraphId(graphId: GraphId): Promise<GraphRevision> | null;
buildGraphForEntries(
entryFiles: ReadonlyArray<string>,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions,
): Promise<OutputGraph>;
getDependencies(
entryFiles: ReadonlyArray<string>,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions,
): Promise<ReadOnlyDependencies<void>>;
buildGraph(
entryFile: string,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions,
): Promise<
Readonly<{graph: OutputGraph; prepend: ReadonlyArray<Module<void>>}>
>;
initializeGraph(
entryFile: string,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions,
): Promise<{
delta: DeltaResult<void>;
revision: GraphRevision;
}>;
updateGraph(
revision: GraphRevision,
reset: boolean,
): Promise<{
delta: DeltaResult<void>;
revision: GraphRevision;
}>;
endGraph(graphId: GraphId): Promise<void>;
ready(): Promise<void>;
}

View File

@@ -0,0 +1,269 @@
"use strict";
const Bundler = require("./Bundler");
const DeltaBundler = require("./DeltaBundler");
const ResourceNotFoundError = require("./IncrementalBundler/ResourceNotFoundError");
const getGraphId = require("./lib/getGraphId");
const getPrependedScripts = require("./lib/getPrependedScripts");
const transformHelpers = require("./lib/transformHelpers");
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
function createRevisionId() {
return crypto.randomBytes(8).toString("hex");
}
function revisionIdFromString(str) {
return str;
}
class IncrementalBundler {
_revisionsById = new Map();
_revisionsByGraphId = new Map();
static revisionIdFromString = revisionIdFromString;
constructor(config, options) {
this._config = config;
this._bundler = new Bundler(config, options);
this._deltaBundler = new DeltaBundler(this._bundler.getWatcher());
}
end() {
this._deltaBundler.end();
this._bundler.end();
}
getBundler() {
return this._bundler;
}
getDeltaBundler() {
return this._deltaBundler;
}
getRevision(revisionId) {
return this._revisionsById.get(revisionId);
}
getRevisionByGraphId(graphId) {
return this._revisionsByGraphId.get(graphId);
}
async buildGraphForEntries(
entryFiles,
transformOptions,
resolverOptions,
otherOptions = {
onProgress: null,
shallow: false,
lazy: false,
}
) {
const absoluteEntryFiles = await this._getAbsoluteEntryFiles(entryFiles);
const graph = await this._deltaBundler.buildGraph(absoluteEntryFiles, {
resolve: await transformHelpers.getResolveDependencyFn(
this._bundler,
transformOptions.platform,
resolverOptions
),
transform: await transformHelpers.getTransformFn(
absoluteEntryFiles,
this._bundler,
this._deltaBundler,
this._config,
transformOptions,
resolverOptions
),
transformOptions,
onProgress: otherOptions.onProgress,
lazy: otherOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
unstable_enablePackageExports:
this._config.resolver.unstable_enablePackageExports,
shallow: otherOptions.shallow,
});
this._config.serializer.experimentalSerializerHook(graph, {
added: graph.dependencies,
modified: new Map(),
deleted: new Set(),
reset: true,
});
return graph;
}
async getDependencies(
entryFiles,
transformOptions,
resolverOptions,
otherOptions = {
onProgress: null,
shallow: false,
lazy: false,
}
) {
const absoluteEntryFiles = await this._getAbsoluteEntryFiles(entryFiles);
const dependencies = await this._deltaBundler.getDependencies(
absoluteEntryFiles,
{
resolve: await transformHelpers.getResolveDependencyFn(
this._bundler,
transformOptions.platform,
resolverOptions
),
transform: await transformHelpers.getTransformFn(
absoluteEntryFiles,
this._bundler,
this._deltaBundler,
this._config,
transformOptions,
resolverOptions
),
transformOptions,
onProgress: otherOptions.onProgress,
lazy: otherOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
unstable_enablePackageExports:
this._config.resolver.unstable_enablePackageExports,
shallow: otherOptions.shallow,
}
);
return dependencies;
}
async buildGraph(
entryFile,
transformOptions,
resolverOptions,
otherOptions = {
onProgress: null,
shallow: false,
lazy: false,
}
) {
const graph = await this.buildGraphForEntries(
[entryFile],
transformOptions,
resolverOptions,
otherOptions
);
const { type: _, ...transformOptionsWithoutType } = transformOptions;
const prepend = await getPrependedScripts(
this._config,
transformOptionsWithoutType,
resolverOptions,
this._bundler,
this._deltaBundler
);
return {
prepend,
graph,
};
}
async initializeGraph(
entryFile,
transformOptions,
resolverOptions,
otherOptions = {
onProgress: null,
shallow: false,
lazy: false,
}
) {
const graphId = getGraphId(entryFile, transformOptions, {
resolverOptions,
shallow: otherOptions.shallow,
lazy: otherOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
});
const revisionId = createRevisionId();
const revisionPromise = (async () => {
const { graph, prepend } = await this.buildGraph(
entryFile,
transformOptions,
resolverOptions,
otherOptions
);
return {
id: revisionId,
date: new Date(),
graphId,
graph,
prepend,
};
})();
this._revisionsById.set(revisionId, revisionPromise);
this._revisionsByGraphId.set(graphId, revisionPromise);
try {
const revision = await revisionPromise;
const delta = {
added: revision.graph.dependencies,
modified: new Map(),
deleted: new Set(),
reset: true,
};
return {
revision,
delta,
};
} catch (err) {
this._revisionsById.delete(revisionId);
this._revisionsByGraphId.delete(graphId);
throw err;
}
}
async updateGraph(revision, reset) {
const delta = await this._deltaBundler.getDelta(revision.graph, {
reset,
shallow: false,
});
this._config.serializer.experimentalSerializerHook(revision.graph, delta);
if (
delta.added.size > 0 ||
delta.modified.size > 0 ||
delta.deleted.size > 0
) {
this._revisionsById.delete(revision.id);
revision = {
...revision,
id: crypto.randomBytes(8).toString("hex"),
date: new Date(),
};
const revisionPromise = Promise.resolve(revision);
this._revisionsById.set(revision.id, revisionPromise);
this._revisionsByGraphId.set(revision.graphId, revisionPromise);
}
return {
revision,
delta,
};
}
async endGraph(graphId) {
const revPromise = this._revisionsByGraphId.get(graphId);
if (!revPromise) {
return;
}
const revision = await revPromise;
this._deltaBundler.endGraph(revision.graph);
this._revisionsByGraphId.delete(graphId);
this._revisionsById.delete(revision.id);
}
async _getAbsoluteEntryFiles(entryFiles) {
const absoluteEntryFiles = entryFiles.map((entryFile) =>
path.resolve(
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
entryFile
)
);
await Promise.all(
absoluteEntryFiles.map(
(entryFile) =>
new Promise((resolve, reject) => {
fs.realpath(entryFile, (err) => {
if (err) {
reject(new ResourceNotFoundError(entryFile));
} else {
resolve();
}
});
})
)
);
return absoluteEntryFiles;
}
async ready() {
await this._bundler.ready();
}
}
module.exports = IncrementalBundler;

View File

@@ -0,0 +1,371 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {DeltaResult, Graph, MixedOutput, Module} from './DeltaBundler';
import type {
Options as DeltaBundlerOptions,
ReadOnlyDependencies,
TransformInputOptions,
} from './DeltaBundler/types.flow';
import type {GraphId} from './lib/getGraphId';
import type {ResolverInputOptions} from './shared/types.flow';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
const Bundler = require('./Bundler');
const DeltaBundler = require('./DeltaBundler');
const ResourceNotFoundError = require('./IncrementalBundler/ResourceNotFoundError');
const getGraphId = require('./lib/getGraphId');
const getPrependedScripts = require('./lib/getPrependedScripts');
const transformHelpers = require('./lib/transformHelpers');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
export opaque type RevisionId: string = string;
export type OutputGraph = Graph<>;
type OtherOptions = $ReadOnly<{
onProgress: $PropertyType<DeltaBundlerOptions<>, 'onProgress'>,
shallow: boolean,
lazy: boolean,
}>;
export type GraphRevision = {
// Identifies the last computed revision.
+id: RevisionId,
+date: Date,
+graphId: GraphId,
+graph: OutputGraph,
+prepend: $ReadOnlyArray<Module<>>,
};
export type IncrementalBundlerOptions = $ReadOnly<{
hasReducedPerformance?: boolean,
watch?: boolean,
}>;
function createRevisionId(): RevisionId {
return crypto.randomBytes(8).toString('hex');
}
function revisionIdFromString(str: string): RevisionId {
return str;
}
class IncrementalBundler {
_config: ConfigT;
_bundler: Bundler;
_deltaBundler: DeltaBundler<>;
_revisionsById: Map<RevisionId, Promise<GraphRevision>> = new Map();
_revisionsByGraphId: Map<GraphId, Promise<GraphRevision>> = new Map();
static revisionIdFromString: (str: string) => RevisionId =
revisionIdFromString;
constructor(config: ConfigT, options?: IncrementalBundlerOptions) {
this._config = config;
this._bundler = new Bundler(config, options);
this._deltaBundler = new DeltaBundler(this._bundler.getWatcher());
}
end(): void {
this._deltaBundler.end();
// $FlowFixMe[unused-promise]
this._bundler.end();
}
getBundler(): Bundler {
return this._bundler;
}
getDeltaBundler(): DeltaBundler<> {
return this._deltaBundler;
}
getRevision(revisionId: RevisionId): ?Promise<GraphRevision> {
return this._revisionsById.get(revisionId);
}
getRevisionByGraphId(graphId: GraphId): ?Promise<GraphRevision> {
return this._revisionsByGraphId.get(graphId);
}
async buildGraphForEntries(
entryFiles: $ReadOnlyArray<string>,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions = {
onProgress: null,
shallow: false,
lazy: false,
},
): Promise<OutputGraph> {
const absoluteEntryFiles = await this._getAbsoluteEntryFiles(entryFiles);
const graph = await this._deltaBundler.buildGraph(absoluteEntryFiles, {
resolve: await transformHelpers.getResolveDependencyFn(
this._bundler,
transformOptions.platform,
resolverOptions,
),
transform: await transformHelpers.getTransformFn(
absoluteEntryFiles,
this._bundler,
this._deltaBundler,
this._config,
transformOptions,
resolverOptions,
),
transformOptions,
onProgress: otherOptions.onProgress,
lazy: otherOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
unstable_enablePackageExports:
this._config.resolver.unstable_enablePackageExports,
shallow: otherOptions.shallow,
});
this._config.serializer.experimentalSerializerHook(graph, {
added: graph.dependencies,
modified: new Map(),
deleted: new Set(),
reset: true,
});
return graph;
}
async getDependencies(
entryFiles: $ReadOnlyArray<string>,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions = {
onProgress: null,
shallow: false,
lazy: false,
},
): Promise<ReadOnlyDependencies<>> {
const absoluteEntryFiles = await this._getAbsoluteEntryFiles(entryFiles);
const dependencies = await this._deltaBundler.getDependencies(
absoluteEntryFiles,
{
resolve: await transformHelpers.getResolveDependencyFn(
this._bundler,
transformOptions.platform,
resolverOptions,
),
transform: await transformHelpers.getTransformFn(
absoluteEntryFiles,
this._bundler,
this._deltaBundler,
this._config,
transformOptions,
resolverOptions,
),
transformOptions,
onProgress: otherOptions.onProgress,
lazy: otherOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
unstable_enablePackageExports:
this._config.resolver.unstable_enablePackageExports,
shallow: otherOptions.shallow,
},
);
return dependencies;
}
async buildGraph(
entryFile: string,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions = {
onProgress: null,
shallow: false,
lazy: false,
},
): Promise<{+graph: OutputGraph, +prepend: $ReadOnlyArray<Module<>>}> {
const graph = await this.buildGraphForEntries(
[entryFile],
transformOptions,
resolverOptions,
otherOptions,
);
const {type: _, ...transformOptionsWithoutType} = transformOptions;
const prepend = await getPrependedScripts(
this._config,
transformOptionsWithoutType,
resolverOptions,
this._bundler,
this._deltaBundler,
);
return {
prepend,
graph,
};
}
// TODO T34760750 (alexkirsz) Eventually, I'd like to get to a point where
// this class exposes only initializeGraph and updateGraph.
async initializeGraph(
entryFile: string,
transformOptions: TransformInputOptions,
resolverOptions: ResolverInputOptions,
otherOptions?: OtherOptions = {
onProgress: null,
shallow: false,
lazy: false,
},
): Promise<{
delta: DeltaResult<>,
revision: GraphRevision,
...
}> {
const graphId = getGraphId(entryFile, transformOptions, {
resolverOptions,
shallow: otherOptions.shallow,
lazy: otherOptions.lazy,
unstable_allowRequireContext:
this._config.transformer.unstable_allowRequireContext,
});
const revisionId = createRevisionId();
const revisionPromise = (async () => {
const {graph, prepend} = await this.buildGraph(
entryFile,
transformOptions,
resolverOptions,
otherOptions,
);
return {
id: revisionId,
date: new Date(),
graphId,
graph,
prepend,
};
})();
this._revisionsById.set(revisionId, revisionPromise);
this._revisionsByGraphId.set(graphId, revisionPromise);
try {
const revision = await revisionPromise;
const delta = {
added: revision.graph.dependencies,
modified: new Map<string, Module<MixedOutput>>(),
deleted: new Set<string>(),
reset: true,
};
return {
revision,
delta,
};
} catch (err) {
// Evict a bad revision from the cache since otherwise
// we'll keep getting it even after the build is fixed.
this._revisionsById.delete(revisionId);
this._revisionsByGraphId.delete(graphId);
throw err;
}
}
async updateGraph(
revision: GraphRevision,
reset: boolean,
): Promise<{
delta: DeltaResult<>,
revision: GraphRevision,
...
}> {
const delta = await this._deltaBundler.getDelta(revision.graph, {
reset,
shallow: false,
});
this._config.serializer.experimentalSerializerHook(revision.graph, delta);
if (
delta.added.size > 0 ||
delta.modified.size > 0 ||
delta.deleted.size > 0
) {
this._revisionsById.delete(revision.id);
revision = {
...revision,
// Generate a new revision id, to be used to verify the next incremental
// request.
id: crypto.randomBytes(8).toString('hex'),
date: new Date(),
};
const revisionPromise = Promise.resolve(revision);
this._revisionsById.set(revision.id, revisionPromise);
this._revisionsByGraphId.set(revision.graphId, revisionPromise);
}
return {revision, delta};
}
async endGraph(graphId: GraphId): Promise<void> {
const revPromise = this._revisionsByGraphId.get(graphId);
if (!revPromise) {
return;
}
const revision = await revPromise;
this._deltaBundler.endGraph(revision.graph);
this._revisionsByGraphId.delete(graphId);
this._revisionsById.delete(revision.id);
}
async _getAbsoluteEntryFiles(
entryFiles: $ReadOnlyArray<string>,
): Promise<$ReadOnlyArray<string>> {
const absoluteEntryFiles = entryFiles.map((entryFile: string) =>
path.resolve(
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
entryFile,
),
);
await Promise.all(
absoluteEntryFiles.map(
(entryFile: string) =>
new Promise((resolve: void => void, reject: mixed => mixed) => {
// This should throw an error if the file doesn't exist.
// Using this instead of fs.exists to account for SimLinks.
fs.realpath(entryFile, err => {
if (err) {
reject(new ResourceNotFoundError(entryFile));
} else {
resolve();
}
});
}),
),
);
return absoluteEntryFiles;
}
// Wait for the bundler to become ready.
async ready(): Promise<void> {
await this._bundler.ready();
}
}
module.exports = IncrementalBundler;

View File

@@ -0,0 +1,9 @@
"use strict";
class GraphNotFoundError extends Error {
constructor(graphId) {
super(`The graph \`${graphId}\` was not found.`);
this.graphId = graphId;
}
}
module.exports = GraphNotFoundError;

View File

@@ -0,0 +1,25 @@
/**
* 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 {GraphId} from '../lib/getGraphId';
class GraphNotFoundError extends Error {
graphId: GraphId;
constructor(graphId: GraphId) {
super(`The graph \`${graphId}\` was not found.`);
this.graphId = graphId;
}
}
module.exports = GraphNotFoundError;

View File

@@ -0,0 +1,9 @@
"use strict";
class ResourceNotFoundError extends Error {
constructor(resourcePath) {
super(`The resource \`${resourcePath}\` was not found.`);
this.resourcePath = resourcePath;
}
}
module.exports = ResourceNotFoundError;

View File

@@ -0,0 +1,23 @@
/**
* 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';
class ResourceNotFoundError extends Error {
resourcePath: string;
constructor(resourcePath: string) {
super(`The resource \`${resourcePath}\` was not found.`);
this.resourcePath = resourcePath;
}
}
module.exports = ResourceNotFoundError;

View File

@@ -0,0 +1,9 @@
"use strict";
class RevisionNotFoundError extends Error {
constructor(revisionId) {
super(`The revision \`${revisionId}\` was not found.`);
this.revisionId = revisionId;
}
}
module.exports = RevisionNotFoundError;

View File

@@ -0,0 +1,25 @@
/**
* 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 {RevisionId} from '../IncrementalBundler';
class RevisionNotFoundError extends Error {
revisionId: RevisionId;
constructor(revisionId: RevisionId) {
super(`The revision \`${revisionId}\` was not found.`);
this.revisionId = revisionId;
}
}
module.exports = RevisionNotFoundError;

View File

@@ -0,0 +1,70 @@
"use strict";
const generate = require("@babel/generator").default;
const { toMatchSnapshot } = require("jest-snapshot");
const generateOptions = {
concise: true,
sourceType: "module",
};
const codeFromAst = (ast) => generate(ast, generateOptions).code;
const comparableCode = (code) => code.trim().replace(/\s+/g, " ");
function toEqualComparableCode(received, expected) {
const comparableExpected = comparableCode(expected);
const pass = received === comparableExpected;
const options = {
isNot: this.isNot,
promise: this.promise,
};
const message = pass
? () =>
this.utils.matcherHint(
"toEqualComparableCode",
undefined,
undefined,
options
) +
"\n\n" +
`Expected: not ${this.utils.printExpected(comparableExpected)}\n` +
`Received: ${this.utils.printReceived(received)}`
: () => {
const diffString = this.utils.printDiffOrStringify(
comparableExpected,
received,
"expected",
"received",
this.expand
);
return (
this.utils.matcherHint(
"toEqualComparableCode",
undefined,
undefined,
options
) +
"\n\n" +
diffString
);
};
return {
actual: received,
message,
pass,
};
}
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
function trimANSICodes(input) {
return input.replace(ANSI_PATTERN, "");
}
function toMatchCodeFrameSnapshot(received) {
return toMatchSnapshot.call(
this,
trimANSICodes(received),
"toMatchCodeFrameSnapshot"
);
}
module.exports = {
codeFromAst,
comparableCode,
toEqualComparableCode,
toMatchCodeFrameSnapshot,
};

View File

@@ -0,0 +1,138 @@
"use strict";
var _template = _interopRequireDefault(require("@babel/template"));
var _traverse = _interopRequireDefault(require("@babel/traverse"));
var t = _interopRequireWildcard(require("@babel/types"));
var _invariant = _interopRequireDefault(require("invariant"));
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function (nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interopRequireWildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
return { default: obj };
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {};
var hasPropertyDescriptor =
Object.defineProperty && Object.getOwnPropertyDescriptor;
for (var key in obj) {
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor
? Object.getOwnPropertyDescriptor(obj, key)
: null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const WRAP_NAME = "$$_REQUIRE";
const IIFE_PARAM = _template.default.expression(
"typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this"
);
function wrapModule(
fileAst,
importDefaultName,
importAllName,
dependencyMapName,
globalPrefix,
skipRequireRename
) {
const params = buildParameters(
importDefaultName,
importAllName,
dependencyMapName
);
const factory = functionFromProgram(fileAst.program, params);
const def = t.callExpression(t.identifier(`${globalPrefix}__d`), [factory]);
const ast = t.file(t.program([t.expressionStatement(def)]));
const requireName = skipRequireRename ? "require" : renameRequires(ast);
return {
ast,
requireName,
};
}
function wrapPolyfill(fileAst) {
const factory = functionFromProgram(fileAst.program, ["global"]);
const iife = t.callExpression(factory, [IIFE_PARAM()]);
return t.file(t.program([t.expressionStatement(iife)]));
}
function jsonToCommonJS(source) {
return `module.exports = ${source};`;
}
function wrapJson(source, globalPrefix) {
const moduleFactoryParameters = buildParameters(
"_importDefaultUnused",
"_importAllUnused",
"_dependencyMapUnused"
);
return [
`${globalPrefix}__d(function(${moduleFactoryParameters.join(", ")}) {`,
` ${jsonToCommonJS(source)}`,
"});",
].join("\n");
}
function functionFromProgram(program, parameters) {
return t.functionExpression(
undefined,
parameters.map(makeIdentifier),
t.blockStatement(program.body, program.directives)
);
}
function makeIdentifier(name) {
return t.identifier(name);
}
function buildParameters(importDefaultName, importAllName, dependencyMapName) {
return [
"global",
"require",
importDefaultName,
importAllName,
"module",
"exports",
dependencyMapName,
];
}
function renameRequires(ast) {
let newRequireName = WRAP_NAME;
(0, _traverse.default)(ast, {
Program(path) {
const body = path.get("body.0.expression.arguments.0.body");
(0, _invariant.default)(
!Array.isArray(body),
"metro: Expected `body` to be a single path."
);
newRequireName = body.scope.generateUid(WRAP_NAME);
body.scope.rename("require", newRequireName);
},
});
return newRequireName;
}
module.exports = {
WRAP_NAME,
wrapJson,
jsonToCommonJS,
wrapModule,
wrapPolyfill,
};

View File

@@ -0,0 +1,145 @@
/**
* 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
* @flow strict-local
*/
'use strict';
import type {FunctionExpression, Identifier, Program} from '@babel/types';
import template from '@babel/template';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import invariant from 'invariant';
const WRAP_NAME = '$$_REQUIRE'; // note: babel will prefix this with _
// Check first the `global` variable as the global object. This way serializers
// can create a local variable called global to fake it as a global object
// without having to pollute the window object on web.
const IIFE_PARAM = template.expression(
"typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this",
);
function wrapModule(
fileAst: BabelNodeFile,
importDefaultName: string,
importAllName: string,
dependencyMapName: string,
globalPrefix: string,
skipRequireRename: boolean,
): {
ast: BabelNodeFile,
requireName: string,
} {
const params = buildParameters(
importDefaultName,
importAllName,
dependencyMapName,
);
const factory = functionFromProgram(fileAst.program, params);
const def = t.callExpression(t.identifier(`${globalPrefix}__d`), [factory]);
const ast = t.file(t.program([t.expressionStatement(def)]));
// `require` doesn't need to be scoped when Metro serializes to iife because the local function
// `require` will be used instead of the global one.
const requireName = skipRequireRename ? 'require' : renameRequires(ast);
return {ast, requireName};
}
function wrapPolyfill(fileAst: BabelNodeFile): BabelNodeFile {
const factory = functionFromProgram(fileAst.program, ['global']);
const iife = t.callExpression(factory, [IIFE_PARAM()]);
return t.file(t.program([t.expressionStatement(iife)]));
}
function jsonToCommonJS(source: string): string {
return `module.exports = ${source};`;
}
function wrapJson(source: string, globalPrefix: string): string {
// Unused parameters; remember that's wrapping JSON.
const moduleFactoryParameters = buildParameters(
'_importDefaultUnused',
'_importAllUnused',
'_dependencyMapUnused',
);
return [
`${globalPrefix}__d(function(${moduleFactoryParameters.join(', ')}) {`,
` ${jsonToCommonJS(source)}`,
'});',
].join('\n');
}
function functionFromProgram(
program: Program,
parameters: $ReadOnlyArray<string>,
): FunctionExpression {
return t.functionExpression(
undefined,
parameters.map(makeIdentifier),
t.blockStatement(program.body, program.directives),
);
}
function makeIdentifier(name: string): Identifier {
return t.identifier(name);
}
function buildParameters(
importDefaultName: string,
importAllName: string,
dependencyMapName: string,
): $ReadOnlyArray<string> {
return [
'global',
'require',
importDefaultName,
importAllName,
'module',
'exports',
dependencyMapName,
];
}
// Renaming requires should ideally only be done when generating for the target
// that expects the custom require name in the optimize step.
// This visitor currently renames all `require` references even if the module
// contains a custom `require` declaration. This should be fixed by only renaming
// if the `require` symbol hasn't been redeclared.
function renameRequires(ast: BabelNodeFile): string {
let newRequireName = WRAP_NAME;
traverse(ast, {
Program(path) {
const body = path.get('body.0.expression.arguments.0.body');
invariant(
!Array.isArray(body),
'metro: Expected `body` to be a single path.',
);
newRequireName = body.scope.generateUid(WRAP_NAME);
body.scope.rename('require', newRequireName);
},
});
return newRequireName;
}
module.exports = {
WRAP_NAME,
wrapJson,
jsonToCommonJS,
wrapModule,
wrapPolyfill,
};

View File

@@ -0,0 +1,27 @@
/**
* 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
*/
export type ContextMode = 'sync' | 'eager' | 'lazy' | 'lazy-once';
export interface ContextFilter {
pattern: string;
flags: string;
}
export interface RequireContextParams {
/* Should search for files recursively. Optional, default `true` when `require.context` is used */
readonly recursive: boolean;
/* Filename filter pattern for use in `require.context`. Optional, default `.*` (any file) when `require.context` is used */
readonly filter: Readonly<ContextFilter>;
/** Mode for resolving dynamic dependencies. Defaults to `sync` */
readonly mode: ContextMode;
}
export type DynamicRequiresBehavior = 'throwAtRuntime' | 'reject';

View File

@@ -0,0 +1,552 @@
"use strict";
const generate = require("@babel/generator").default;
const template = require("@babel/template").default;
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");
const crypto = require("crypto");
const nullthrows = require("nullthrows");
const { isImport } = types;
function collectDependencies(ast, options) {
const visited = new WeakSet();
const state = {
asyncRequireModulePathStringLiteral: null,
dependencyCalls: new Set(),
dependencyRegistry: new DependencyRegistry(),
dependencyTransformer:
options.dependencyTransformer ?? DefaultDependencyTransformer,
dependencyMapIdentifier: null,
dynamicRequires: options.dynamicRequires,
keepRequireNames: options.keepRequireNames,
allowOptionalDependencies: options.allowOptionalDependencies,
unstable_allowRequireContext: options.unstable_allowRequireContext,
};
const visitor = {
CallExpression(path, state) {
if (visited.has(path.node)) {
return;
}
const callee = path.node.callee;
const name = callee.type === "Identifier" ? callee.name : null;
if (isImport(callee)) {
processImportCall(path, state, {
asyncType: "async",
});
return;
}
if (name === "__prefetchImport" && !path.scope.getBinding(name)) {
processImportCall(path, state, {
asyncType: "prefetch",
});
return;
}
if (
state.unstable_allowRequireContext &&
callee.type === "MemberExpression" &&
callee.object.type === "Identifier" &&
callee.object.name === "require" &&
callee.property.type === "Identifier" &&
callee.property.name === "context" &&
!callee.computed &&
!path.scope.getBinding("require")
) {
processRequireContextCall(path, state);
visited.add(path.node);
return;
}
if (
callee.type === "MemberExpression" &&
callee.object.type === "Identifier" &&
callee.object.name === "require" &&
callee.property.type === "Identifier" &&
callee.property.name === "resolveWeak" &&
!callee.computed &&
!path.scope.getBinding("require")
) {
processResolveWeakCall(path, state);
visited.add(path.node);
return;
}
if (
callee.type === "MemberExpression" &&
callee.object.type === "Identifier" &&
callee.object.name === "require" &&
callee.property.type === "Identifier" &&
callee.property.name === "unstable_importMaybeSync" &&
!callee.computed &&
!path.scope.getBinding("require")
) {
processImportCall(path, state, {
asyncType: "maybeSync",
});
visited.add(path.node);
return;
}
if (
name != null &&
state.dependencyCalls.has(name) &&
!path.scope.getBinding(name)
) {
processRequireCall(path, state);
visited.add(path.node);
}
},
ImportDeclaration: collectImports,
ExportNamedDeclaration: collectImports,
ExportAllDeclaration: collectImports,
Program(path, state) {
state.asyncRequireModulePathStringLiteral = types.stringLiteral(
options.asyncRequireModulePath
);
if (options.dependencyMapName != null) {
state.dependencyMapIdentifier = types.identifier(
options.dependencyMapName
);
} else {
state.dependencyMapIdentifier =
path.scope.generateUidIdentifier("dependencyMap");
}
state.dependencyCalls = new Set(["require", ...options.inlineableCalls]);
},
};
traverse(ast, visitor, null, state);
const collectedDependencies = state.dependencyRegistry.getDependencies();
const dependencies = new Array(collectedDependencies.length);
for (const { index, name, ...dependencyData } of collectedDependencies) {
dependencies[index] = {
name,
data: dependencyData,
};
}
return {
ast,
dependencies,
dependencyMapName: nullthrows(state.dependencyMapIdentifier).name,
};
}
function getRequireContextArgs(path) {
const args = path.get("arguments");
let directory;
if (!Array.isArray(args) || args.length < 1) {
throw new InvalidRequireCallError(path);
} else {
const result = args[0].evaluate();
if (result.confident && typeof result.value === "string") {
directory = result.value;
} else {
throw new InvalidRequireCallError(
result.deopt ?? args[0],
"First argument of `require.context` should be a string denoting the directory to require."
);
}
}
let recursive = true;
if (args.length > 1) {
const result = args[1].evaluate();
if (result.confident && typeof result.value === "boolean") {
recursive = result.value;
} else if (!(result.confident && typeof result.value === "undefined")) {
throw new InvalidRequireCallError(
result.deopt ?? args[1],
"Second argument of `require.context` should be an optional boolean indicating if files should be imported recursively or not."
);
}
}
let filter = {
pattern: ".*",
flags: "",
};
if (args.length > 2) {
const result = args[2].evaluate();
const argNode = args[2].node;
if (argNode.type === "RegExpLiteral") {
filter = {
pattern: argNode.pattern,
flags: argNode.flags || "",
};
} else if (!(result.confident && typeof result.value === "undefined")) {
throw new InvalidRequireCallError(
args[2],
`Third argument of \`require.context\` should be an optional RegExp pattern matching all of the files to import, instead found node of type: ${argNode.type}.`
);
}
}
let mode = "sync";
if (args.length > 3) {
const result = args[3].evaluate();
if (result.confident && typeof result.value === "string") {
mode = getContextMode(args[3], result.value);
} else if (!(result.confident && typeof result.value === "undefined")) {
throw new InvalidRequireCallError(
result.deopt ?? args[3],
'Fourth argument of `require.context` should be an optional string "mode" denoting how the modules will be resolved.'
);
}
}
if (args.length > 4) {
throw new InvalidRequireCallError(
path,
`Too many arguments provided to \`require.context\` call. Expected 4, got: ${args.length}`
);
}
return [
directory,
{
recursive,
filter,
mode,
},
];
}
function getContextMode(path, mode) {
if (
mode === "sync" ||
mode === "eager" ||
mode === "lazy" ||
mode === "lazy-once"
) {
return mode;
}
throw new InvalidRequireCallError(
path,
`require.context "${mode}" mode is not supported. Expected one of: sync, eager, lazy, lazy-once`
);
}
function processRequireContextCall(path, state) {
const [directory, contextParams] = getRequireContextArgs(path);
const transformer = state.dependencyTransformer;
const dep = registerDependency(
state,
{
name: directory,
contextParams,
asyncType: null,
optional: isOptionalDependency(directory, path, state),
},
path
);
path.get("callee").replaceWith(types.identifier("require"));
transformer.transformSyncRequire(path, dep, state);
}
function processResolveWeakCall(path, state) {
const name = getModuleNameFromCallArgs(path);
if (name == null) {
throw new InvalidRequireCallError(path);
}
const dependency = registerDependency(
state,
{
name,
asyncType: "weak",
optional: isOptionalDependency(name, path, state),
},
path
);
path.replaceWith(
makeResolveWeakTemplate({
MODULE_ID: createModuleIDExpression(dependency, state),
})
);
}
function collectImports(path, state) {
if (path.node.source) {
registerDependency(
state,
{
name: path.node.source.value,
asyncType: null,
optional: false,
},
path
);
}
}
function processImportCall(path, state, options) {
const name = getModuleNameFromCallArgs(path);
if (name == null) {
throw new InvalidRequireCallError(path);
}
const dep = registerDependency(
state,
{
name,
asyncType: options.asyncType,
optional: isOptionalDependency(name, path, state),
},
path
);
const transformer = state.dependencyTransformer;
switch (options.asyncType) {
case "async":
transformer.transformImportCall(path, dep, state);
break;
case "maybeSync":
transformer.transformImportMaybeSyncCall(path, dep, state);
break;
case "prefetch":
transformer.transformPrefetch(path, dep, state);
break;
case "weak":
throw new Error("Unreachable");
default:
options.asyncType;
throw new Error("Unreachable");
}
}
function processRequireCall(path, state) {
const name = getModuleNameFromCallArgs(path);
const transformer = state.dependencyTransformer;
if (name == null) {
if (state.dynamicRequires === "reject") {
throw new InvalidRequireCallError(path);
}
transformer.transformIllegalDynamicRequire(path, state);
return;
}
const dep = registerDependency(
state,
{
name,
asyncType: null,
optional: isOptionalDependency(name, path, state),
},
path
);
transformer.transformSyncRequire(path, dep, state);
}
function getNearestLocFromPath(path) {
let current = path;
while (
current &&
!current.node.loc &&
!current.node.METRO_INLINE_REQUIRES_INIT_LOC
) {
current = current.parentPath;
}
return current?.node.METRO_INLINE_REQUIRES_INIT_LOC ?? current?.node.loc;
}
function registerDependency(state, qualifier, path) {
const dependency = state.dependencyRegistry.registerDependency(qualifier);
const loc = getNearestLocFromPath(path);
if (loc != null) {
dependency.locs.push(loc);
}
return dependency;
}
function isOptionalDependency(name, path, state) {
const { allowOptionalDependencies } = state;
if (name === state.asyncRequireModulePathStringLiteral?.value) {
return false;
}
const isExcluded = () =>
Array.isArray(allowOptionalDependencies.exclude) &&
allowOptionalDependencies.exclude.includes(name);
if (!allowOptionalDependencies || isExcluded()) {
return false;
}
let sCount = 0;
let p = path;
while (p && sCount < 3) {
if (p.isStatement()) {
if (p.node.type === "BlockStatement") {
return (
p.parentPath != null &&
p.parentPath.node.type === "TryStatement" &&
p.key === "block"
);
}
sCount += 1;
}
p = p.parentPath;
}
return false;
}
function getModuleNameFromCallArgs(path) {
const args = path.get("arguments");
if (!Array.isArray(args) || args.length !== 1) {
throw new InvalidRequireCallError(path);
}
const result = args[0].evaluate();
if (result.confident && typeof result.value === "string") {
return result.value;
}
return null;
}
collectDependencies.getModuleNameFromCallArgs = getModuleNameFromCallArgs;
class InvalidRequireCallError extends Error {
constructor({ node }, message) {
const line = node.loc && node.loc.start && node.loc.start.line;
super(
[
`Invalid call at line ${line || "<unknown>"}: ${generate(node).code}`,
message,
]
.filter(Boolean)
.join("\n")
);
}
}
collectDependencies.InvalidRequireCallError = InvalidRequireCallError;
const dynamicRequireErrorTemplate = template.expression(`
(function(line) {
throw new Error(
'Dynamic require defined at line ' + line + '; not supported by Metro',
);
})(LINE)
`);
const makeAsyncRequireTemplate = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH)(MODULE_ID, DEPENDENCY_MAP.paths)
`);
const makeAsyncRequireTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH)(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);
const makeAsyncPrefetchTemplate = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, DEPENDENCY_MAP.paths)
`);
const makeAsyncPrefetchTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);
const makeAsyncImportMaybeSyncTemplate = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).unstable_importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths)
`);
const makeAsyncImportMaybeSyncTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).unstable_importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);
const makeResolveWeakTemplate = template.expression(`
MODULE_ID
`);
const DefaultDependencyTransformer = {
transformSyncRequire(path, dependency, state) {
const moduleIDExpression = createModuleIDExpression(dependency, state);
path.node.arguments = [moduleIDExpression];
if (state.keepRequireNames) {
path.node.arguments.push(types.stringLiteral(dependency.name));
}
},
transformImportCall(path, dependency, state) {
const makeNode = state.keepRequireNames
? makeAsyncRequireTemplateWithName
: makeAsyncRequireTemplate;
const opts = {
ASYNC_REQUIRE_MODULE_PATH: nullthrows(
state.asyncRequireModulePathStringLiteral
),
MODULE_ID: createModuleIDExpression(dependency, state),
DEPENDENCY_MAP: nullthrows(state.dependencyMapIdentifier),
...(state.keepRequireNames
? {
MODULE_NAME: createModuleNameLiteral(dependency),
}
: null),
};
path.replaceWith(makeNode(opts));
},
transformImportMaybeSyncCall(path, dependency, state) {
const makeNode = state.keepRequireNames
? makeAsyncImportMaybeSyncTemplateWithName
: makeAsyncImportMaybeSyncTemplate;
const opts = {
ASYNC_REQUIRE_MODULE_PATH: nullthrows(
state.asyncRequireModulePathStringLiteral
),
MODULE_ID: createModuleIDExpression(dependency, state),
DEPENDENCY_MAP: nullthrows(state.dependencyMapIdentifier),
...(state.keepRequireNames
? {
MODULE_NAME: createModuleNameLiteral(dependency),
}
: null),
};
path.replaceWith(makeNode(opts));
},
transformPrefetch(path, dependency, state) {
const makeNode = state.keepRequireNames
? makeAsyncPrefetchTemplateWithName
: makeAsyncPrefetchTemplate;
const opts = {
ASYNC_REQUIRE_MODULE_PATH: nullthrows(
state.asyncRequireModulePathStringLiteral
),
MODULE_ID: createModuleIDExpression(dependency, state),
DEPENDENCY_MAP: nullthrows(state.dependencyMapIdentifier),
...(state.keepRequireNames
? {
MODULE_NAME: createModuleNameLiteral(dependency),
}
: null),
};
path.replaceWith(makeNode(opts));
},
transformIllegalDynamicRequire(path, state) {
path.replaceWith(
dynamicRequireErrorTemplate({
LINE: types.numericLiteral(path.node.loc?.start.line ?? 0),
})
);
},
};
function createModuleIDExpression(dependency, state) {
return types.memberExpression(
nullthrows(state.dependencyMapIdentifier),
types.numericLiteral(dependency.index),
true
);
}
function createModuleNameLiteral(dependency) {
return types.stringLiteral(dependency.name);
}
function getKeyForDependency(qualifier) {
let key = qualifier.name;
const { asyncType } = qualifier;
if (asyncType) {
key += ["", asyncType].join("\0");
}
const { contextParams } = qualifier;
if (contextParams) {
key += [
"",
"context",
String(contextParams.recursive),
String(contextParams.filter.pattern),
String(contextParams.filter.flags),
contextParams.mode,
].join("\0");
}
return key;
}
class DependencyRegistry {
_dependencies = new Map();
registerDependency(qualifier) {
const key = getKeyForDependency(qualifier);
let dependency = this._dependencies.get(key);
if (dependency == null) {
const newDependency = {
name: qualifier.name,
asyncType: qualifier.asyncType,
locs: [],
index: this._dependencies.size,
key: crypto.createHash("sha1").update(key).digest("base64"),
};
if (qualifier.optional) {
newDependency.isOptional = true;
}
if (qualifier.contextParams) {
newDependency.contextParams = qualifier.contextParams;
}
dependency = newDependency;
} else {
if (dependency.isOptional && !qualifier.optional) {
dependency = {
...dependency,
isOptional: false,
};
}
}
this._dependencies.set(key, dependency);
return dependency;
}
getDependencies() {
return Array.from(this._dependencies.values());
}
}
module.exports = collectDependencies;

View File

@@ -0,0 +1,885 @@
/**
* 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
* @flow
*/
'use strict';
import type {NodePath} from '@babel/traverse';
import type {CallExpression, Identifier, StringLiteral} from '@babel/types';
import type {
AllowOptionalDependencies,
AsyncDependencyType,
} from 'metro/src/DeltaBundler/types.flow.js';
const generate = require('@babel/generator').default;
const template = require('@babel/template').default;
const traverse = require('@babel/traverse').default;
const types = require('@babel/types');
const crypto = require('crypto');
const nullthrows = require('nullthrows');
const {isImport} = types;
type ImportDependencyOptions = $ReadOnly<{
asyncType: AsyncDependencyType,
}>;
export type Dependency = $ReadOnly<{
data: DependencyData,
name: string,
}>;
// TODO: Convert to a Flow enum
export type ContextMode = 'sync' | 'eager' | 'lazy' | 'lazy-once';
type ContextFilter = $ReadOnly<{pattern: string, flags: string}>;
export type RequireContextParams = $ReadOnly<{
/* Should search for files recursively. Optional, default `true` when `require.context` is used */
recursive: boolean,
/* Filename filter pattern for use in `require.context`. Optional, default `.*` (any file) when `require.context` is used */
filter: $ReadOnly<ContextFilter>,
/** Mode for resolving dynamic dependencies. Defaults to `sync` */
mode: ContextMode,
}>;
type DependencyData = $ReadOnly<{
// A locally unique key for this dependency within the current module.
key: string,
// If null, then the dependency is synchronous.
// (ex. `require('foo')`)
asyncType: AsyncDependencyType | null,
isOptional?: boolean,
locs: $ReadOnlyArray<BabelSourceLocation>,
/** Context for requiring a collection of modules. */
contextParams?: RequireContextParams,
}>;
export type MutableInternalDependency = {
...DependencyData,
locs: Array<BabelSourceLocation>,
index: number,
name: string,
};
export type InternalDependency = $ReadOnly<MutableInternalDependency>;
export type State = {
asyncRequireModulePathStringLiteral: ?StringLiteral,
dependencyCalls: Set<string>,
dependencyRegistry: DependencyRegistry,
dependencyTransformer: DependencyTransformer,
dynamicRequires: DynamicRequiresBehavior,
dependencyMapIdentifier: ?Identifier,
keepRequireNames: boolean,
allowOptionalDependencies: AllowOptionalDependencies,
/** Enable `require.context` statements which can be used to import multiple files in a directory. */
unstable_allowRequireContext: boolean,
};
export type Options = $ReadOnly<{
asyncRequireModulePath: string,
dependencyMapName: ?string,
dynamicRequires: DynamicRequiresBehavior,
inlineableCalls: $ReadOnlyArray<string>,
keepRequireNames: boolean,
allowOptionalDependencies: AllowOptionalDependencies,
dependencyTransformer?: DependencyTransformer,
/** Enable `require.context` statements which can be used to import multiple files in a directory. */
unstable_allowRequireContext: boolean,
}>;
export type CollectedDependencies = $ReadOnly<{
ast: BabelNodeFile,
dependencyMapName: string,
dependencies: $ReadOnlyArray<Dependency>,
}>;
export interface DependencyTransformer {
transformSyncRequire(
path: NodePath<CallExpression>,
dependency: InternalDependency,
state: State,
): void;
transformImportCall(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void;
transformImportMaybeSyncCall(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void;
transformPrefetch(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void;
transformIllegalDynamicRequire(path: NodePath<>, state: State): void;
}
export type DynamicRequiresBehavior = 'throwAtRuntime' | 'reject';
/**
* Transform all the calls to `require()` and `import()` in a file into ID-
* independent code, and return the list of dependencies. For example, a call
* like `require('Foo')` could be transformed to `require(_depMap[3], 'Foo')`
* where `_depMap` is provided by the outer scope. As such, we don't need to
* know the actual module ID.
*
* The second argument is only provided for debugging purposes.
*/
function collectDependencies(
ast: BabelNodeFile,
options: Options,
): CollectedDependencies {
const visited = new WeakSet<BabelNodeCallExpression>();
const state: State = {
asyncRequireModulePathStringLiteral: null,
dependencyCalls: new Set(),
dependencyRegistry: new DependencyRegistry(),
dependencyTransformer:
options.dependencyTransformer ?? DefaultDependencyTransformer,
dependencyMapIdentifier: null,
dynamicRequires: options.dynamicRequires,
keepRequireNames: options.keepRequireNames,
allowOptionalDependencies: options.allowOptionalDependencies,
unstable_allowRequireContext: options.unstable_allowRequireContext,
};
const visitor = {
CallExpression(
path: NodePath<BabelNodeCallExpression>,
state: State,
): void {
if (visited.has(path.node)) {
return;
}
const callee = path.node.callee;
const name = callee.type === 'Identifier' ? callee.name : null;
if (isImport(callee)) {
processImportCall(path, state, {
asyncType: 'async',
});
return;
}
if (name === '__prefetchImport' && !path.scope.getBinding(name)) {
processImportCall(path, state, {
asyncType: 'prefetch',
});
return;
}
// Match `require.context`
if (
// Feature gate, defaults to `false`.
state.unstable_allowRequireContext &&
callee.type === 'MemberExpression' &&
// `require`
callee.object.type === 'Identifier' &&
callee.object.name === 'require' &&
// `context`
callee.property.type === 'Identifier' &&
callee.property.name === 'context' &&
!callee.computed &&
// Ensure `require` refers to the global and not something else.
!path.scope.getBinding('require')
) {
processRequireContextCall(path, state);
visited.add(path.node);
return;
}
// Match `require.resolveWeak`
if (
callee.type === 'MemberExpression' &&
// `require`
callee.object.type === 'Identifier' &&
callee.object.name === 'require' &&
// `resolveWeak`
callee.property.type === 'Identifier' &&
callee.property.name === 'resolveWeak' &&
!callee.computed &&
// Ensure `require` refers to the global and not something else.
!path.scope.getBinding('require')
) {
processResolveWeakCall(path, state);
visited.add(path.node);
return;
}
// Match `require.unstable_importMaybeSync`
if (
callee.type === 'MemberExpression' &&
// `require`
callee.object.type === 'Identifier' &&
callee.object.name === 'require' &&
// `unstable_importMaybeSync`
callee.property.type === 'Identifier' &&
callee.property.name === 'unstable_importMaybeSync' &&
!callee.computed &&
// Ensure `require` refers to the global and not something else.
!path.scope.getBinding('require')
) {
processImportCall(path, state, {
asyncType: 'maybeSync',
});
visited.add(path.node);
return;
}
if (
name != null &&
state.dependencyCalls.has(name) &&
!path.scope.getBinding(name)
) {
processRequireCall(path, state);
visited.add(path.node);
}
},
ImportDeclaration: collectImports,
ExportNamedDeclaration: collectImports,
ExportAllDeclaration: collectImports,
Program(path: NodePath<BabelNodeProgram>, state: State) {
state.asyncRequireModulePathStringLiteral = types.stringLiteral(
options.asyncRequireModulePath,
);
if (options.dependencyMapName != null) {
state.dependencyMapIdentifier = types.identifier(
options.dependencyMapName,
);
} else {
state.dependencyMapIdentifier =
path.scope.generateUidIdentifier('dependencyMap');
}
state.dependencyCalls = new Set(['require', ...options.inlineableCalls]);
},
};
traverse(ast, visitor, null, state);
const collectedDependencies = state.dependencyRegistry.getDependencies();
// Compute the list of dependencies.
const dependencies = new Array<Dependency>(collectedDependencies.length);
for (const {index, name, ...dependencyData} of collectedDependencies) {
dependencies[index] = {
name,
data: dependencyData,
};
}
return {
ast,
dependencies,
dependencyMapName: nullthrows(state.dependencyMapIdentifier).name,
};
}
/** Extract args passed to the `require.context` method. */
function getRequireContextArgs(
path: NodePath<CallExpression>,
): [string, RequireContextParams] {
const args = path.get('arguments');
let directory: string;
if (!Array.isArray(args) || args.length < 1) {
throw new InvalidRequireCallError(path);
} else {
const result = args[0].evaluate();
if (result.confident && typeof result.value === 'string') {
directory = result.value;
} else {
throw new InvalidRequireCallError(
result.deopt ?? args[0],
'First argument of `require.context` should be a string denoting the directory to require.',
);
}
}
// Default to requiring through all directories.
let recursive: boolean = true;
if (args.length > 1) {
const result = args[1].evaluate();
if (result.confident && typeof result.value === 'boolean') {
recursive = result.value;
} else if (!(result.confident && typeof result.value === 'undefined')) {
throw new InvalidRequireCallError(
result.deopt ?? args[1],
'Second argument of `require.context` should be an optional boolean indicating if files should be imported recursively or not.',
);
}
}
// Default to all files.
let filter: ContextFilter = {pattern: '.*', flags: ''};
if (args.length > 2) {
// evaluate() to check for undefined (because it's technically a scope lookup)
// but check the AST for the regex literal, since evaluate() doesn't do regex.
const result = args[2].evaluate();
const argNode = args[2].node;
if (argNode.type === 'RegExpLiteral') {
// TODO: Handle `new RegExp(...)` -- `argNode.type === 'NewExpression'`
filter = {
pattern: argNode.pattern,
flags: argNode.flags || '',
};
} else if (!(result.confident && typeof result.value === 'undefined')) {
throw new InvalidRequireCallError(
args[2],
`Third argument of \`require.context\` should be an optional RegExp pattern matching all of the files to import, instead found node of type: ${argNode.type}.`,
);
}
}
// Default to `sync`.
let mode: ContextMode = 'sync';
if (args.length > 3) {
const result = args[3].evaluate();
if (result.confident && typeof result.value === 'string') {
mode = getContextMode(args[3], result.value);
} else if (!(result.confident && typeof result.value === 'undefined')) {
throw new InvalidRequireCallError(
result.deopt ?? args[3],
'Fourth argument of `require.context` should be an optional string "mode" denoting how the modules will be resolved.',
);
}
}
if (args.length > 4) {
throw new InvalidRequireCallError(
path,
`Too many arguments provided to \`require.context\` call. Expected 4, got: ${args.length}`,
);
}
return [
directory,
{
recursive,
filter,
mode,
},
];
}
function getContextMode(path: NodePath<>, mode: string): ContextMode {
if (
mode === 'sync' ||
mode === 'eager' ||
mode === 'lazy' ||
mode === 'lazy-once'
) {
return mode;
}
throw new InvalidRequireCallError(
path,
`require.context "${mode}" mode is not supported. Expected one of: sync, eager, lazy, lazy-once`,
);
}
function processRequireContextCall(
path: NodePath<CallExpression>,
state: State,
): void {
const [directory, contextParams] = getRequireContextArgs(path);
const transformer = state.dependencyTransformer;
const dep = registerDependency(
state,
{
// We basically want to "import" every file in a folder and then filter them out with the given `filter` RegExp.
name: directory,
// Capture the matching context
contextParams,
asyncType: null,
optional: isOptionalDependency(directory, path, state),
},
path,
);
// require() the generated module representing this context
path.get('callee').replaceWith(types.identifier('require'));
transformer.transformSyncRequire(path, dep, state);
}
function processResolveWeakCall(
path: NodePath<CallExpression>,
state: State,
): void {
const name = getModuleNameFromCallArgs(path);
if (name == null) {
throw new InvalidRequireCallError(path);
}
const dependency = registerDependency(
state,
{
name,
asyncType: 'weak',
optional: isOptionalDependency(name, path, state),
},
path,
);
path.replaceWith(
makeResolveWeakTemplate({
MODULE_ID: createModuleIDExpression(dependency, state),
}),
);
}
function collectImports(path: NodePath<>, state: State): void {
if (path.node.source) {
registerDependency(
state,
{
name: path.node.source.value,
asyncType: null,
optional: false,
},
path,
);
}
}
function processImportCall(
path: NodePath<CallExpression>,
state: State,
options: ImportDependencyOptions,
): void {
const name = getModuleNameFromCallArgs(path);
if (name == null) {
throw new InvalidRequireCallError(path);
}
const dep = registerDependency(
state,
{
name,
asyncType: options.asyncType,
optional: isOptionalDependency(name, path, state),
},
path,
);
const transformer = state.dependencyTransformer;
switch (options.asyncType) {
case 'async':
transformer.transformImportCall(path, dep, state);
break;
case 'maybeSync':
transformer.transformImportMaybeSyncCall(path, dep, state);
break;
case 'prefetch':
transformer.transformPrefetch(path, dep, state);
break;
case 'weak':
throw new Error('Unreachable');
default:
options.asyncType as empty;
throw new Error('Unreachable');
}
}
function processRequireCall(
path: NodePath<CallExpression>,
state: State,
): void {
const name = getModuleNameFromCallArgs(path);
const transformer = state.dependencyTransformer;
if (name == null) {
if (state.dynamicRequires === 'reject') {
throw new InvalidRequireCallError(path);
}
transformer.transformIllegalDynamicRequire(path, state);
return;
}
const dep = registerDependency(
state,
{
name,
asyncType: null,
optional: isOptionalDependency(name, path, state),
},
path,
);
transformer.transformSyncRequire(path, dep, state);
}
function getNearestLocFromPath(path: NodePath<>): ?BabelSourceLocation {
let current: ?(NodePath<> | NodePath<BabelNode>) = path;
while (
current &&
!current.node.loc &&
// $FlowIgnore[prop-missing] METRO_INLINE_REQUIRES_INIT_LOC is Metro-specific and not typed
!current.node.METRO_INLINE_REQUIRES_INIT_LOC
) {
current = current.parentPath;
}
return (
// $FlowIgnore[prop-missing] METRO_INLINE_REQUIRES_INIT_LOC is Metro-specific and not typed
current?.node.METRO_INLINE_REQUIRES_INIT_LOC ?? current?.node.loc
);
}
export type ImportQualifier = $ReadOnly<{
name: string,
asyncType: AsyncDependencyType | null,
optional: boolean,
contextParams?: RequireContextParams,
}>;
function registerDependency(
state: State,
qualifier: ImportQualifier,
path: NodePath<>,
): InternalDependency {
const dependency = state.dependencyRegistry.registerDependency(qualifier);
const loc = getNearestLocFromPath(path);
if (loc != null) {
dependency.locs.push(loc);
}
return dependency;
}
function isOptionalDependency(
name: string,
path: NodePath<>,
state: State,
): boolean {
const {allowOptionalDependencies} = state;
// The async require module is a 'built-in'. Resolving should never fail -> treat it as non-optional.
if (name === state.asyncRequireModulePathStringLiteral?.value) {
return false;
}
const isExcluded = () =>
Array.isArray(allowOptionalDependencies.exclude) &&
allowOptionalDependencies.exclude.includes(name);
if (!allowOptionalDependencies || isExcluded()) {
return false;
}
// Valid statement stack for single-level try-block: expressionStatement -> blockStatement -> tryStatement
let sCount = 0;
let p: ?(NodePath<> | NodePath<BabelNode>) = path;
while (p && sCount < 3) {
if (p.isStatement()) {
if (p.node.type === 'BlockStatement') {
// A single-level should have the tryStatement immediately followed BlockStatement
// with the key 'block' to distinguish from the finally block, which has key = 'finalizer'
return (
p.parentPath != null &&
p.parentPath.node.type === 'TryStatement' &&
p.key === 'block'
);
}
sCount += 1;
}
p = p.parentPath;
}
return false;
}
function getModuleNameFromCallArgs(path: NodePath<CallExpression>): ?string {
const args = path.get('arguments');
if (!Array.isArray(args) || args.length !== 1) {
throw new InvalidRequireCallError(path);
}
const result = args[0].evaluate();
if (result.confident && typeof result.value === 'string') {
return result.value;
}
return null;
}
collectDependencies.getModuleNameFromCallArgs = getModuleNameFromCallArgs;
class InvalidRequireCallError extends Error {
constructor({node}: NodePath<>, message?: string) {
const line = node.loc && node.loc.start && node.loc.start.line;
super(
[
`Invalid call at line ${line || '<unknown>'}: ${generate(node).code}`,
message,
]
.filter(Boolean)
.join('\n'),
);
}
}
collectDependencies.InvalidRequireCallError = InvalidRequireCallError;
/**
* Produces a Babel template that will throw at runtime when the require call
* is reached. This makes dynamic require errors catchable by libraries that
* want to use them.
*/
const dynamicRequireErrorTemplate = template.expression(`
(function(line) {
throw new Error(
'Dynamic require defined at line ' + line + '; not supported by Metro',
);
})(LINE)
`);
/**
* Produces a Babel template that transforms an "import(...)" call into a
* "require(...)" call to the asyncRequire specified.
*/
const makeAsyncRequireTemplate = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH)(MODULE_ID, DEPENDENCY_MAP.paths)
`);
const makeAsyncRequireTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH)(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);
const makeAsyncPrefetchTemplate = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, DEPENDENCY_MAP.paths)
`);
const makeAsyncPrefetchTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).prefetch(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);
const makeAsyncImportMaybeSyncTemplate = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).unstable_importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths)
`);
const makeAsyncImportMaybeSyncTemplateWithName = template.expression(`
require(ASYNC_REQUIRE_MODULE_PATH).unstable_importMaybeSync(MODULE_ID, DEPENDENCY_MAP.paths, MODULE_NAME)
`);
const makeResolveWeakTemplate = template.expression(`
MODULE_ID
`);
const DefaultDependencyTransformer: DependencyTransformer = {
transformSyncRequire(
path: NodePath<CallExpression>,
dependency: InternalDependency,
state: State,
): void {
const moduleIDExpression = createModuleIDExpression(dependency, state);
path.node.arguments = ([moduleIDExpression]: Array<
| BabelNodeExpression
| BabelNodeSpreadElement
| BabelNodeJSXNamespacedName
| BabelNodeArgumentPlaceholder,
>);
// Always add the debug name argument last
if (state.keepRequireNames) {
path.node.arguments.push(types.stringLiteral(dependency.name));
}
},
transformImportCall(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void {
const makeNode = state.keepRequireNames
? makeAsyncRequireTemplateWithName
: makeAsyncRequireTemplate;
const opts = {
ASYNC_REQUIRE_MODULE_PATH: nullthrows(
state.asyncRequireModulePathStringLiteral,
),
MODULE_ID: createModuleIDExpression(dependency, state),
DEPENDENCY_MAP: nullthrows(state.dependencyMapIdentifier),
...(state.keepRequireNames
? {MODULE_NAME: createModuleNameLiteral(dependency)}
: null),
};
path.replaceWith(makeNode(opts));
},
transformImportMaybeSyncCall(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void {
const makeNode = state.keepRequireNames
? makeAsyncImportMaybeSyncTemplateWithName
: makeAsyncImportMaybeSyncTemplate;
const opts = {
ASYNC_REQUIRE_MODULE_PATH: nullthrows(
state.asyncRequireModulePathStringLiteral,
),
MODULE_ID: createModuleIDExpression(dependency, state),
DEPENDENCY_MAP: nullthrows(state.dependencyMapIdentifier),
...(state.keepRequireNames
? {MODULE_NAME: createModuleNameLiteral(dependency)}
: null),
};
path.replaceWith(makeNode(opts));
},
transformPrefetch(
path: NodePath<>,
dependency: InternalDependency,
state: State,
): void {
const makeNode = state.keepRequireNames
? makeAsyncPrefetchTemplateWithName
: makeAsyncPrefetchTemplate;
const opts = {
ASYNC_REQUIRE_MODULE_PATH: nullthrows(
state.asyncRequireModulePathStringLiteral,
),
MODULE_ID: createModuleIDExpression(dependency, state),
DEPENDENCY_MAP: nullthrows(state.dependencyMapIdentifier),
...(state.keepRequireNames
? {MODULE_NAME: createModuleNameLiteral(dependency)}
: null),
};
path.replaceWith(makeNode(opts));
},
transformIllegalDynamicRequire(path: NodePath<>, state: State): void {
path.replaceWith(
dynamicRequireErrorTemplate({
LINE: types.numericLiteral(path.node.loc?.start.line ?? 0),
}),
);
},
};
function createModuleIDExpression(
dependency: InternalDependency,
state: State,
): BabelNodeExpression {
return types.memberExpression(
nullthrows(state.dependencyMapIdentifier),
types.numericLiteral(dependency.index),
true,
);
}
function createModuleNameLiteral(dependency: InternalDependency) {
return types.stringLiteral(dependency.name);
}
/**
* Given an import qualifier, return a key used to register the dependency.
* Generally this return the `ImportQualifier.name` property, but more
* attributes can be appended to distinguish various combinations that would
* otherwise conflict.
*
* For example, the following case would have collision issues if they all utilized the `name` property:
* ```
* require('./foo');
* require.context('./foo');
* require.context('./foo', true, /something/);
* require.context('./foo', false, /something/);
* require.context('./foo', false, /something/, 'lazy');
* ```
*
* This method should be utilized by `registerDependency`.
*/
function getKeyForDependency(qualifier: ImportQualifier): string {
let key = qualifier.name;
const {asyncType} = qualifier;
if (asyncType) {
key += ['', asyncType].join('\0');
}
const {contextParams} = qualifier;
// Add extra qualifiers when using `require.context` to prevent collisions.
if (contextParams) {
// NOTE(EvanBacon): Keep this synchronized with `RequireContextParams`, if any other properties are added
// then this key algorithm should be updated to account for those properties.
// Example: `./directory__true__/foobar/m__lazy`
key += [
'',
'context',
String(contextParams.recursive),
String(contextParams.filter.pattern),
String(contextParams.filter.flags),
contextParams.mode,
// Join together and append to the name:
].join('\0');
}
return key;
}
class DependencyRegistry {
_dependencies: Map<string, InternalDependency> = new Map();
registerDependency(qualifier: ImportQualifier): InternalDependency {
const key = getKeyForDependency(qualifier);
let dependency: ?InternalDependency = this._dependencies.get(key);
if (dependency == null) {
const newDependency: MutableInternalDependency = {
name: qualifier.name,
asyncType: qualifier.asyncType,
locs: [],
index: this._dependencies.size,
key: crypto.createHash('sha1').update(key).digest('base64'),
};
if (qualifier.optional) {
newDependency.isOptional = true;
}
if (qualifier.contextParams) {
newDependency.contextParams = qualifier.contextParams;
}
dependency = newDependency;
} else {
if (dependency.isOptional && !qualifier.optional) {
// A previously optionally required dependency was required non-optionally.
// Mark it non optional for the whole module
dependency = {
...dependency,
isOptional: false,
};
}
}
this._dependencies.set(key, dependency);
return dependency;
}
getDependencies(): Array<InternalDependency> {
return Array.from(this._dependencies.values());
}
}
module.exports = collectDependencies;

View File

@@ -0,0 +1,20 @@
"use strict";
const traverse = require("@babel/traverse").default;
const nullthrows = require("nullthrows");
function generateImportNames(ast) {
let importDefault;
let importAll;
traverse(ast, {
Program(path) {
importAll = path.scope.generateUid("$$_IMPORT_ALL");
importDefault = path.scope.generateUid("$$_IMPORT_DEFAULT");
path.stop();
},
});
return {
importAll: nullthrows(importAll),
importDefault: nullthrows(importDefault),
};
}
module.exports = generateImportNames;

View File

@@ -0,0 +1,42 @@
/**
* 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
* @flow strict-local
*/
'use strict';
const traverse = require('@babel/traverse').default;
const nullthrows = require('nullthrows');
/**
* Select unused names for "metroImportDefault" and "metroImportAll", by
* calling "generateUid".
*/
function generateImportNames(ast: BabelNode): {
importAll: string,
importDefault: string,
} {
let importDefault;
let importAll;
traverse(ast, {
Program(path) {
importAll = path.scope.generateUid('$$_IMPORT_ALL');
importDefault = path.scope.generateUid('$$_IMPORT_DEFAULT');
path.stop();
},
});
return {
importAll: nullthrows(importAll),
importDefault: nullthrows(importDefault),
};
}
module.exports = generateImportNames;

View File

@@ -0,0 +1,114 @@
/**
* 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
*/
import type {AssetData} from './Asset';
import type {RamBundleInfo} from './DeltaBundler/Serializers/getRamBundleInfo';
import type {GraphId} from './lib/getGraphId';
import type MultipartResponse from './Server/MultipartResponse';
import type {
BundleOptions,
GraphOptions,
SplitBundleOptions,
} from './shared/types';
import type {IncomingMessage, ServerResponse} from 'http';
import type {
CustomTransformOptions,
TransformProfile,
} from 'metro-babel-transformer';
import type {ConfigT, RootPerfLogger} from 'metro-config';
import type {CustomResolverOptions} from 'metro-resolver';
import IncrementalBundler, {RevisionId} from './IncrementalBundler';
export interface SegmentLoadData {
[index: number]: [number[], number | null];
}
export interface BundleMetadata {
hash: string;
otaBuildNumber: string | null;
mobileConfigs: string[];
segmentHashes: string[];
segmentLoadData: SegmentLoadData;
}
export interface ProcessStartContext extends SplitBundleOptions {
readonly buildNumber: number;
readonly bundleOptions: BundleOptions;
readonly graphId: GraphId;
readonly graphOptions: GraphOptions;
readonly mres: MultipartResponse | ServerResponse;
readonly req: IncomingMessage;
readonly revisionId?: RevisionId | null;
readonly bundlePerfLogger: RootPerfLogger;
}
export interface ProcessDeleteContext {
readonly graphId: GraphId;
readonly req: IncomingMessage;
readonly res: ServerResponse;
}
export interface ProcessEndContext<T> extends ProcessStartContext {
readonly result: T;
}
export type ServerOptions = Readonly<{
hasReducedPerformance?: boolean;
onBundleBuilt?: (bundlePath: string) => void;
watch?: boolean;
}>;
export interface DefaultGraphOptions {
customResolverOptions: CustomResolverOptions;
customTransformOptions: CustomTransformOptions;
dev: boolean;
hot: boolean;
minify: boolean;
runtimeBytecodeVersion?: number;
unstable_transformProfile: TransformProfile;
}
export interface DefaultBundleOptions extends DefaultGraphOptions {
excludeSource: false;
inlineSourceMap: false;
modulesOnly: false;
onProgress: null;
runModule: true;
shallow: false;
sourceMapUrl: null;
sourceUrl: null;
}
export default class Server {
static DEFAULT_GRAPH_OPTIONS: DefaultGraphOptions;
static DEFAULT_BUNDLE_OPTIONS: BundleOptions;
constructor(config: ConfigT, options?: ServerOptions);
end(): void;
getBundler(): IncrementalBundler;
getCreateModuleId(): (path: string) => number;
build(options: BundleOptions): Promise<{
code: string;
map: string;
}>;
getRamBundleInfo(options: BundleOptions): Promise<RamBundleInfo>;
getAssets(options: BundleOptions): Promise<ReadonlyArray<AssetData>>;
getOrderedDependencyPaths(options: {
readonly dev: boolean;
readonly entryFile: string;
readonly minify: boolean;
readonly platform: string;
}): Promise<string[]>;
processRequest(
IncomingMessage: IncomingMessage,
ServerResponse: ServerResponse,
next: (e: Error | null) => unknown,
): void;
}

1194
smart-app-city/frontend/node_modules/metro/src/Server.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
/**
* 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
*/
import type {IncomingMessage, ServerResponse} from 'http';
export type Data = string | Buffer | Uint8Array;
export interface Headers {
[name: string]: string | number;
}
export default class MultipartResponse {
static wrapIfSupported(
req: IncomingMessage,
res: ServerResponse,
): MultipartResponse | ServerResponse;
static serializeHeaders(headers: Headers): string;
res: ServerResponse;
headers: Headers;
constructor(res: ServerResponse);
writeChunk(headers: Headers | null, data?: Data, isLast?: boolean): void;
writeHead(status: number, headers?: Headers): void;
setHeader(name: string, value: string | number): void;
end(data?: Data): void;
}

View File

@@ -0,0 +1,64 @@
"use strict";
const accepts = require("accepts");
const CRLF = "\r\n";
const BOUNDARY = "3beqjf3apnqeu3h5jqorms4i";
class MultipartResponse {
static wrapIfSupported(req, res) {
if (accepts(req).types().includes("multipart/mixed")) {
return new MultipartResponse(res);
}
return res;
}
static serializeHeaders(headers) {
return Object.keys(headers)
.map((key) => `${key}: ${headers[key]}`)
.join(CRLF);
}
constructor(res) {
this.res = res;
this.headers = {};
res.writeHead(200, {
"Content-Type": `multipart/mixed; boundary="${BOUNDARY}"`,
});
res.write(
"If you are seeing this, your client does not support multipart response"
);
}
writeChunk(headers, data, isLast = false) {
if (this.res.finished) {
return;
}
this.res.write(`${CRLF}--${BOUNDARY}${CRLF}`);
if (headers) {
this.res.write(MultipartResponse.serializeHeaders(headers) + CRLF + CRLF);
}
if (data != null) {
this.res.write(data);
}
if (isLast) {
this.res.write(`${CRLF}--${BOUNDARY}--${CRLF}`);
}
}
writeHead(status, headers) {
this.setHeader("X-Http-Status", status);
if (!headers) {
return;
}
for (const key in headers) {
this.setHeader(key, headers[key]);
}
}
setHeader(name, value) {
this.headers[name] = value;
}
end(data) {
this.writeChunk(this.headers, data, true);
this.res.end();
}
once(name, fn) {
this.res.once(name, fn);
return this;
}
}
module.exports = MultipartResponse;

View File

@@ -0,0 +1,104 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
import type {IncomingMessage, ServerResponse} from 'http';
const accepts = require('accepts');
const CRLF = '\r\n';
const BOUNDARY = '3beqjf3apnqeu3h5jqorms4i';
type Data = string | Buffer | Uint8Array;
type Headers = {[string]: string | number};
class MultipartResponse {
static wrapIfSupported(
req: IncomingMessage,
res: ServerResponse,
): MultipartResponse | ServerResponse {
if (accepts(req).types().includes('multipart/mixed')) {
return new MultipartResponse(res);
}
return res;
}
static serializeHeaders(headers: Headers): string {
return Object.keys(headers)
.map(key => `${key}: ${headers[key]}`)
.join(CRLF);
}
res: ServerResponse;
headers: Headers;
constructor(res: ServerResponse) {
this.res = res;
this.headers = {};
res.writeHead(200, {
'Content-Type': `multipart/mixed; boundary="${BOUNDARY}"`,
});
res.write(
'If you are seeing this, your client does not support multipart response',
);
}
writeChunk(
headers: Headers | null,
data?: Data,
isLast?: boolean = false,
): void {
if (this.res.finished) {
return;
}
this.res.write(`${CRLF}--${BOUNDARY}${CRLF}`);
if (headers) {
this.res.write(MultipartResponse.serializeHeaders(headers) + CRLF + CRLF);
}
if (data != null) {
this.res.write(data);
}
if (isLast) {
this.res.write(`${CRLF}--${BOUNDARY}--${CRLF}`);
}
}
writeHead(status: number, headers?: Headers): void {
// We can't actually change the response HTTP status code
// because the headers have already been sent
this.setHeader('X-Http-Status', status);
if (!headers) {
return;
}
for (const key in headers) {
this.setHeader(key, headers[key]);
}
}
setHeader(name: string, value: string | number): void {
this.headers[name] = value;
}
end(data?: Data): void {
this.writeChunk(this.headers, data, true);
this.res.end();
}
once(name: string, fn: () => mixed): this {
this.res.once(name, fn);
return this;
}
}
module.exports = MultipartResponse;

View File

@@ -0,0 +1,137 @@
"use strict";
const { greatestLowerBound } = require("metro-source-map/src/Consumer/search");
const {
SourceMetadataMapConsumer,
} = require("metro-symbolicate/src/Symbolication");
function createFunctionNameGetter(module) {
const consumer = new SourceMetadataMapConsumer(
{
version: 3,
mappings: "",
sources: ["dummy"],
names: [],
x_facebook_sources: [[module.functionMap]],
},
(name) => name
);
return ({ line1Based, column0Based }) =>
consumer.functionNameFor({
line: line1Based,
column: column0Based,
source: "dummy",
});
}
async function symbolicate(stack, maps, config, extraData) {
const mapsByUrl = new Map();
for (const [url, map] of maps) {
mapsByUrl.set(url, map);
}
const functionNameGetters = new Map();
function findModule(frame) {
const map = mapsByUrl.get(frame.file);
if (!map || frame.lineNumber == null) {
return null;
}
const moduleIndex = greatestLowerBound(
map,
frame.lineNumber,
(target, candidate) => target - candidate.firstLine1Based
);
if (moduleIndex == null) {
return null;
}
return map[moduleIndex];
}
function findOriginalPos(frame, module) {
if (
module.map == null ||
frame.lineNumber == null ||
frame.column == null
) {
return null;
}
const generatedPosInModule = {
line1Based: frame.lineNumber - module.firstLine1Based + 1,
column0Based: frame.column,
};
const mappingIndex = greatestLowerBound(
module.map,
generatedPosInModule,
(target, candidate) => {
if (target.line1Based === candidate[0]) {
return target.column0Based - candidate[1];
}
return target.line1Based - candidate[0];
}
);
if (mappingIndex == null) {
return null;
}
const mapping = module.map[mappingIndex];
if (mapping[0] !== generatedPosInModule.line1Based || mapping.length < 4) {
return null;
}
return {
line1Based: mapping[2],
column0Based: mapping[3],
};
}
function findFunctionName(originalPos, module) {
if (module.functionMap) {
let getFunctionName = functionNameGetters.get(module);
if (!getFunctionName) {
getFunctionName = createFunctionNameGetter(module);
functionNameGetters.set(module, getFunctionName);
}
return getFunctionName(originalPos);
}
return null;
}
function symbolicateFrame(frame) {
const module = findModule(frame);
if (!module) {
return {
...frame,
};
}
if (!Array.isArray(module.map)) {
throw new Error(
`Unexpected module with serialized source map found: ${module.path}`
);
}
const originalPos = findOriginalPos(frame, module);
if (!originalPos) {
return {
...frame,
};
}
const methodName =
findFunctionName(originalPos, module) ?? frame.methodName;
return {
...frame,
methodName,
file: module.path,
lineNumber: originalPos.line1Based,
column: originalPos.column0Based,
};
}
async function customizeFrame(frame) {
const customizations =
(await config.symbolicator.customizeFrame(frame)) || {};
return {
...frame,
...customizations,
};
}
async function customizeStack(symbolicatedStack) {
return await config.symbolicator.customizeStack(
symbolicatedStack,
extraData
);
}
return Promise.all(stack.map(symbolicateFrame).map(customizeFrame)).then(
customizeStack
);
}
module.exports = symbolicate;

View File

@@ -0,0 +1,226 @@
/**
* 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 {
FBSourceFunctionMap,
MetroSourceMapSegmentTuple,
} from '../../../metro-source-map/src/source-map';
import type {ExplodedSourceMap} from '../DeltaBundler/Serializers/getExplodedSourceMap';
import type {ConfigT} from 'metro-config/src/configTypes.flow';
const {greatestLowerBound} = require('metro-source-map/src/Consumer/search');
const {
SourceMetadataMapConsumer,
} = require('metro-symbolicate/src/Symbolication');
export type StackFrameInput = {
+file: ?string,
+lineNumber: ?number,
+column: ?number,
+methodName: ?string,
...
};
export type IntermediateStackFrame = {
...StackFrameInput,
collapse?: boolean,
...
};
export type StackFrameOutput = $ReadOnly<{
...IntermediateStackFrame,
...
}>;
type ExplodedSourceMapModule = $ElementType<ExplodedSourceMap, number>;
type Position = {+line1Based: number, column0Based: number};
function createFunctionNameGetter(
module: ExplodedSourceMapModule,
): Position => ?string {
const consumer = new SourceMetadataMapConsumer(
{
version: 3,
mappings: '',
sources: ['dummy'],
names: [],
x_facebook_sources: [[module.functionMap]],
},
name => name /* no normalization needed */,
);
return ({line1Based, column0Based}) =>
consumer.functionNameFor({
line: line1Based,
column: column0Based,
source: 'dummy',
});
}
async function symbolicate(
stack: $ReadOnlyArray<StackFrameInput>,
maps: Iterable<[string, ExplodedSourceMap]>,
config: ConfigT,
extraData: mixed,
): Promise<$ReadOnlyArray<StackFrameOutput>> {
const mapsByUrl = new Map<?string, ExplodedSourceMap>();
for (const [url, map] of maps) {
mapsByUrl.set(url, map);
}
const functionNameGetters = new Map<
{
+firstLine1Based: number,
+functionMap: ?FBSourceFunctionMap,
+map: Array<MetroSourceMapSegmentTuple>,
+path: string,
},
(Position) => ?string,
>();
function findModule(frame: StackFrameInput): ?ExplodedSourceMapModule {
const map = mapsByUrl.get(frame.file);
if (!map || frame.lineNumber == null) {
return null;
}
const moduleIndex = greatestLowerBound(
map,
frame.lineNumber,
(target, candidate) => target - candidate.firstLine1Based,
);
if (moduleIndex == null) {
return null;
}
return map[moduleIndex];
}
function findOriginalPos(
frame: StackFrameInput,
module: ExplodedSourceMapModule,
): ?Position {
if (
module.map == null ||
frame.lineNumber == null ||
frame.column == null
) {
return null;
}
const generatedPosInModule = {
line1Based: frame.lineNumber - module.firstLine1Based + 1,
column0Based: frame.column,
};
const mappingIndex = greatestLowerBound(
module.map,
generatedPosInModule,
(target, candidate) => {
if (target.line1Based === candidate[0]) {
return target.column0Based - candidate[1];
}
return target.line1Based - candidate[0];
},
);
if (mappingIndex == null) {
return null;
}
const mapping = module.map[mappingIndex];
if (
mapping[0] !== generatedPosInModule.line1Based ||
mapping.length < 4 /* no source line/column info */
) {
return null;
}
return {
// $FlowFixMe: Length checks do not refine tuple unions.
line1Based: mapping[2],
// $FlowFixMe: Length checks do not refine tuple unions.
column0Based: mapping[3],
};
}
function findFunctionName(
originalPos: Position,
module: {
+firstLine1Based: number,
+functionMap: ?FBSourceFunctionMap,
+map: Array<MetroSourceMapSegmentTuple>,
+path: string,
},
): ?string {
if (module.functionMap) {
let getFunctionName = functionNameGetters.get(module);
if (!getFunctionName) {
getFunctionName = createFunctionNameGetter(module);
functionNameGetters.set(module, getFunctionName);
}
return getFunctionName(originalPos);
}
return null;
}
function symbolicateFrame(frame: StackFrameInput): IntermediateStackFrame {
const module = findModule(frame);
if (!module) {
return {...frame};
}
if (!Array.isArray(module.map)) {
throw new Error(
`Unexpected module with serialized source map found: ${module.path}`,
);
}
const originalPos = findOriginalPos(frame, module);
if (!originalPos) {
return {...frame};
}
const methodName =
findFunctionName(originalPos, module) ?? frame.methodName;
return {
...frame,
methodName,
file: module.path,
lineNumber: originalPos.line1Based,
column: originalPos.column0Based,
};
}
/**
* `customizeFrame` allows for custom modifications of the symbolicated frame in a stack.
* It can be used to collapse stack frames that are not relevant to users, pointing them
* to more relevant product code instead.
*
* An example usecase is a library throwing an error while sanitizing inputs from product code.
* In some cases, it's more useful to point the developer looking at the error towards the product code directly.
*/
async function customizeFrame(
frame: IntermediateStackFrame,
): Promise<IntermediateStackFrame> {
const customizations =
(await config.symbolicator.customizeFrame(frame)) || {};
return {...frame, ...customizations};
}
/**
* `customizeStack` allows for custom modifications of a symbolicated stack.
* Where `customizeFrame` operates on individual frames, this hook can process the entire stack in context.
*
* Note: `customizeStack` has access to an `extraData` object which can be used to attach metadata
* to the error coming in, to be used by the customizeStack hook.
*/
async function customizeStack(
symbolicatedStack: Array<IntermediateStackFrame>,
): Promise<Array<IntermediateStackFrame>> {
return await config.symbolicator.customizeStack(
symbolicatedStack,
extraData,
);
}
return Promise.all(stack.map(symbolicateFrame).map(customizeFrame)).then(
customizeStack,
);
}
module.exports = symbolicate;

View File

@@ -0,0 +1,15 @@
"use strict";
const fs = require("fs");
exports.watchFile = async function (filename, callback) {
fs.watchFile(filename, () => {
callback();
});
await callback();
};
exports.makeAsyncCommand = (command) => (argv) => {
Promise.resolve(command(argv)).catch((error) => {
console.error(error.stack);
process.exitCode = 1;
});
};

View File

@@ -0,0 +1,34 @@
/**
* 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
* @format
* @oncall react_native
*/
'use strict';
const fs = require('fs');
exports.watchFile = async function (
filename: string,
callback: () => any,
): Promise<void> {
fs.watchFile(filename, () => {
callback();
});
await callback();
};
exports.makeAsyncCommand =
<T>(command: (argv: T) => Promise<void>): ((argv: T) => void) =>
(argv: T) => {
Promise.resolve(command(argv)).catch(error => {
console.error(error.stack);
process.exitCode = 1;
});
};

9
smart-app-city/frontend/node_modules/metro/src/cli.js generated vendored Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env node
"use strict";
try {
require("metro-babel-register").unstable_registerForMetroMonorepo();
} catch {}
const { attachMetroCli } = require("./index");
const yargs = require("yargs");
attachMetroCli(yargs.demandCommand(1)).argv;

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
/**
* 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';
try {
// $FlowFixMe[untyped-import]
require('metro-babel-register').unstable_registerForMetroMonorepo();
} catch {}
const {attachMetroCli} = require('./index');
const yargs = require('yargs');
// $FlowFixMe[unused-promise]
attachMetroCli(yargs.demandCommand(1)).argv;

View File

@@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true,
});
exports.default = coerceKeyValueArray;
function coerceKeyValueArray(keyValueArray) {
const result = Object.create(null);
for (const item of keyValueArray) {
if (item.indexOf("=") === -1) {
throw new Error('Expected parameter to include "=" but found: ' + item);
}
if (item.indexOf("&") !== -1) {
throw new Error('Parameter cannot include "&" but found: ' + item);
}
const params = new URLSearchParams(item);
params.forEach((value, key) => {
result[key] = value;
});
}
return result;
}

View File

@@ -0,0 +1,33 @@
/**
* 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
*/
export default function coerceKeyValueArray(
keyValueArray: $ReadOnlyArray<string>,
): {
[key: string]: string,
__proto__: null,
} {
const result: {[key: string]: string, __proto__: null} = Object.create(null);
for (const item of keyValueArray) {
if (item.indexOf('=') === -1) {
throw new Error('Expected parameter to include "=" but found: ' + item);
}
if (item.indexOf('&') !== -1) {
throw new Error('Parameter cannot include "&" but found: ' + item);
}
const params = new URLSearchParams(item);
params.forEach((value, key) => {
// $FlowExpectedError[prop-missing]
result[key] = value;
});
}
return result;
}

View File

@@ -0,0 +1,128 @@
"use strict";
var _parseKeyValueParamArray = _interopRequireDefault(
require("../cli/parseKeyValueParamArray")
);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const { makeAsyncCommand } = require("../cli-utils");
const TerminalReporter = require("../lib/TerminalReporter");
const { loadConfig } = require("metro-config");
const { Terminal } = require("metro-core");
const term = new Terminal(process.stdout);
const updateReporter = new TerminalReporter(term);
module.exports = () => ({
command: "build <entry>",
desc: "Generates a JavaScript bundle containing the specified entrypoint and its descendants",
builder: (yargs) => {
yargs.option("project-roots", {
alias: "P",
type: "string",
array: true,
});
yargs.option("out", {
alias: "O",
type: "string",
demandOption: true,
});
yargs.option("platform", {
alias: "p",
type: "string",
});
yargs.option("output-type", {
alias: "t",
type: "string",
});
yargs.option("max-workers", {
alias: "j",
type: "number",
});
yargs.option("minify", {
alias: "z",
type: "boolean",
});
yargs.option("dev", {
alias: "g",
type: "boolean",
});
yargs.option("source-map", {
type: "boolean",
});
yargs.option("source-map-url", {
type: "string",
});
yargs.option("legacy-bundler", {
type: "boolean",
});
yargs.option("config", {
alias: "c",
type: "string",
});
yargs.option("transform-option", {
type: "string",
array: true,
alias: "transformer-option",
coerce: _parseKeyValueParamArray.default,
describe:
"Custom transform options of the form key=value. URL-encoded. May be specified multiple times.",
});
yargs.option("resolver-option", {
type: "string",
array: true,
coerce: _parseKeyValueParamArray.default,
describe:
"Custom resolver options of the form key=value. URL-encoded. May be specified multiple times.",
});
yargs.option("reset-cache", {
type: "boolean",
});
},
handler: makeAsyncCommand(async (argv) => {
const config = await loadConfig(argv);
const options = {
entry: argv.entry,
dev: argv.dev,
out: argv.out,
minify: argv.minify,
platform: argv.platform,
sourceMap: argv.sourceMap,
sourceMapUrl: argv.sourceMapUrl,
customResolverOptions: argv.resolverOption,
customTransformOptions: argv.transformOption,
};
const MetroApi = require("../index");
await MetroApi.runBuild(config, {
...options,
onBegin: () => {
updateReporter.update({
buildID: "$",
type: "bundle_build_started",
bundleDetails: {
bundleType: "Bundle",
customResolverOptions: options.customResolverOptions ?? {},
customTransformOptions: options.customTransformOptions ?? {},
dev: !!options.dev,
entryFile: options.entry,
minify: !!options.minify,
platform: options.platform,
},
});
},
onProgress: (transformedFileCount, totalFileCount) => {
updateReporter.update({
buildID: "$",
type: "bundle_transform_progressed",
transformedFileCount,
totalFileCount,
});
},
onComplete: () => {
updateReporter.update({
buildID: "$",
type: "bundle_build_done",
});
},
});
}),
});

View File

@@ -0,0 +1,150 @@
/**
* 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
* @format
* @oncall react_native
*/
import type {RunBuildOptions} from '../index';
import type {CustomTransformOptions} from 'metro-babel-transformer';
import type {CustomResolverOptions} from 'metro-resolver';
import type {ModuleObject} from 'yargs';
import typeof Yargs from 'yargs';
import parseKeyValueParamArray from '../cli/parseKeyValueParamArray';
const {makeAsyncCommand} = require('../cli-utils');
const TerminalReporter = require('../lib/TerminalReporter');
const {loadConfig} = require('metro-config');
const {Terminal} = require('metro-core');
const term = new Terminal(process.stdout);
const updateReporter = new TerminalReporter(term);
type Args = $ReadOnly<{
config?: string,
dev?: boolean,
entry: string,
legacyBundler?: boolean,
maxWorkers?: number,
minify?: boolean,
out: string,
outputType?: string,
platform?: string,
projectRoots?: $ReadOnlyArray<string>,
resetCache?: boolean,
sourceMap?: boolean,
sourceMapUrl?: string,
transformOption: CustomTransformOptions,
resolverOption: CustomResolverOptions,
}>;
module.exports = (): {
...ModuleObject,
handler: Function,
} => ({
command: 'build <entry>',
desc: 'Generates a JavaScript bundle containing the specified entrypoint and its descendants',
builder: (yargs: Yargs): void => {
yargs.option('project-roots', {
alias: 'P',
type: 'string',
array: true,
});
yargs.option('out', {alias: 'O', type: 'string', demandOption: true});
yargs.option('platform', {alias: 'p', type: 'string'});
yargs.option('output-type', {alias: 't', type: 'string'});
yargs.option('max-workers', {alias: 'j', type: 'number'});
yargs.option('minify', {alias: 'z', type: 'boolean'});
yargs.option('dev', {alias: 'g', type: 'boolean'});
yargs.option('source-map', {type: 'boolean'});
yargs.option('source-map-url', {type: 'string'});
yargs.option('legacy-bundler', {type: 'boolean'});
yargs.option('config', {alias: 'c', type: 'string'});
yargs.option('transform-option', {
type: 'string',
array: true,
alias: 'transformer-option',
coerce: (parseKeyValueParamArray: $FlowFixMe),
describe:
'Custom transform options of the form key=value. URL-encoded. May be specified multiple times.',
});
yargs.option('resolver-option', {
type: 'string',
array: true,
coerce: (parseKeyValueParamArray: $FlowFixMe),
describe:
'Custom resolver options of the form key=value. URL-encoded. May be specified multiple times.',
});
// Deprecated
yargs.option('reset-cache', {type: 'boolean'});
},
handler: makeAsyncCommand(async (argv: Args) => {
const config = await loadConfig(argv);
const options: RunBuildOptions = {
entry: argv.entry,
dev: argv.dev,
out: argv.out,
minify: argv.minify,
platform: argv.platform,
sourceMap: argv.sourceMap,
sourceMapUrl: argv.sourceMapUrl,
customResolverOptions: argv.resolverOption,
customTransformOptions: argv.transformOption,
};
// Inline require() to avoid circular dependency with ../index
const MetroApi = require('../index');
await MetroApi.runBuild(config, {
...options,
onBegin: (): void => {
updateReporter.update({
buildID: '$',
type: 'bundle_build_started',
bundleDetails: {
bundleType: 'Bundle',
customResolverOptions: options.customResolverOptions ?? {},
customTransformOptions: options.customTransformOptions ?? {},
dev: !!options.dev,
entryFile: options.entry,
minify: !!options.minify,
platform: options.platform,
},
});
},
onProgress: (
transformedFileCount: number,
totalFileCount: number,
): void => {
updateReporter.update({
buildID: '$',
type: 'bundle_transform_progressed',
transformedFileCount,
totalFileCount,
});
},
onComplete: (): void => {
updateReporter.update({
buildID: '$',
type: 'bundle_build_done',
});
},
});
}),
});

View File

@@ -0,0 +1,89 @@
"use strict";
const { makeAsyncCommand } = require("../cli-utils");
const Server = require("../Server");
const fs = require("fs");
const { loadConfig } = require("metro-config");
const path = require("path");
const { promisify } = require("util");
async function dependencies(args, config) {
const rootModuleAbsolutePath = args.entryFile;
if (!fs.existsSync(rootModuleAbsolutePath)) {
return Promise.reject(
new Error(`File ${rootModuleAbsolutePath} does not exist`)
);
}
config.cacheStores = [];
const relativePath = path.relative(
config.server.unstable_serverRoot ?? config.projectRoot,
rootModuleAbsolutePath
);
const options = {
platform: args.platform,
entryFile: relativePath,
dev: args.dev,
minify: false,
generateSourceMaps: !args.dev,
};
const outStream =
args.output != null ? fs.createWriteStream(args.output) : process.stdout;
const server = new Server(config);
const deps = await server.getOrderedDependencyPaths(options);
deps.forEach((modulePath) => {
const isInsideProjectRoots =
config.watchFolders.filter((root) => modulePath.startsWith(root)).length >
0;
if (isInsideProjectRoots) {
outStream.write(modulePath + "\n");
}
});
server.end();
return args.output != null
? promisify(outStream.end).bind(outStream)()
: Promise.resolve();
}
module.exports = () => ({
command: "get-dependencies [entryFile]",
desc: "List all dependencies that will be bundled for a given entry point",
builder: (yargs) => {
yargs.option("entry-file", {
type: "string",
demandOption: true,
describe: "Absolute path to the root JS file",
});
yargs.option("output", {
type: "string",
describe:
"File name where to store the output, ex. /tmp/dependencies.txt",
});
yargs.option("platform", {
type: "string",
describe: "The platform extension used for selecting modules",
});
yargs.option("transformer", {
type: "string",
describe: "Specify a custom transformer to be used",
});
yargs.option("max-workers", {
type: "number",
describe:
"Specifies the maximum number of workers the worker-pool " +
"will spawn for transforming files. This defaults to the number of the " +
"cores available on your machine.",
});
yargs.option("dev", {
type: "boolean",
default: true,
describe: "If false, skip all dev-only code path",
});
yargs.option("verbose", {
type: "boolean",
default: false,
description: "Enables logging",
});
},
handler: makeAsyncCommand(async (argv) => {
const config = await loadConfig(argv);
await dependencies(argv, config);
}),
});

Some files were not shown because too many files have changed in this diff Show More