"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyHtmlTransforms = exports.resolveHtmlTransforms = exports.buildHtmlPlugin = exports.htmlPlugin = exports.scriptRE = exports.htmlCommentRE = exports.isHTMLProxy = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const utils_1 = require("../utils");
const slash_1 = __importDefault(require("slash"));
const compiler_dom_1 = require("@vue/compiler-dom");
const magic_string_1 = __importDefault(require("magic-string"));
const asset_1 = require("./asset");
const css_1 = require("./css");
const htmlProxyRE = /\?html-proxy&index=(\d+)\.js$/;
exports.isHTMLProxy = (id) => htmlProxyRE.test(id);
exports.htmlCommentRE = /<!--[\s\S]*?-->/g;
exports.scriptRE = /(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)([\s\S]*?)<\/script>/gm;
function htmlPlugin() {
    return {
        name: 'vite:html',
        resolveId(id) {
            if (htmlProxyRE.test(id)) {
                return id;
            }
        },
        load(id) {
            const proxyMatch = id.match(htmlProxyRE);
            if (proxyMatch) {
                const index = Number(proxyMatch[1]);
                const file = utils_1.cleanUrl(id);
                const html = fs_1.default.readFileSync(file, 'utf-8').replace(exports.htmlCommentRE, '');
                let match;
                exports.scriptRE.lastIndex = 0;
                for (let i = 0; i <= index; i++) {
                    match = exports.scriptRE.exec(html);
                }
                if (match) {
                    return match[2];
                }
                else {
                    throw new Error(`No matching html proxy module found from ${id}`);
                }
            }
        }
    };
}
exports.htmlPlugin = htmlPlugin;
// this extends the config in @vue/compiler-sfc with <link href>
const assetAttrsConfig = {
    link: ['href'],
    video: ['src', 'poster'],
    source: ['src'],
    img: ['src'],
    image: ['xlink:href', 'href'],
    use: ['xlink:href', 'href']
};
/**
 * Compiles index.html into an entry js module
 */
