"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDepHash = exports.optimizeDeps = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const chalk_1 = __importDefault(require("chalk"));
const crypto_1 = require("crypto");
const constants_1 = require("../constants");
const es_module_lexer_1 = require("es-module-lexer");
const build_1 = require("../build");
const utils_1 = require("../utils");
const depAssetPlugin_1 = require("./depAssetPlugin");
const depMetadataPlugin_1 = require("./depMetadataPlugin");
const pluginContainer_1 = require("../server/pluginContainer");
const resolve_1 = require("../plugins/resolve");
const plugin_alias_1 = __importDefault(require("@rollup/plugin-alias"));
const plugin_commonjs_1 = __importDefault(require("@rollup/plugin-commonjs"));
const plugin_json_1 = __importDefault(require("@rollup/plugin-json"));
const define_1 = require("../plugins/define");
const debug = utils_1.createDebugger('vite:optimize');
async function optimizeDeps(config, force = config.server.force, asCommand = false) {
    config = {
        ...config,
        command: 'build'
    };
    const { root, logger, optimizeCacheDir: cacheDir } = config;
    const log = asCommand ? logger.info : debug;
    if (!cacheDir) {
        log(`No package.json. Skipping.`);
        return;
    }
    const dataPath = path_1.default.join(cacheDir, 'metadata.json');
    const data = {
        hash: getDepHash(root, config.configPath),
        map: {},
        cjsEntries: {}
    };
    if (!force) {
        let prevData;
        try {
            prevData = JSON.parse(fs_1.default.readFileSync(dataPath, 'utf-8'));
        }
        catch (e) { }
        // hash is consistent, no need to re-bundle
        if (prevData && prevData.hash === data.hash) {
            log('Hash is consistent. Skipping. Use --force to override.');
            return;
        }
    }
    if (fs_1.default.existsSync(cacheDir)) {
        utils_1.emptyDir(cacheDir);
    }
    else {
        fs_1.default.mkdirSync(cacheDir, { recursive: true });
    }
    const options = config.optimizeDeps || {};
    // Determine deps to optimize. The goal is to only pre-bundle deps that falls
    // under one of the following categories:
    // 1. Has imports to relative files (e.g. lodash-es, lit-html)
    // 2. Has imports to bare modules that are not in the project's own deps
    //    (i.e. esm that imports its own dependencies, e.g. styled-components)
    await es_module_lexer_1.init;
    const aliasResolver = await pluginContainer_1.createPluginContainer({
        ...config,
        plugins: [plugin_alias_1.default({ entries: config.alias })]
    });
    const { qualified, external } = await resolveQualifiedDeps(root, config, aliasResolver);
    // Resolve deps from linked packages in a monorepo
    if (options.link) {
        for (const linkedDep of options.link) {
            await resolveLinkedDeps(config.root, linkedDep, qualified, external, config, aliasResolver);
        }
    }
    // Force included deps - these can also be deep paths
    if (options.include) {
        options.include.forEach((id) => {
            const filePath = resolve_1.tryNodeResolve(id, root);
            if (filePath) {
                qualified[id] = filePath.id;
            }
        });
    }
    if (!Object.keys(qualified).length) {
        utils_1.writeFile(dataPath, JSON.stringify(data, null, 2));
        log(`No listed dependency requires optimization. Skipping.`);
        return;
    }
    const depsString = Object.keys(qualified)
        .map((id) => chalk_1.default.yellow(id))
        .join(`, `);
    if (!asCommand) {
        // This is auto run on server start - let the user know that we are
        // pre-optimizing deps
        logger.info(chalk_1.default.greenBright(`Optimizable dependencies detected:\n${depsString}`));
        logger.info(`Pre-bundling them to speed up dev server page load...\n` +
            `(this will be run only when your dependencies have changed)`);
    }
    else {
        logger.info(chalk_1.default.greenBright(`Optimizing dependencies:\n${depsString}`));
    }
    try {
        const rollup = require('rollup');
        const bundle = await rollup.rollup({
            input: qualified,
            external,
            onwarn(warning, warn) {
                build_1.onRollupWarning(warning, warn, options.allowNodeBuiltins);
            },
            plugins: [
                plugin_alias_1.default({ entries: config.alias }),
                depAssetPlugin_1.depAssetExternalPlugin(config),
                resolve_1.resolvePlugin({
                    root: config.root,
                    dedupe: config.dedupe,
                    isBuild: true,
                    asSrc: false
                }),
                plugin_json_1.default({
                    preferConst: true,
                    namedExports: true
                }),
                plugin_commonjs_1.default({
                    include: [/node_modules/],
                    extensions: ['.js', '.cjs']
                }),
                define_1.buildDefinePlugin(config),
                depAssetPlugin_1.depAssetRewritePlugin(config),
                depMetadataPlugin_1.recordCjsEntryPlugin(data)
            ]
        });
        const { output } = await bundle.generate({
            format: 'es',
            exports: 'named',
            entryFileNames: '[name].[hash].js',
            chunkFileNames: 'common/[name].[hash].js'
        });
        for (const chunk of output) {
            if (chunk.type === 'chunk') {
                utils_1.writeFile(path_1.default.join(cacheDir, chunk.fileName), chunk.code);
            }
        }
        utils_1.writeFile(dataPath, JSON.stringify(data, null, 2));
    }
    catch (e) {
        delete e.watchFiles;
        logger.error(chalk_1.default.red(`\nDep optimization failed with error:`));
        if (e.code === 'PARSE_ERROR') {
            e.message += `\n\n${chalk_1.default.cyan(path_1.default.relative(root, e.loc.file))}\n${chalk_1.default.dim(e.frame)}`;
        }
        else if (e.message.match('Node built-in')) {
            e.message += chalk_1.default.yellow(`\n\nTip:\nMake sure your "dependencies" only include packages that you\n` +
                `intend to use in the browser. If it's a Node.js package, it\n` +
                `should be in "devDependencies".\n\n` +
                `If you do intend to use this dependency in the browser and the\n` +
                `dependency does not actually use these Node built-ins in the\n` +
                `browser, you can add the dependency (not the built-in) to the\n` +
                `"optimizeDeps.allowNodeBuiltins" option in vite.config.js.\n\n` +
                `If that results in a runtime error, then unfortunately the\n` +
                `package is not distributed in a web-friendly format. You should\n` +
                `open an issue in its repo, or look for a modern alternative.`);
        }
        throw e;
    }
}
exports.optimizeDeps = optimizeDeps;
const KNOWN_IGNORE_LIST = new Set([
    'vue',
    'vite',
    'vitepress',
    'tailwindcss',
    '@tailwindcss/ui',
    '@pika/react',
    '@pika/react-dom'
]);
async function resolveQualifiedDeps(root, config, aliasResolver) {
    var _a;
    const { include, exclude, link } = config.optimizeDeps || {};
    const qualified = {};
    const external = [];
    const pkgContent = utils_1.lookupFile(root, ['package.json']);
    if (!pkgContent) {
        return {
            qualified,
            external
        };
    }
    const pkg = JSON.parse(pkgContent);
    const deps = Object.keys(pkg.dependencies || {});
    const linked = [];
    for (const id of deps) {
        if (include && include.includes(id)) {
            // already force included
            continue;
        }
        if (exclude && exclude.includes(id)) {
            debug(`skipping ${id} (excluded)`);
            continue;
        }
        if (link && link.includes(id)) {
            debug(`skipping ${id} (link)`);
            continue;
        }
        if (KNOWN_IGNORE_LIST.has(id)) {
            debug(`skipping ${id} (internal excluded)`);
            continue;
        }
        // #804
        if (id.startsWith('@types/')) {
            debug(`skipping ${id} (ts declaration)`);
            continue;
        }
        let filePath;
        try {
            const resolved = resolve_1.tryNodeResolve(id, root);
            filePath = resolved && resolved.id;
        }
        catch (e) { }
        if (!filePath) {
            debug(`skipping ${id} (cannot resolve entry)`);
            continue;
        }
        if (!filePath.includes('node_modules')) {
            debug(`skipping ${id} (not a node_modules dep, likely linked)`);
            // resolve deps of the linked module
            linked.push(id);
            continue;
        }
        if (!constants_1.SUPPORTED_EXTS.includes(path_1.default.extname(filePath))) {
            debug(`skipping ${id} (entry is not js)`);
            continue;
        }
        const content = fs_1.default.readFileSync(filePath, 'utf-8');
        const [imports, exports] = es_module_lexer_1.parse(content);
        if (!exports.length && !/export\s+\*\s+from/.test(content)) {
            debug(`optimizing ${id} (no exports, likely commonjs)`);
            qualified[id] = filePath;
            continue;
        }
        for (const { s, e } of imports) {
            let i = content.slice(s, e).trim();
            i = ((_a = (await aliasResolver.resolveId(i))) === null || _a === void 0 ? void 0 : _a.id) || i;
            if (i.startsWith('.')) {
                debug(`optimizing ${id} (contains relative imports)`);
                qualified[id] = filePath;
                continue;
            }
            if (!deps.includes(i)) {
                debug(`optimizing ${id} (imports sub dependencies)`);
                qualified[id] = filePath;
                continue;
            }
        }
        debug(`skipping ${id} (single esm file, doesn't need optimization)`);
    }
    // mark non-optimized deps as external
    external.push(...(await Promise.all(deps
        .filter((id) => !qualified[id])
        // make sure aliased deps are external
        // https://github.com/vitejs/vite-plugin-react/issues/4
        .map(async (id) => { var _a; return ((_a = (await aliasResolver.resolveId(id))) === null || _a === void 0 ? void 0 : _a.id) || id; }))));
    if (linked.length) {
        for (const dep of linked) {
            await resolveLinkedDeps(root, dep, qualified, external, config, aliasResolver);
        }
    }
    return {
        qualified,
        external
    };
}
async function resolveLinkedDeps(root, dep, qualified, external, config, aliasResolver) {
    const depRoot = path_1.default.dirname(utils_1.resolveFrom(`${dep}/package.json`, root));
    const { qualified: q, external: e } = await resolveQualifiedDeps(depRoot, config, aliasResolver);
    Object.keys(q).forEach((id) => {
        if (!qualified[id]) {
            qualified[id] = q[id];
        }
    });
    e.forEach((id) => {
        if (!external.includes(id)) {
            external.push(id);
        }
    });
}
const lockfileFormats = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
let cachedHash;
function getDepHash(root, configPath) {
    if (cachedHash) {
        return cachedHash;
    }
    let content = utils_1.lookupFile(root, lockfileFormats) || '';
    const pkg = JSON.parse(utils_1.lookupFile(root, [`package.json`]) || '{}');
    content += JSON.stringify(pkg.dependencies);
    // also take config into account
    if (configPath) {
        content += fs_1.default.readFileSync(configPath, 'utf-8');
    }
    return crypto_1.createHash('sha1').update(content).digest('base64');
}
exports.getDepHash = getDepHash;
//# sourceMappingURL=index.js.map