"use strict";
// How HMR works
// 1. `.vue` files are transformed into `.js` files before being served
// 2. All `.js` files, before being served, are parsed to detect their imports
//    (this is done in `./serverPluginModuleRewrite.ts`) for module import rewriting.
//    During this we also record the importer/importee relationships which can be used for
//    HMR analysis (we do both at the same time to avoid double parse costs)
// 3. When a file changes, it triggers an HMR graph analysis, where we try to
//    walk its importer chains and see if we reach a "HMR boundary". An HMR
//    boundary is a file that explicitly indicated that it accepts hot updates
//    (by calling `import.meta.hot` APIs)
// 4. If any parent chain exhausts without ever running into an HMR boundary,
//    it's considered a "dead end". This causes a full page reload.
// 5. If a boundary is encountered, we check if the boundary's current
//    child importer is in the accepted list of the boundary (recorded while
//    parsing the file for HRM rewrite). If yes, record current child importer
//    in the `hmrBoundaries` Set.
// 6. If the graph walk finished without running into dead ends, send the
//    client to update all `hmrBoundaries`.
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rewriteFileWithHMR = exports.ensureMapEntry = exports.hmrPlugin = exports.latestVersionsMap = exports.hmrDirtyFilesMap = exports.importeeMap = exports.importerMap = exports.hmrDeclineSet = exports.hmrAcceptanceMap = exports.debugHmr = void 0;
const ws_1 = __importDefault(require("ws"));
const path_1 = __importDefault(require("path"));
const chalk_1 = __importDefault(require("chalk"));
const serverPluginVue_1 = require("./serverPluginVue");
const serverPluginModuleRewrite_1 = require("./serverPluginModuleRewrite");
const babelParse_1 = require("../utils/babelParse");
const lru_cache_1 = __importDefault(require("lru-cache"));
const slash_1 = __importDefault(require("slash"));
const cssUtils_1 = require("../utils/cssUtils");
const utils_1 = require("../utils");
const serverPluginClient_1 = require("./serverPluginClient");
exports.debugHmr = require('debug')('vite:hmr');
exports.hmrAcceptanceMap = new Map();
exports.hmrDeclineSet = new Set();
exports.importerMap = new Map();
exports.importeeMap = new Map();
// files that are dirty (i.e. in the import chain between the accept boundary
// and the actual changed file) for an hmr update at a given timestamp.
exports.hmrDirtyFilesMap = new lru_cache_1.default({ max: 10 });
exports.latestVersionsMap = new Map();
exports.hmrPlugin = ({ root, app, server, watcher, resolver, config }) => {
    app.use((ctx, next) => {
        if (ctx.query.t) {
            exports.latestVersionsMap.set(ctx.path, ctx.query.t);
        }
        return next();
    });
    // start a websocket server to send hmr notifications to the client
    const wss = new ws_1.default.Server({ noServer: true });
    server.on('upgrade', (req, socket, head) => {
        if (req.headers['sec-websocket-protocol'] === 'vite-hmr') {
            wss.handleUpgrade(req, socket, head, (ws) => {
                wss.emit('connection', ws, req);
            });
        }
    });
    wss.on('connection', (socket) => {
        exports.debugHmr('ws client connected');
        socket.send(JSON.stringify({ type: 'connected' }));
    });
    wss.on('error', (e) => {
        if (e.code !== 'EADDRINUSE') {
            console.error(chalk_1.default.red(`[vite] WebSocket server error:`));
            console.error(e);
        }
    });
    const send = (watcher.send = (payload) => {
        const stringified = JSON.stringify(payload, null, 2);
        exports.debugHmr(`update: ${stringified}`);
        wss.clients.forEach((client) => {
            if (client.readyState === ws_1.default.OPEN) {
                client.send(stringified);
            }
        });
    });
    const handleJSReload = (watcher.handleJSReload = (filePath, timestamp = Date.now()) => {
        // normal js file, but could be compiled from anything.
        // bust the vue cache in case this is a src imported file
        if (serverPluginVue_1.srcImportMap.has(filePath)) {
            exports.debugHmr(`busting Vue cache for ${filePath}`);
            serverPluginVue_1.vueCache.del(filePath);
        }
        const publicPath = resolver.fileToRequest(filePath);
        const importers = exports.importerMap.get(publicPath);
        if (importers || isHmrAccepted(publicPath, publicPath)) {
            const hmrBoundaries = new Set();
            const dirtyFiles = new Set();
            dirtyFiles.add(publicPath);
            const hasDeadEnd = walkImportChain(publicPath, importers || new Set(), hmrBoundaries, dirtyFiles);
            // record dirty files - this is used when HMR requests coming in with
            // timestamp to determine what files need to be force re-fetched
            exports.hmrDirtyFilesMap.set(String(timestamp), dirtyFiles);
            const relativeFile = '/' + slash_1.default(path_1.default.relative(root, filePath));
            if (hasDeadEnd) {
                send({
                    type: 'full-reload',
                    path: publicPath
                });
                console.log(chalk_1.default.green(`[vite] `) + `page reloaded.`);
            }
            else {
                const boundaries = [...hmrBoundaries];
                const file = boundaries.length === 1 ? boundaries[0] : `${boundaries.length} files`;
                console.log(chalk_1.default.green(`[vite:hmr] `) +
                    `${file} hot updated due to change in ${relativeFile}.`);
                send({
                    type: 'multi',
                    updates: boundaries.map((boundary) => {
                        return {
                            type: boundary.endsWith('vue') ? 'vue-reload' : 'js-update',
                            path: boundary,
                            changeSrcPath: publicPath,
                            timestamp
                        };
                    })
                });
            }
        }
        else {
            exports.debugHmr(`no importers for ${publicPath}.`);
        }
    });
    watcher.on('change', (file) => {
        if (!(file.endsWith('.vue') || cssUtils_1.isCSSRequest(file))) {
            // everything except plain .css are considered HMR dependencies.
            // plain css has its own HMR logic in ./serverPluginCss.ts.
            handleJSReload(file);
        }
    });
};
function walkImportChain(importee, importers, hmrBoundaries, dirtyFiles, currentChain = []) {
    if (exports.hmrDeclineSet.has(importee)) {
        // module explicitly declines HMR = dead end
        return true;
    }
    if (isHmrAccepted(importee, importee)) {
        // self-accepting module.
        hmrBoundaries.add(importee);
        dirtyFiles.add(importee);
        return false;
    }
    for (const importer of importers) {
        if (importer.endsWith('.vue') ||
            // explicitly accepted by this importer
            isHmrAccepted(importer, importee) ||
            // importer is a self accepting module
            isHmrAccepted(importer, importer)) {
            // vue boundaries are considered dirty for the reload
            if (importer.endsWith('.vue')) {
                dirtyFiles.add(importer);
            }
            hmrBoundaries.add(importer);
            currentChain.forEach((file) => dirtyFiles.add(file));
        }
        else {
            const parentImpoters = exports.importerMap.get(importer);
            if (!parentImpoters) {
                return true;
            }
            else if (!currentChain.includes(importer)) {
                if (walkImportChain(importer, parentImpoters, hmrBoundaries, dirtyFiles, currentChain.concat(importer))) {
                    return true;
                }
            }
        }
    }
    return false;
}
function isHmrAccepted(importer, dep) {
    const deps = exports.hmrAcceptanceMap.get(importer);
    return deps ? deps.has(dep) : false;
}
function ensureMapEntry(map, key) {
    let entry = map.get(key);
    if (!entry) {
        entry = new Set();
        map.set(key, entry);
    }
    return entry;
}
exports.ensureMapEntry = ensureMapEntry;
function rewriteFileWithHMR(root, source, importer, resolver, s) {
    let hasDeclined = false;
    const registerDep = (e) => {
        const deps = ensureMapEntry(exports.hmrAcceptanceMap, importer);
        const depPublicPath = serverPluginModuleRewrite_1.resolveImport(root, importer, e.value, resolver);
        deps.add(depPublicPath);
        exports.debugHmr(`        ${importer} accepts ${depPublicPath}`);
        ensureMapEntry(exports.importerMap, depPublicPath).add(importer);
        s.overwrite(e.start, e.end, JSON.stringify(depPublicPath));
    };
    const checkHotCall = (node, isTopLevel, isDevBlock) => {
        if (node.type === 'CallExpression' &&
            node.callee.type === 'MemberExpression' &&
            isMetaHot(node.callee.object)) {
            if (isTopLevel) {
                const { generateCodeFrame } = utils_1.resolveCompiler(root);
                console.warn(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: import.meta.hot.accept() ` +
                    `should be wrapped in \`if (import.meta.hot) {}\` conditional ` +
                    `blocks so that they can be tree-shaken in production.`));
                console.warn(chalk_1.default.yellow(generateCodeFrame(source, node.start, node.end)));
            }
            const method = node.callee.property.type === 'Identifier' && node.callee.property.name;
            if (method === 'accept' || method === 'acceptDeps') {
                if (!isDevBlock) {
                    console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: import.meta.hot.${method}() ` +
                        `cannot be conditional except for \`if (import.meta.hot)\` check ` +
                        `because the server relies on static analysis to construct the HMR graph.`));
                }
                // register the accepted deps
                const accepted = node.arguments[0];
                if (accepted && accepted.type === 'ArrayExpression') {
                    if (method !== 'acceptDeps') {
                        console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.accept() only accepts ` +
                            `a single callback. Use hot.acceptDeps() to handle dep updates.`));
                    }
                    // import.meta.hot.accept(['./foo', './bar'], () => {})
                    accepted.elements.forEach((e) => {
                        if (e && e.type !== 'StringLiteral') {
                            console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.accept() deps ` +
                                `list can only contain string literals.`));
                        }
                        else if (e) {
                            registerDep(e);
                        }
                    });
                }
                else if (accepted && accepted.type === 'StringLiteral') {
                    if (method !== 'acceptDeps') {
                        console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.accept() only accepts ` +
                            `a single callback. Use hot.acceptDeps() to handle dep updates.`));
                    }
                    // import.meta.hot.accept('./foo', () => {})
                    registerDep(accepted);
                }
                else if (!accepted || accepted.type.endsWith('FunctionExpression')) {
                    if (method !== 'accept') {
                        console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.acceptDeps() ` +
                            `expects a dependency or an array of dependencies. ` +
                            `Use hot.accept() for handling self updates.`));
                    }
                    // self accepting
                    // import.meta.hot.accept() OR import.meta.hot.accept(() => {})
                    ensureMapEntry(exports.hmrAcceptanceMap, importer).add(importer);
                    exports.debugHmr(`${importer} self accepts`);
                }
                else {
                    console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: ` +
                        `import.meta.hot.accept() expects a dep string, an array of ` +
                        `deps, or a callback.`));
                }
            }
            if (method === 'decline') {
                hasDeclined = true;
                exports.hmrDeclineSet.add(importer);
            }
        }
    };
    const checkStatements = (node, isTopLevel, isDevBlock) => {
        if (node.type === 'ExpressionStatement') {
            // top level hot.accept() call
            checkHotCall(node.expression, isTopLevel, isDevBlock);
        }
        // if (import.meta.hot) ...
        if (node.type === 'IfStatement') {
            const isDevBlock = isMetaHot(node.test);
            if (node.consequent.type === 'BlockStatement') {
                node.consequent.body.forEach((s) => checkStatements(s, false, isDevBlock));
            }
            if (node.consequent.type === 'ExpressionStatement') {
                checkHotCall(node.consequent.expression, false, isDevBlock);
            }
        }
    };
    const ast = babelParse_1.parse(source);
    ast.forEach((s) => checkStatements(s, true, false));
    // inject import.meta.hot
    s.prepend(`import { createHotContext } from "${serverPluginClient_1.clientPublicPath}"; ` +
        `import.meta.hot = createHotContext(${JSON.stringify(importer)}); `);
    // clear decline state
    if (!hasDeclined) {
        exports.hmrDeclineSet.delete(importer);
    }
}
exports.rewriteFileWithHMR = rewriteFileWithHMR;
function isMetaHot(node) {
    return (node.type === 'MemberExpression' &&
        node.object.type === 'MetaProperty' &&
        node.property.type === 'Identifier' &&
        node.property.name === 'hot');
}
//# sourceMappingURL=serverPluginHmr.js.map