2021-03-01 15:06:11 +08:00

296 lines
14 KiB
JavaScript

"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