"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.importAnalysisPlugin = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const chalk_1 = __importDefault(require("chalk"));
const magic_string_1 = __importDefault(require("magic-string"));
const es_module_lexer_1 = require("es-module-lexer");
const css_1 = require("./css");
const utils_1 = require("../utils");
const hmr_1 = require("../server/hmr");
const constants_1 = require("../constants");
const asset_1 = require("./asset");
const acorn_1 = require("acorn");
const pluginutils_1 = require("@rollup/pluginutils");
const isDebug = !!process.env.DEBUG;
const debugRewrite = utils_1.createDebugger('vite:rewrite');
const skipRE = /\.(map|json)$/;
const canSkip = (id) => skipRE.test(id) || css_1.isDirectCSSRequest(id);
function markExplicitImport(url) {
    if (!utils_1.isJSRequest(utils_1.cleanUrl(url)) && !css_1.isCSSRequest(url)) {
        return utils_1.injectQuery(url, 'import');
    }
    return url;
}
/**
 * Server-only plugin that lexes, resolves, rewrites and analyzes url imports.
 *
 * - Imports are resolved to ensure they exist on disk
 *
 * - Lexes HMR accept calls and updates import relationships in the module graph
 *
 * - Bare module imports are resolved (by @rollup-plugin/node-resolve) to
 * absolute file paths, e.g.
 *
 *     ```js
 *     import 'foo'
 *     ```
 *     is rewritten to
 *     ```js
 *     import '/@fs//project/node_modules/foo/dist/foo.js'
 *     ```
 *
 * - CSS imports are appended with `.js` since both the js module and the actual
 * css (referenced via <link>) may go through the trasnform pipeline:
 *
 *     ```js
 *     import './style.css'
 *     ```
 *     is rewritten to
 *     ```js
 *     import './style.css.js'
 *     ```
 */
