Files
Fluxup_PAP/node_modules/@expo/metro-config/build/serializer/serializeChunks.js
2026-03-10 16:18:05 +00:00

569 lines
25 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Chunk = void 0;
exports.graphToSerialAssetsAsync = graphToSerialAssetsAsync;
exports.getSortedModules = getSortedModules;
const bundleToString_1 = __importDefault(require("@expo/metro/metro/lib/bundleToString"));
const isResolvedDependency_1 = require("@expo/metro/metro/lib/isResolvedDependency");
const assert_1 = __importDefault(require("assert"));
const path_1 = __importDefault(require("path"));
const debugId_1 = require("./debugId");
const exportPath_1 = require("./exportPath");
const getCssDeps_1 = require("./getCssDeps");
const getAssets_1 = __importDefault(require("../transform-worker/getAssets"));
const filePath_1 = require("../utils/filePath");
// Lazy-loaded to avoid pulling in metro-source-map at startup
let _buildHermesBundleAsync;
function getBuildHermesBundleAsync() {
if (!_buildHermesBundleAsync) {
_buildHermesBundleAsync = require('./exportHermes').buildHermesBundleAsync;
}
return _buildHermesBundleAsync;
}
// Lazy-loaded to avoid pulling in metro's getAppendScripts -> sourceMapString -> @babel/traverse at startup
let _sourceMapString;
function getSourceMapString() {
if (!_sourceMapString) {
_sourceMapString =
require('@expo/metro/metro/DeltaBundler/Serializers/sourceMapString').sourceMapString;
}
return _sourceMapString;
}
let _baseJSBundleWithDependencies;
function getBaseJSBundleWithDependencies() {
if (!_baseJSBundleWithDependencies) {
_baseJSBundleWithDependencies = require('./fork/baseJSBundle').baseJSBundleWithDependencies;
}
return _baseJSBundleWithDependencies;
}
let _getBaseUrlOption;
function getBaseUrlOption(...args) {
if (!_getBaseUrlOption) {
_getBaseUrlOption = require('./fork/baseJSBundle').getBaseUrlOption;
}
return _getBaseUrlOption(...args);
}
let _getPlatformOption;
function getPlatformOption(...args) {
if (!_getPlatformOption) {
_getPlatformOption = require('./fork/baseJSBundle').getPlatformOption;
}
return _getPlatformOption(...args);
}
async function graphToSerialAssetsAsync(config, serializeChunkOptions, ...props) {
const [entryFile, preModules, graph, options] = props;
const cssDeps = (0, getCssDeps_1.getCssSerialAssets)(graph.dependencies, {
entryFile,
projectRoot: options.projectRoot,
});
// Create chunks for splitting.
const chunks = new Set();
gatherChunks(preModules, chunks, { test: pathToRegex(entryFile) }, preModules, graph, options, false, true);
const entryChunk = findEntryChunk(chunks, entryFile);
if (entryChunk) {
removeEntryDepsFromAsyncChunks(entryChunk, chunks);
const commonChunk = extractCommonChunk(chunks, graph, options);
if (commonChunk) {
entryChunk.requiredChunks.add(commonChunk);
chunks.add(commonChunk);
}
deduplicateAgainstKnownChunks(chunks, entryChunk, commonChunk);
removeEmptyChunks(chunks);
if (commonChunk) {
createRuntimeChunk(entryChunk, chunks, graph, options);
}
}
const jsAssets = await serializeChunksAsync(chunks, config.serializer ?? {}, serializeChunkOptions);
// TODO: Can this be anything besides true?
const isExporting = true;
const baseUrl = getBaseUrlOption(graph, { serializerOptions: serializeChunkOptions });
const assetPublicUrl = (baseUrl.replace(/\/+$/, '') ?? '') + '/assets';
const platform = getPlatformOption(graph, options) ?? 'web';
const isHosted = platform === 'web' || (graph.transformOptions?.customTransformOptions?.hosted && isExporting);
const publicPath = isExporting
? isHosted
? `/assets?export_path=${assetPublicUrl}`
: assetPublicUrl
: '/assets/?unstable_path=.';
// TODO: Convert to serial assets
// TODO: Disable this call dynamically in development since assets are fetched differently.
const metroAssets = (await (0, getAssets_1.default)(graph.dependencies, {
processModuleFilter: options.processModuleFilter,
assetPlugins: config.transformer?.assetPlugins ?? [],
platform,
projectRoot: options.projectRoot, // this._getServerRootDir(),
publicPath,
isHosted,
}));
return {
artifacts: [...jsAssets, ...cssDeps],
assets: metroAssets,
};
}
class Chunk {
name;
entries;
graph;
options;
isAsync;
isVendor;
isEntry;
deps = new Set();
preModules = new Set();
// Chunks that are required to be loaded synchronously before this chunk.
// These are included in the HTML as <script> tags.
requiredChunks = new Set();
constructor(name, entries, graph, options, isAsync = false, isVendor = false, isEntry = false) {
this.name = name;
this.entries = entries;
this.graph = graph;
this.options = options;
this.isAsync = isAsync;
this.isVendor = isVendor;
this.isEntry = isEntry;
this.deps = new Set(entries);
}
getPlatform() {
(0, assert_1.default)(this.graph.transformOptions.platform, "platform is required to be in graph's transformOptions");
return this.graph.transformOptions.platform;
}
getFilename(src) {
return !this.options.serializerOptions?.exporting
? this.name
: (0, exportPath_1.getExportPathForDependencyWithOptions)(this.name, {
platform: this.getPlatform(),
src,
serverRoot: this.options.serverRoot,
});
}
getStableChunkSource(serializerConfig) {
return this.options.dev
? ''
: this.serializeToCodeWithTemplates(serializerConfig, {
// Disable source maps when creating a sha to reduce the number of possible changes that could
// influence the cache hit.
serializerOptions: {
includeSourceMaps: false,
},
sourceMapUrl: undefined,
debugId: undefined,
}).code;
}
getFilenameForConfig(serializerConfig) {
return this.getFilename(this.getStableChunkSource(serializerConfig));
}
serializeToCodeWithTemplates(serializerConfig, options = {}) {
const entryFile = this.name;
// TODO: Disable all debugId steps when a dev server is enabled. This is an export-only feature.
const preModules = [...(options.preModules ?? this.preModules).values()];
const dependencies = [...this.deps];
const jsSplitBundle = getBaseJSBundleWithDependencies()(entryFile, preModules, dependencies, {
...this.options,
runBeforeMainModule: serializerConfig?.getModulesRunBeforeMainModule?.(path_1.default.relative(this.options.projectRoot, entryFile)) ?? [],
runModule: this.options.runModule && !this.isVendor && (this.isEntry || !this.isAsync),
modulesOnly: this.options.modulesOnly || preModules.length === 0,
platform: this.getPlatform(),
baseUrl: getBaseUrlOption(this.graph, this.options),
splitChunks: !!this.options.serializerOptions?.splitChunks,
skipWrapping: true,
computedAsyncModulePaths: null,
...options,
});
return { code: (0, bundleToString_1.default)(jsSplitBundle).code, paths: jsSplitBundle.paths };
}
hasAbsolutePath(absolutePath) {
return [...this.deps].some((module) => module.path === absolutePath);
}
getComputedPathsForAsyncDependencies(serializerConfig, chunks) {
const baseUrl = getBaseUrlOption(this.graph, this.options);
// Only calculate production paths when all chunks are being exported.
if (this.options.includeAsyncPaths) {
return null;
}
const computedAsyncModulePaths = {};
this.deps.forEach((module) => {
module.dependencies.forEach((dependency) => {
if ((0, isResolvedDependency_1.isResolvedDependency)(dependency) && dependency.data.data.asyncType) {
const chunkContainingModule = chunks.find((chunk) => chunk.hasAbsolutePath(dependency.absolutePath));
(0, assert_1.default)(chunkContainingModule, 'Chunk containing module not found: ' + dependency.absolutePath);
// NOTE(kitten): We shouldn't have any async imports on non-async chunks
// However, due to how chunks merge, some async imports may now be pointing
// at entrypoint (or vendor) chunks. We omit the path so that the async import
// helper doesn't reload and reevaluate the entrypoint.
if (chunkContainingModule.isAsync) {
const moduleIdName = chunkContainingModule.getFilenameForConfig(serializerConfig);
computedAsyncModulePaths[dependency.absolutePath] = (baseUrl ?? '/') + moduleIdName;
}
}
});
});
return computedAsyncModulePaths;
}
getAdjustedSourceMapUrl(serializerConfig) {
// Metro really only accounts for development, so we'll use the defaults here.
if (this.options.dev) {
return this.options.sourceMapUrl ?? null;
}
if (this.options.serializerOptions?.includeSourceMaps !== true) {
return null;
}
if (this.options.inlineSourceMap || !this.options.sourceMapUrl) {
return this.options.sourceMapUrl ?? null;
}
const platform = this.getPlatform();
const isAbsolute = platform !== 'web';
const baseUrl = getBaseUrlOption(this.graph, this.options);
const filename = this.getFilenameForConfig(serializerConfig);
const isAbsoluteBaseUrl = !!baseUrl?.match(/https?:\/\//);
const pathname = (isAbsoluteBaseUrl ? '' : baseUrl.replace(/\/+$/, '')) +
'/' +
filename.replace(/^\/+$/, '') +
'.map';
let adjustedSourceMapUrl = this.options.sourceMapUrl;
// Metro has lots of issues...
if (this.options.sourceMapUrl.startsWith('//localhost')) {
adjustedSourceMapUrl = 'http:' + this.options.sourceMapUrl;
}
try {
const parsed = new URL(pathname, isAbsoluteBaseUrl ? baseUrl : adjustedSourceMapUrl);
if (isAbsoluteBaseUrl || isAbsolute) {
return parsed.href;
}
return parsed.pathname;
}
catch (error) {
// NOTE: export:embed that don't use baseUrl will use file paths instead of URLs.
if (!this.options.dev && isAbsolute) {
return adjustedSourceMapUrl;
}
console.error(`Failed to link source maps because the source map URL "${this.options.sourceMapUrl}" is corrupt:`, error);
return null;
}
}
serializeToCode(serializerConfig, { debugId, chunks, preModules }) {
return this.serializeToCodeWithTemplates(serializerConfig, {
skipWrapping: false,
sourceMapUrl: this.getAdjustedSourceMapUrl(serializerConfig) ?? undefined,
computedAsyncModulePaths: this.getComputedPathsForAsyncDependencies(serializerConfig, chunks),
debugId,
preModules,
});
}
boolishTransformOption(name) {
const value = this.graph.transformOptions?.customTransformOptions?.[name];
return value === true || value === 'true' || value === '1';
}
async serializeToAssetsAsync(serializerConfig, chunks, { includeSourceMaps, unstable_beforeAssetSerializationPlugins }) {
// Create hash without wrapping to prevent it changing when the wrapping changes.
const outputFile = this.getFilenameForConfig(serializerConfig);
// We already use a stable hash for the output filename, so we'll reuse that for the debugId.
const debugId = (0, debugId_1.stringToUUID)(path_1.default.basename(outputFile, path_1.default.extname(outputFile)));
let finalPreModules = [...this.preModules];
if (unstable_beforeAssetSerializationPlugins) {
for (const plugin of unstable_beforeAssetSerializationPlugins) {
finalPreModules = plugin({
graph: this.graph,
premodules: finalPreModules,
debugId,
});
}
}
const jsCode = this.serializeToCode(serializerConfig, {
chunks,
debugId,
preModules: new Set(finalPreModules),
});
const relativeEntry = path_1.default.relative(this.options.projectRoot, this.name);
const jsAsset = {
filename: outputFile,
originFilename: relativeEntry,
type: 'js',
metadata: {
isAsync: this.isAsync,
requires: [...this.requiredChunks.values()].map((chunk) => chunk.getFilenameForConfig(serializerConfig)),
// Provide a list of module paths that can be used for matching chunks to routes.
// TODO: Move HTML serializing closer to this code so we can reduce passing this much data around.
modulePaths: [...this.deps].map((module) => module.path),
paths: jsCode.paths,
expoDomComponentReferences: collectOutputReferences(this.deps, 'expoDomComponentReference'),
reactClientReferences: collectOutputReferences(this.deps, 'reactClientReference'),
reactServerReferences: collectOutputReferences(this.deps, 'reactServerReference'),
loaderReferences: collectOutputReferences(this.deps, 'loaderReference'),
},
source: jsCode.code,
};
const assets = [jsAsset];
const mutateSourceMapWithDebugId = (sourceMap) => {
// TODO: Upstream this so we don't have to parse the source map back and forth.
if (!debugId) {
return sourceMap;
}
// NOTE: debugId isn't required for inline source maps because the source map is included in the same file, therefore
// we don't need to disambiguate between multiple source maps.
const sourceMapObject = JSON.parse(sourceMap);
sourceMapObject.debugId = debugId;
// NOTE: Sentry does this, but bun does not.
// sourceMapObject.debug_id = debugId;
return JSON.stringify(sourceMapObject);
};
if (
// Only include the source map if the `options.sourceMapUrl` option is provided and we are exporting a static build.
includeSourceMaps &&
!this.options.inlineSourceMap &&
this.options.sourceMapUrl) {
const modules = [
...finalPreModules,
...getSortedModules([...this.deps], {
createModuleId: this.options.createModuleId,
}),
].map((module) => {
// TODO: Make this user-configurable.
// Make all paths relative to the server root to prevent the entire user filesystem from being exposed.
if (path_1.default.isAbsolute(module.path)) {
return {
...module,
path: '/' +
(0, filePath_1.toPosixPath)(path_1.default.relative(this.options.serverRoot ?? this.options.projectRoot, module.path)),
};
}
return module;
});
// TODO: We may not need to mutate the original source map with a `debugId` when hermes is enabled since we'll have different source maps.
const sourceMap = mutateSourceMapWithDebugId(getSourceMapString()(modules, {
excludeSource: false,
...this.options,
}));
assets.push({
filename: this.options.dev ? jsAsset.filename + '.map' : outputFile + '.map',
originFilename: jsAsset.originFilename,
type: 'map',
metadata: {},
source: sourceMap,
});
}
if (this.boolishTransformOption('bytecode') && this.isHermesEnabled()) {
const adjustedSource = jsAsset.source.replace(/^\/\/# (sourceMappingURL)=(.*)$/gm, (...props) => {
if (props[1] === 'sourceMappingURL') {
const mapName = props[2].replace(/\.js\.map$/, '.hbc.map');
return `//# ${props[1]}=` + mapName;
}
return '';
});
// TODO: Generate hbc for each chunk
const hermesBundleOutput = await getBuildHermesBundleAsync()({
projectRoot: this.options.projectRoot,
filename: this.name,
code: adjustedSource,
map: assets[1] ? assets[1].source : null,
// TODO: Maybe allow prod + no minify.
minify: true, //!this.options.dev,
});
if (hermesBundleOutput.hbc) {
// TODO: Unclear if we should add multiple assets, link the assets, or mutate the first asset.
// jsAsset.metadata.hbc = hermesBundleOutput.hbc;
// @ts-expect-error: TODO
jsAsset.source = hermesBundleOutput.hbc;
jsAsset.filename = jsAsset.filename.replace(/\.js$/, '.hbc');
// Replace mappings with hbc
if (jsAsset.metadata.paths) {
jsAsset.metadata.paths = Object.fromEntries(Object.entries(jsAsset.metadata.paths).map(([key, value]) => [
key,
Object.fromEntries(Object.entries(value).map(([key, value]) => [
key,
value ? value.replace(/\.js$/, '.hbc') : value,
])),
]));
}
}
if (assets[1] && hermesBundleOutput.sourcemap) {
assets[1].source = mutateSourceMapWithDebugId(hermesBundleOutput.sourcemap);
assets[1].filename = assets[1].filename.replace(/\.js\.map$/, '.hbc.map');
}
}
return assets;
}
supportsBytecode() {
return this.getPlatform() !== 'web';
}
isHermesEnabled() {
// TODO: Revisit.
// TODO: There could be an issue with having the serializer for export:embed output hermes since the native scripts will
// also create hermes bytecode. We may need to disable in one of the two places.
return (!this.options.dev &&
this.supportsBytecode() &&
this.graph.transformOptions.customTransformOptions?.engine === 'hermes');
}
}
exports.Chunk = Chunk;
function getSortedModules(modules, { createModuleId, }) {
// Assign IDs to modules in a consistent order
for (const module of modules) {
createModuleId(module.path);
}
// Sort by IDs
return modules.sort((a, b) => createModuleId(a.path) - createModuleId(b.path));
}
// Convert file paths to regex matchers.
function pathToRegex(path) {
// Escape regex special characters, except for '*'
let regexSafePath = path.replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&');
// Replace '*' with '.*' to act as a wildcard in regex
regexSafePath = regexSafePath.replace(/\*/g, '.*');
// Create a RegExp object with the modified string
return new RegExp('^' + regexSafePath + '$');
}
function collectOutputReferences(modules, key) {
return [
...new Set([...modules]
.map((module) => {
return module.output.map((output) => {
if (key in output.data && typeof output.data[key] === 'string') {
return output.data[key];
}
return undefined;
});
})
.flat()),
].filter((value) => typeof value === 'string');
}
function getEntryModulesForChunkSettings(graph, settings) {
return [...graph.dependencies.entries()]
.filter(([path]) => settings.test.test(path))
.map(([, module]) => module);
}
function chunkIdForModules(modules) {
return modules
.map((module) => module.path)
.sort()
.join('=>');
}
function gatherChunks(runtimePremodules, chunks, settings, preModules, graph, options, isAsync = false, isEntry = false) {
let entryModules = getEntryModulesForChunkSettings(graph, settings);
const existingChunks = [...chunks.values()];
entryModules = entryModules.filter((module) => {
return !existingChunks.find((chunk) => chunk.entries.includes(module));
});
// Prevent processing the same entry file twice.
if (!entryModules.length) {
return chunks;
}
const entryChunk = new Chunk(chunkIdForModules(entryModules), entryModules, graph, options, isAsync, false, isEntry);
// Add all the pre-modules to the first chunk.
if (preModules.length) {
// On native, use the preModules in insert code in the entry chunk.
for (const module of preModules.values()) {
entryChunk.preModules.add(module);
}
}
chunks.add(entryChunk);
function includeModule(entryModule) {
for (const dependency of entryModule.dependencies.values()) {
if (!(0, isResolvedDependency_1.isResolvedDependency)(dependency)) {
continue;
}
else if (dependency.data.data.asyncType &&
// Support disabling multiple chunks.
entryChunk.options.serializerOptions?.splitChunks !== false) {
const isEntry = dependency.data.data.asyncType === 'worker';
gatherChunks(runtimePremodules, chunks, { test: pathToRegex(dependency.absolutePath) }, isEntry ? runtimePremodules : [], graph, options, true, isEntry);
}
else {
const module = graph.dependencies.get(dependency.absolutePath);
if (module) {
// Prevent circular dependencies from creating infinite loops.
if (!entryChunk.deps.has(module)) {
entryChunk.deps.add(module);
includeModule(module);
}
}
}
}
}
for (const entryModule of entryModules) {
includeModule(entryModule);
}
return chunks;
}
function findEntryChunk(chunks, entryFile) {
return [...chunks.values()].find((chunk) => !chunk.isAsync && chunk.hasAbsolutePath(entryFile));
}
function removeEntryDepsFromAsyncChunks(entryChunk, chunks) {
for (const chunk of chunks.values()) {
if (!chunk.isEntry && chunk.isAsync) {
for (const dep of chunk.deps.values()) {
if (entryChunk.deps.has(dep)) {
// Remove the dependency from the async chunk since it will be loaded in the main chunk.
chunk.deps.delete(dep);
}
}
}
}
}
function extractCommonChunk(chunks, graph, options) {
const toCompare = [...chunks.values()];
const commonDependencies = [];
while (toCompare.length) {
const chunk = toCompare.shift();
for (const chunk2 of toCompare) {
if (chunk !== chunk2 && chunk.isAsync && chunk2.isAsync) {
const commonDeps = [...chunk.deps].filter((dep) => chunk2.deps.has(dep));
for (const dep of commonDeps) {
chunk.deps.delete(dep);
chunk2.deps.delete(dep);
}
commonDependencies.push(...commonDeps);
}
}
}
// If common dependencies were found, extract them to the shared chunk.
if (commonDependencies.length) {
const commonDependenciesUnique = [...new Set(commonDependencies)];
return new Chunk('/__common.js', commonDependenciesUnique, graph, options, false, true);
}
return undefined;
}
function deduplicateAgainstKnownChunks(chunks, entryChunk, commonChunk) {
// TODO: Optimize this pass more.
// Remove all dependencies from async chunks that are already in the common chunk.
for (const chunk of [...chunks.values()]) {
if (!chunk.isEntry && chunk !== commonChunk) {
for (const dep of chunk.deps) {
if (entryChunk.deps.has(dep) || commonChunk?.deps.has(dep)) {
chunk.deps.delete(dep);
}
}
}
}
}
function removeEmptyChunks(chunks) {
for (const chunk of [...chunks.values()]) {
if (!chunk.isEntry && chunk.deps.size === 0) {
chunks.delete(chunk);
}
}
}
function createRuntimeChunk(entryChunk, chunks, graph, options) {
const runtimeChunk = new Chunk('/__expo-metro-runtime.js', [], graph, options, false, true);
// All premodules (including metro-runtime) should load first
for (const preModule of entryChunk.preModules) {
runtimeChunk.preModules.add(preModule);
}
entryChunk.preModules = new Set();
for (const chunk of chunks) {
// Runtime chunk has to load before any other a.k.a all chunks require it.
chunk.requiredChunks.add(runtimeChunk);
}
chunks.add(runtimeChunk);
}
async function serializeChunksAsync(chunks, serializerConfig, options) {
const jsAssets = [];
const chunksArray = [...chunks.values()];
await Promise.all(chunksArray.map(async (chunk) => {
jsAssets.push(...(await chunk.serializeToAssetsAsync(serializerConfig, chunksArray, options)));
}));
return jsAssets;
}
//# sourceMappingURL=serializeChunks.js.map