"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.lexAcceptedHmrDeps = exports.handlePrunedModules = exports.handleHMRUpdate = exports.debugHmr = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const __1 = require("..");
const utils_1 = require("../utils");
const chalk_1 = __importDefault(require("chalk"));
const slash_1 = __importDefault(require("slash"));
const constants_1 = require("../constants");
exports.debugHmr = utils_1.createDebugger('vite:hmr');
const normalizedClientDir = utils_1.normalizePath(constants_1.CLIENT_DIR);
async function handleHMRUpdate(file, server) {
    const { ws, config, moduleGraph } = server;
    const shortFile = file.startsWith(config.root + '/')
        ? path_1.default.posix.relative(config.root, file)
        : file;
    if (file === config.configPath || file.endsWith('.env')) {
        // TODO auto restart server
        exports.debugHmr(`[config change] ${chalk_1.default.dim(shortFile)}`);
        config.logger.info(chalk_1.default.green('config or .env file changed, restarting server...'), { clear: true, timestamp: true });
        await server.close();
        global.__vite_start_time = Date.now();
        server = await __1.createServer(config.inlineConfig, config.configPath);
        await server.listen();
        return;
    }
    exports.debugHmr(`[file change] ${chalk_1.default.dim(shortFile)}`);
    // (dev only) the client itself cannot be hot updated.
    if (file.startsWith(normalizedClientDir)) {
        ws.send({
            type: 'full-reload',
            path: '/' + slash_1.default(path_1.default.relative(config.root, file))
        });
        return;
    }
    const mods = moduleGraph.getModulesByFile(file);
    // check if any plugin wants to perform custom HMR handling
    const timestamp = Date.now();
    const hmrContext = {
        file,
        timestamp,
        modules: mods ? [...mods] : [],
        read: () => readModifiedFile(file),
        server
    };
    for (const plugin of config.plugins) {
        if (plugin.handleHotUpdate) {
            const filteredModules = await plugin.handleHotUpdate(hmrContext);
            if (filteredModules) {
                hmrContext.modules = filteredModules;
            }
        }
    }
    if (!hmrContext.modules.length) {
        // html file cannot be hot updated
        if (file.endsWith('.html')) {
            config.logger.info(chalk_1.default.green(`page reload `) + chalk_1.default.dim(shortFile), {
                clear: true,
                timestamp: true
            });
            ws.send({
                type: 'full-reload',
                path: '/' + slash_1.default(path_1.default.relative(config.root, file))
            });
        }
        else {
            // loaded but not in the module graph, probably not js
            exports.debugHmr(`[no modules matched] ${chalk_1.default.dim(shortFile)}`);
        }
        return;
    }
    const updates = [];
    for (const mod of hmrContext.modules) {
        const boundaries = new Set();
        const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries);
        if (hasDeadEnd) {
            config.logger.info(chalk_1.default.green(`page reload `) + chalk_1.default.dim(shortFile), {
                clear: true,
                timestamp: true
            });
            ws.send({
                type: 'full-reload'
            });
            return;
        }
        updates.push(...[...boundaries].map(({ boundary, acceptedVia }) => ({
            type: `${boundary.type}-update`,
            timestamp,
            path: boundary.url,
            acceptedPath: acceptedVia.url
        })));
    }
    config.logger.info(updates
        .map(({ path }) => chalk_1.default.green(`hmr update `) + chalk_1.default.dim(path))
        .join('\n'), { clear: true, timestamp: true });
    ws.send({
        type: 'update',
        updates
    });
}
exports.handleHMRUpdate = handleHMRUpdate;
function propagateUpdate(node, timestamp, boundaries, currentChain = [node]) {
    if (node.isSelfAccepting) {
        boundaries.add({
            boundary: node,
            acceptedVia: node
        });
        // mark current propagation chain dirty.
        // timestamp is used for injecting timestamp query during rewrite
        // also invalidate cache
        invalidateChain(currentChain, timestamp);
        return false;
    }
    if (!node.importers.size) {
        return true;
    }
    for (const importer of node.importers) {
        const subChain = currentChain.concat(importer);
        if (importer.acceptedHmrDeps.has(node)) {
            boundaries.add({
                boundary: importer,
                acceptedVia: node
            });
            invalidateChain(subChain, timestamp);
            continue;
        }
        if (!currentChain.includes(importer)) {
            if (propagateUpdate(importer, timestamp, boundaries, subChain)) {
                return true;
            }
        }
    }
    return false;
}
function invalidateChain(chain, timestamp) {
    chain.forEach((node) => {
        node.lastHMRTimestamp = timestamp;
        node.transformResult = null;
    });
}
function handlePrunedModules(mods, { ws }) {
    // update the disposed modules' hmr timestamp
    // since if it's re-imported, it should re-apply side effects
    // and without the timestamp the browser will not re-import it!
    const t = Date.now();
    mods.forEach((mod) => {
        mod.lastHMRTimestamp = t;
        exports.debugHmr(`[dispose] ${chalk_1.default.dim(mod.file)}`);
    });
    ws.send({
        type: 'prune',
        paths: [...mods].map((m) => m.url)
    });
}
exports.handlePrunedModules = handlePrunedModules;
/**
 * Lex import.meta.hot.accept() for accepted deps.
 * Since hot.accept() can only accept string literals or array of string
 * literals, we don't really need a heavy @babel/parse call on the entire source.
 *
 * @returns selfAccepts
 */