function importAnalysisPlugin(config) {
    let server;
    return {
        name: 'vite:imports',
        configureServer(_server) {
            server = _server;
        },
        async transform(source, importer) {
            const prettyImporter = utils_1.prettifyUrl(utils_1.normalizePath(importer), config.root);
            if (canSkip(importer)) {
                isDebug && debugRewrite(chalk_1.default.dim(`[skipped] ${prettyImporter}`));
                return null;
            }
            const rewriteStart = Date.now();
            let timeSpentResolving = 0;
            await es_module_lexer_1.init;
            let imports = [];
            try {
                imports = es_module_lexer_1.parse(source)[0];
            }
            catch (e) {
                const isVue = importer.endsWith('.vue');
                const maybeJSX = !isVue && utils_1.isJSRequest(importer);
                const msg = isVue
                    ? `Install @vitejs/plugin-vue to handle .vue files.`
                    : maybeJSX
                        ? `If you are using JSX, make sure to name the file with the .jsx or .tsx extension.`
                        : `You may need to install appropriate plugins to handle the ${path_1.default.extname(importer)} file format.`;
                this.error(`Failed to parse source for import analysis because the content ` +
                    `contains invalid JS syntax. ` +
                    msg, e.idx);
            }
            if (!imports.length) {
                isDebug &&
                    debugRewrite(`${utils_1.timeFrom(rewriteStart)} ${chalk_1.default.dim(`[no imports] ${prettyImporter}`)}`);
                return source;
            }
            let hasHMR = false;
            let isSelfAccepting = false;
            let hasEnv = false;
            let s;
            const str = () => s || (s = new magic_string_1.default(source));
            // vite-only server context
            const { moduleGraph } = server;
            // since we are already in the transform phase of the importer, it must
            // have been loaded so its entry is guaranteed in the module graph.
            const importerModule = moduleGraph.getModuleById(importer);
            const importedUrls = new Set();
            const acceptedUrls = new Set();
            const toAbsoluteUrl = (url) => path_1.default.posix.resolve(path_1.default.posix.dirname(importerModule.url), url);
            for (let i = 0; i < imports.length; i++) {
                const { s: start, e: end, ss: expStart, se: expEnd, d: dynamicIndex } = imports[i];
                const rawUrl = source.slice(start, end);
                let url = rawUrl;
                if (utils_1.isExternalUrl(url) || utils_1.isDataUrl(url)) {
                    continue;
                }
                // check import.meta usage
                if (url === 'import.meta') {
                    const prop = source.slice(end, end + 4);
                    if (prop === '.hot') {
                        hasHMR = true;
                        if (source.slice(end + 4, end + 11) === '.accept') {
                            // further analyze accepted modules
                            if (hmr_1.lexAcceptedHmrDeps(source, source.indexOf('(', end + 11) + 1, acceptedUrls)) {
                                isSelfAccepting = true;
                            }
                        }
                    }
                    else if (prop === '.env') {
                        hasEnv = true;
                    }
                    continue;
                }
                // For dynamic id, check if it's a literal that we can resolve
                let hasViteIgnore = false;
                let isLiteralDynamicId = false;
                if (dynamicIndex >= 0) {
                    // check @vite-ignore which suppresses dynamic import warning
                    hasViteIgnore = /\/\*\s*@vite-ignore\s*\*\//.test(url);
                    // #998 remove comment
                    url = url.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '');
                    const literalIdMatch = url.match(/^\s*(?:'([^']+)'|"([^"]+)")\s*$/);
                    if (literalIdMatch) {
                        isLiteralDynamicId = true;
                        url = literalIdMatch[1] || literalIdMatch[2];
                    }
                }
                // If resolvable, let's resolve it
                if (dynamicIndex === -1 || isLiteralDynamicId) {
                    // skip client
                    if (url === constants_1.CLIENT_PUBLIC_PATH) {
                        continue;
                    }
                    // warn imports to non-asset /public files
                    if (url.startsWith('/') &&
                        !config.assetsInclude(utils_1.cleanUrl(url)) &&
                        asset_1.checkPublicFile(url, config.root)) {
                        throw new Error(`Cannot import non-asset file ${url} which is inside /public.` +
                            `JS/CSS files inside /public are copied as-is on build and ` +
                            `can only be referenced via <script src> or <link href> in html.`);
                    }
                    const resolveStart = Date.now();
                    const resolved = await this.resolve(url, importer);
                    if (!resolved) {
                        this.error(`Failed to resolve import "${rawUrl}". Does the file exist?`, start);
                    }
                    timeSpentResolving += Date.now() - resolveStart;
                    const isRelative = url.startsWith('.');
                    // normalize all imports into resolved URLs
                    // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js`
                    if (url !== resolved.id) {
                        if (resolved.id.startsWith(config.root)) {
                            // in root: infer short absolute path from root
                            url = resolved.id.slice(config.root.length);
                        }
                        else if (fs_1.default.existsSync(utils_1.cleanUrl(resolved.id))) {
                            // exists but out of root: rewrite to absolute /@fs/ paths
                            url = constants_1.FS_PREFIX + utils_1.normalizePath(resolved.id);
                        }
                        else {
                            url = resolved.id;
                        }
                    }
                    // if the resolved id is not a valid browser import specifier,
                    // prefix it to make it valid. We will strip this before feeding it
                    // back into the transform pipeline
                    if (!url.startsWith('.') && !url.startsWith('/')) {
                        url = constants_1.VALID_ID_PREFIX + resolved.id;
                    }
                    // mark non-js/css imports with `?import`
                    url = markExplicitImport(url);
                    // for relative js/css imports, inherit importer's version query
                    // do not do this for unknown type imports, otherwise the appended
                    // query can break 3rd party plugin's extension checks.
                    if (isRelative && !/[\?&]import\b/.test(url)) {
                        const versionMatch = importer.match(constants_1.DEP_VERSION_RE);
                        if (versionMatch) {
                            url = utils_1.injectQuery(url, versionMatch[1]);
                        }
                    }
                    // check if the dep has been hmr updated. If yes, we need to attach
                    // its last updated timestamp to force the browser to fetch the most
                    // up-to-date version of this module.
                    try {
                        const depModule = await moduleGraph.ensureEntryFromUrl(url);
                        if (depModule.lastHMRTimestamp > 0) {
                            url = utils_1.injectQuery(url, `t=${depModule.lastHMRTimestamp}`);
                        }
                    }
                    catch (e) {
                        // it's possible that the dep fails to resolve (non-existent import)
                        // attach location to the missing import
                        e.pos = start;
                        throw e;
                    }
                    // rewrite
                    if (url !== rawUrl) {
                        // for optimized cjs deps, support named imports by rewriting named
                        // imports to const assignments.
                        if (isOptimizedCjs(resolved.id, server)) {
                            if (isLiteralDynamicId) {
                                // rewrite `import('package')` to expose module.exports
                                // note plugin-commonjs' behavior is exposing all properties on
                                // `module.exports` PLUS `module.exports` itself as `default`.
                                str().overwrite(dynamicIndex, end + 1, `import('${url}').then(m => ({ ...m.default, default: m.default }))`);
                            }
                            else {
                                const exp = source.slice(expStart, expEnd);
                                str().overwrite(expStart, expEnd, transformCjsImport(exp, url, rawUrl, i));
                            }
                        }
                        else {
                            str().overwrite(start, end, isLiteralDynamicId ? `'${url}'` : url);
                        }
                    }
                    // record for HMR import chain analysis
                    importedUrls.add(url);
                }
                else if (!hasViteIgnore && !isSupportedDynamicImport(url)) {
                    this.warn(`\n` +
                        chalk_1.default.cyan(importerModule.file) +
                        `\n` +
                        utils_1.generateCodeFrame(source, start) +
                        `\nThe above dynamic import cannot be analyzed by vite.\n` +
                        `See ${chalk_1.default.blue(`https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations`)} ` +
                        `for supported dynamic import formats. ` +
                        `If this is intended to be left as-is, you can use the ` +
                        `/* @vite-ignore */ comment inside the import() call to suppress this warning.\n`);
                }
            }
            if (hasEnv) {
                // inject import.meta.env
                str().prepend(`import.meta.env = ${JSON.stringify(config.env)};`);
            }
            if (hasHMR) {
                hmr_1.debugHmr(`${isSelfAccepting
                    ? `[self-accepts]`
                    : acceptedUrls.size
                        ? `[accepts-deps]`
                        : `[detected api usage]`} ${prettyImporter}`);
                // inject hot context
                str().prepend(`import { createHotContext } from "${constants_1.CLIENT_PUBLIC_PATH}";` +
                    `import.meta.hot = createHotContext(${JSON.stringify(importerModule.url)});`);
            }
            // normalize and rewrite accepted urls
            const normalizedAcceptedUrls = new Set();
            for (const { url, start, end } of acceptedUrls) {
                const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(markExplicitImport(url)));
                normalizedAcceptedUrls.add(normalized);
                str().overwrite(start, end, JSON.stringify(normalized));
            }
            // update the module graph for HMR analysis.
            // node CSS imports does its own graph update in the css plugin so we
            // only handle js graph updates here.
            if (!css_1.isCSSRequest(importer)) {
                const prunedImports = await moduleGraph.updateModuleInfo(importerModule, importedUrls, normalizedAcceptedUrls, isSelfAccepting);
                if (hasHMR && prunedImports) {
                    hmr_1.handlePrunedModules(prunedImports, server);
                }
            }
            isDebug &&
                debugRewrite(`${utils_1.timeFrom(rewriteStart, timeSpentResolving)} ${prettyImporter}`);
            if (s) {
                return s.toString();
            }
            else {
                return source;
            }
        }
    };
}
exports.importAnalysisPlugin = importAnalysisPlugin;
/**
 * https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
 * This is probably less accurate but is much cheaper than a full AST parse.
 */
