"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