function lexAcceptedHmrDeps(code, start, urls) {
    let state = 0 /* inCall */;
    // the state can only be 2 levels deep so no need for a stack
    let prevState = 0 /* inCall */;
    let currentDep = '';
    function addDep(index) {
        urls.add({
            url: currentDep,
            start: index - currentDep.length - 1,
            end: index + 1
        });
        currentDep = '';
    }
    for (let i = start; i < code.length; i++) {
        const char = code.charAt(i);
        switch (state) {
            case 0 /* inCall */:
            case 4 /* inArray */:
                if (char === `'`) {
                    prevState = state;
                    state = 1 /* inSingleQuoteString */;
                }
                else if (char === `"`) {
                    prevState = state;
                    state = 2 /* inDoubleQuoteString */;
                }
                else if (char === '`') {
                    prevState = state;
                    state = 3 /* inTemplateString */;
                }
                else if (/\s/.test(char)) {
                    continue;
                }
                else {
                    if (state === 0 /* inCall */) {
                        if (char === `[`) {
                            state = 4 /* inArray */;
                        }
                        else {
                            // reaching here means the first arg is neither a string literal
                            // nor an Array literal (direct callback) or there is no arg
                            // in both case this indicates a self-accepting module
                            return true; // done
                        }
                    }
                    else if (state === 4 /* inArray */) {
                        if (char === `]`) {
                            return false; // done
                        }
                        else if (char === ',') {
                            continue;
                        }
                        else {
                            error(i);
                        }
                    }
                }
                break;
            case 1 /* inSingleQuoteString */:
                if (char === `'`) {
                    addDep(i);
                    if (prevState === 0 /* inCall */) {
                        // accept('foo', ...)
                        return false;
                    }
                    else {
                        state = prevState;
                    }
                }
                else {
                    currentDep += char;
                }
                break;
            case 2 /* inDoubleQuoteString */:
                if (char === `"`) {
                    addDep(i);
                    if (prevState === 0 /* inCall */) {
                        // accept('foo', ...)
                        return false;
                    }
                    else {
                        state = prevState;
                    }
                }
                else {
                    currentDep += char;
                }
                break;
            case 3 /* inTemplateString */:
                if (char === '`') {
                    addDep(i);
                    if (prevState === 0 /* inCall */) {
                        // accept('foo', ...)
                        return false;
                    }
                    else {
                        state = prevState;
                    }
                }
                else if (char === '$' && code.charAt(i + 1) === '{') {
                    error(i);
                }
                else {
                    currentDep += char;
                }
                break;
            default:
                throw new Error('unknown lexer state');
        }
    }
    return false;
}
exports.lexAcceptedHmrDeps = lexAcceptedHmrDeps;
function error(pos) {
    const err = new Error(`import.meta.accept() can only accept string literals or an ` +
        `Array of string literals.`);
    err.pos = pos;
    throw err;
}
// vitejs/vite#610 when hot-reloading Vue files, we read immediately on file
// change event and sometimes this can be too early and get an empty buffer.
// Poll until the file's modified time has changed before reading again.
async function readModifiedFile(file) {
    const content = fs_1.default.readFileSync(file, 'utf-8');
    if (!content) {
        const mtime = fs_1.default.statSync(file).mtimeMs;
        await new Promise((r) => {
            let n = 0;
            const poll = async () => {
                n++;
                const newMtime = fs_1.default.statSync(file).mtimeMs;
                if (newMtime !== mtime || n > 10) {
                    r(0);
                }
                else {
                    setTimeout(poll, 10);
                }
            };
            setTimeout(poll, 10);
        });
        return fs_1.default.readFileSync(file, 'utf-8');
    }
    else {
        return content;
    }
}
//# sourceMappingURL=hmr.js.map