function isSupportedDynamicImport(url) {
    url = url.trim().slice(1, -1);
    // must be relative
    if (!url.startsWith('./') && !url.startsWith('../')) {
        return false;
    }
    // must have extension
    if (!path_1.default.extname(url)) {
        return false;
    }
    // must be more specific if importing from same dir
    if (url.startsWith('./${') && url.indexOf('/') === url.lastIndexOf('/')) {
        return false;
    }
    return true;
}
function isOptimizedCjs(id, { optimizeDepsMetadata, config: { optimizeCacheDir } }) {
    if (optimizeDepsMetadata && optimizeCacheDir) {
        const relative = path_1.default.relative(optimizeCacheDir, utils_1.cleanUrl(id));
        return relative in optimizeDepsMetadata.cjsEntries;
    }
    return false;
}
/**
 * Detect import statements to a known optimized CJS dependency and provide
 * ES named imports interop. We do this by rewriting named imports to a variable
 * assignment to the corresponding property on the `module.exports` of the cjs
 * module. Note this doesn't support dynamic re-assisgnments from within the cjs
 * module.
 *
 * Credits \@csr632 via #837
 */
function transformCjsImport(importExp, url, rawUrl, importIndex) {
    const ast = acorn_1.parse(importExp, {
        ecmaVersion: 2020,
        sourceType: 'module'
    }).body[0];
    const importNames = [];
    ast.specifiers.forEach((obj) => {
        if (obj.type === 'ImportSpecifier' && obj.imported.type === 'Identifier') {
            const importedName = obj.imported.name;
            const localName = obj.local.name;
            importNames.push({ importedName, localName });
        }
        else if (obj.type === 'ImportDefaultSpecifier') {
            importNames.push({ importedName: 'default', localName: obj.local.name });
        }
        else if (obj.type === 'ImportNamespaceSpecifier') {
            importNames.push({ importedName: '*', localName: obj.local.name });
        }
    });
    // If there is multiple import for same id in one file,
    // importIndex will prevent the cjsModuleName to be duplicate
    const cjsModuleName = pluginutils_1.makeLegalIdentifier(`$viteCjsImport${importIndex}_${rawUrl}`);
    const lines = [`import ${cjsModuleName} from "${url}";`];
    importNames.forEach(({ importedName, localName }) => {
        if (importedName === '*' || importedName === 'default') {
            lines.push(`const ${localName} = ${cjsModuleName};`);
        }
        else {
            lines.push(`const ${localName} = ${cjsModuleName}["${importedName}"];`);
        }
    });
    return lines.join('\n');
}
//# sourceMappingURL=importsAnalysis.js.map