function buildHtmlPlugin(config) {
    const [preHooks, postHooks] = resolveHtmlTransforms(config.plugins);
    const processedHtml = new Map();
    const isExcludedUrl = (url) => utils_1.isExternalUrl(url) || utils_1.isDataUrl(url);
    return {
        name: 'vite:build-html',
        async transform(html, id) {
            if (id.endsWith('.html')) {
                const publicPath = `/${slash_1.default(path_1.default.relative(config.root, id))}`;
                // pre-transform
                html = await applyHtmlTransforms(html, publicPath, id, preHooks);
                function formatError(e) {
                    // normalize the error to rollup format
                    if (e.loc) {
                        e.frame = utils_1.generateCodeFrame(html, e.loc.start.offset);
                        e.loc = {
                            file: id,
                            line: e.loc.start.line,
                            column: e.loc.start.column
                        };
                    }
                    return e;
                }
                // @vue/compiler-core doesn't like lowercase doctypes
                html = html.replace(/<!doctype\s/i, '<!DOCTYPE ');
                let ast;
                try {
                    ast = compiler_dom_1.parse(html);
                }
                catch (e) {
                    this.error(formatError(e));
                }
                let js = '';
                const s = new magic_string_1.default(html);
                const assetUrls = [];
                let inlineModuleIndex = -1;
                const viteHtmlTransform = (node) => {
                    if (node.type !== 1 /* ELEMENT */) {
                        return;
                    }
                    let shouldRemove = false;
                    // script tags
                    if (node.tag === 'script') {
                        const srcAttr = node.props.find((p) => p.type === 6 /* ATTRIBUTE */ && p.name === 'src');
                        const typeAttr = node.props.find((p) => p.type === 6 /* ATTRIBUTE */ && p.name === 'type');
                        const isJsModule = typeAttr && typeAttr.value && typeAttr.value.content === 'module';
                        const url = srcAttr && srcAttr.value && srcAttr.value.content;
                        const publicUrl = url && config.publicUrls[url];
                        if (publicUrl) {
                            // referencing public dir url, prefix with base
                            s.overwrite(srcAttr.value.loc.start.offset, srcAttr.value.loc.end.offset, config.build.base + publicUrl.slice(1));
                        }
                        else if (isJsModule) {
                            inlineModuleIndex++;
                            if (url && !isExcludedUrl(url)) {
                                // <script type="module" src="..."/>
                                // add it as an import
                                js += `\nimport ${JSON.stringify(url)}`;
                                shouldRemove = true;
                            }
                            else if (node.children.length) {
                                // <script type="module">...</script>
                                js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"`;
                                shouldRemove = true;
                            }
                        }
                    }
                    // For asset references in index.html, also generate an import
                    // statement for each - this will be handled by the asset plugin
                    const assetAttrs = assetAttrsConfig[node.tag];
                    if (assetAttrs) {
                        for (const p of node.props) {
                            if (p.type === 6 /* ATTRIBUTE */ &&
                                p.value &&
                                assetAttrs.includes(p.name)) {
                                const url = p.value.content;
                                const publicUrl = config.publicUrls[url];
                                if (publicUrl) {
                                    s.overwrite(p.value.loc.start.offset, p.value.loc.end.offset, config.build.base + publicUrl.slice(1));
                                }
                                else if (!isExcludedUrl(url)) {
                                    if (node.tag === 'link' && css_1.isCSSRequest(url)) {
                                        // CSS references, convert to import
                                        js += `\nimport ${JSON.stringify(url)}`;
                                        shouldRemove = true;
                                    }
                                    else {
                                        assetUrls.push(p);
                                    }
                                }
                            }
                        }
                    }
                    if (shouldRemove) {
                        // remove the script tag from the html. we are going to inject new
                        // ones in the end.
                        s.remove(node.loc.start.offset, node.loc.end.offset);
                    }
                };
                try {
                    compiler_dom_1.transform(ast, {
                        nodeTransforms: [viteHtmlTransform]
                    });
                }
                catch (e) {
                    this.error(formatError(e));
                }
                // for each encountered asset url, rewrite original html so that it
                // references the post-build location.
                for (const attr of assetUrls) {
                    const value = attr.value;
                    const url = await asset_1.urlToBuiltUrl(value.content, id, config, this);
                    s.overwrite(value.loc.start.offset, value.loc.end.offset, JSON.stringify(url));
                }
                processedHtml.set(id, s.toString());
                return js;
            }
        },
        async generateBundle(_, bundle) {
            const getPreloadLinksForChunk = (chunk) => {
                const tags = chunk.imports.map((file) => ({
                    tag: 'link',
                    attrs: {
                        rel: 'modulepreload',
                        href: toPublicPath(file, config)
                    }
                }));
                chunk.imports.forEach((file) => {
                    tags.push(...getPreloadLinksForChunk(bundle[file]));
                });
                return tags;
            };
            const getCssTagsForChunk = (chunk) => {
                const tags = [];
                const cssFileHandle = css_1.chunkToEmittedCssFileMap.get(chunk);
                if (cssFileHandle) {
                    const file = this.getFileName(cssFileHandle);
                    tags.push({
                        tag: 'link',
                        attrs: {
                            rel: 'stylesheet',
                            href: toPublicPath(file, config)
                        }
                    });
                }
                chunk.imports.forEach((file) => {
                    tags.push(...getCssTagsForChunk(bundle[file]));
                });
                return tags;
            };
            for (const [id, html] of processedHtml) {
                // resolve asset url references
                let result = html.replace(asset_1.assetUrlRE, (_, fileId, postfix = '') => {
                    return config.build.base + this.getFileName(fileId) + postfix;
                });
                // find corresponding entry chunk
                const chunk = Object.values(bundle).find((chunk) => chunk.type === 'chunk' &&
                    chunk.isEntry &&
                    chunk.facadeModuleId === id);
                // inject chunk asset links
                if (chunk) {
                    const assetTags = [
                        // js entry chunk for this page
                        {
                            tag: 'script',
                            attrs: {
                                type: 'module',
                                src: toPublicPath(chunk.fileName, config)
                            }
                        },
                        // preload for imports
                        ...getPreloadLinksForChunk(chunk),
                        ...getCssTagsForChunk(chunk)
                    ];
                    result = injectToHead(result, assetTags);
                }
                const shortEmitName = path_1.default.posix.relative(config.root, id);
                result = await applyHtmlTransforms(result, '/' + shortEmitName, id, postHooks, undefined, bundle);
                this.emitFile({
                    type: 'asset',
                    fileName: shortEmitName,
                    source: result
                });
            }
        }
    };
}
exports.buildHtmlPlugin = buildHtmlPlugin;
function resolveHtmlTransforms(plugins) {
    const preHooks = [];
    const postHooks = [];
    for (const plugin of plugins) {
        const hook = plugin.transformIndexHtml;
        if (hook) {
            if (typeof hook === 'function') {
                postHooks.push(hook);
            }
            else if (hook.enforce === 'pre') {
                preHooks.push(hook.transform);
            }
            else {
                postHooks.push(hook.transform);
            }
        }
    }
    return [preHooks, postHooks];
}
exports.resolveHtmlTransforms = resolveHtmlTransforms;
async function applyHtmlTransforms(html, path, filename, hooks, server, bundle) {
    const headTags = [];
    const headPrependTags = [];
    const bodyTags = [];
    const ctx = {
        path,
        filename,
        server,
        bundle
    };
    for (const hook of hooks) {
        const res = await hook(html, ctx);
        if (!res) {
            continue;
        }
        else if (typeof res === 'string') {
            html = res;
        }
        else {
            let tags;
            if (Array.isArray(res)) {
                tags = res;
            }
            else {
                html = res.html || html;
                tags = res.tags;
            }
            for (const tag of tags) {
                if (tag.injectTo === 'body') {
                    bodyTags.push(tag);
                }
                else if (tag.injectTo === 'head') {
                    headTags.push(tag);
                }
                else {
                    headPrependTags.push(tag);
                }
            }
        }
    }
    // inject tags
    if (headPrependTags.length) {
        html = injectToHead(html, headPrependTags, true);
    }
    if (headTags.length) {
        html = injectToHead(html, headTags);
    }
    if (bodyTags.length) {
        html = injectToBody(html, bodyTags);
    }
    return html;
}
exports.applyHtmlTransforms = applyHtmlTransforms;
function toPublicPath(filename, config) {
    return utils_1.isExternalUrl(filename) ? filename : config.build.base + filename;
}
const headInjectRE = /<\/head>/;
const headPrependInjectRE = [/<head>/, /<!doctype html>/i];
function injectToHead(html, tags, prepend = false) {
    const tagsHtml = serializeTags(tags);
    if (prepend) {
        // inject after head or doctype
        for (const re of headPrependInjectRE) {
            if (re.test(html)) {
                return html.replace(re, `$&\n${tagsHtml}`);
            }
        }
    }
    else {
        // inject before head close
        if (headInjectRE.test(html)) {
            return html.replace(headInjectRE, `${tagsHtml}\n$&`);
        }
    }
    // if no <head> tag is present, just prepend
    return tagsHtml + `\n` + html;
}
const bodyInjectRE = /<\/body>/;
function injectToBody(html, tags) {
    const tagsHtml = `\n` + serializeTags(tags);
    if (bodyInjectRE.test(html)) {
        return html.replace(bodyInjectRE, `${tagsHtml}\n$&`);
    }
    // if no body, append
    return html + `\n` + tagsHtml;
}
const unaryTags = new Set(['link', 'meta', 'base']);
function serializeTag({ tag, attrs, children }) {
    if (unaryTags.has(tag)) {
        return `<${tag}${serializeAttrs(attrs)}>`;
    }
    else {
        return `<${tag}${serializeAttrs(attrs)}>${serializeTags(children)}</${tag}>`;
    }
}
function serializeTags(tags) {
    if (typeof tags === 'string') {
        return tags;
    }
    else if (tags) {
        return tags.map(serializeTag).join(`\n  `);
    }
    return '';
}
function serializeAttrs(attrs) {
    let res = '';
    for (const key in attrs) {
        res += ` ${key}=${JSON.stringify(attrs[key])}`;
    }
    return res;
}
//# sourceMappingURL=html.js.map