| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- "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
|