serverPluginHmr.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. "use strict";
  2. // How HMR works
  3. // 1. `.vue` files are transformed into `.js` files before being served
  4. // 2. All `.js` files, before being served, are parsed to detect their imports
  5. // (this is done in `./serverPluginModuleRewrite.ts`) for module import rewriting.
  6. // During this we also record the importer/importee relationships which can be used for
  7. // HMR analysis (we do both at the same time to avoid double parse costs)
  8. // 3. When a file changes, it triggers an HMR graph analysis, where we try to
  9. // walk its importer chains and see if we reach a "HMR boundary". An HMR
  10. // boundary is a file that explicitly indicated that it accepts hot updates
  11. // (by calling `import.meta.hot` APIs)
  12. // 4. If any parent chain exhausts without ever running into an HMR boundary,
  13. // it's considered a "dead end". This causes a full page reload.
  14. // 5. If a boundary is encountered, we check if the boundary's current
  15. // child importer is in the accepted list of the boundary (recorded while
  16. // parsing the file for HRM rewrite). If yes, record current child importer
  17. // in the `hmrBoundaries` Set.
  18. // 6. If the graph walk finished without running into dead ends, send the
  19. // client to update all `hmrBoundaries`.
  20. var __importDefault = (this && this.__importDefault) || function (mod) {
  21. return (mod && mod.__esModule) ? mod : { "default": mod };
  22. };
  23. Object.defineProperty(exports, "__esModule", { value: true });
  24. exports.rewriteFileWithHMR = exports.ensureMapEntry = exports.hmrPlugin = exports.latestVersionsMap = exports.hmrDirtyFilesMap = exports.importeeMap = exports.importerMap = exports.hmrDeclineSet = exports.hmrAcceptanceMap = exports.debugHmr = void 0;
  25. const ws_1 = __importDefault(require("ws"));
  26. const path_1 = __importDefault(require("path"));
  27. const chalk_1 = __importDefault(require("chalk"));
  28. const serverPluginVue_1 = require("./serverPluginVue");
  29. const serverPluginModuleRewrite_1 = require("./serverPluginModuleRewrite");
  30. const babelParse_1 = require("../utils/babelParse");
  31. const lru_cache_1 = __importDefault(require("lru-cache"));
  32. const slash_1 = __importDefault(require("slash"));
  33. const cssUtils_1 = require("../utils/cssUtils");
  34. const utils_1 = require("../utils");
  35. const serverPluginClient_1 = require("./serverPluginClient");
  36. exports.debugHmr = require('debug')('vite:hmr');
  37. exports.hmrAcceptanceMap = new Map();
  38. exports.hmrDeclineSet = new Set();
  39. exports.importerMap = new Map();
  40. exports.importeeMap = new Map();
  41. // files that are dirty (i.e. in the import chain between the accept boundary
  42. // and the actual changed file) for an hmr update at a given timestamp.
  43. exports.hmrDirtyFilesMap = new lru_cache_1.default({ max: 10 });
  44. exports.latestVersionsMap = new Map();
  45. exports.hmrPlugin = ({ root, app, server, watcher, resolver, config }) => {
  46. app.use((ctx, next) => {
  47. if (ctx.query.t) {
  48. exports.latestVersionsMap.set(ctx.path, ctx.query.t);
  49. }
  50. return next();
  51. });
  52. // start a websocket server to send hmr notifications to the client
  53. const wss = new ws_1.default.Server({ noServer: true });
  54. server.on('upgrade', (req, socket, head) => {
  55. if (req.headers['sec-websocket-protocol'] === 'vite-hmr') {
  56. wss.handleUpgrade(req, socket, head, (ws) => {
  57. wss.emit('connection', ws, req);
  58. });
  59. }
  60. });
  61. wss.on('connection', (socket) => {
  62. exports.debugHmr('ws client connected');
  63. socket.send(JSON.stringify({ type: 'connected' }));
  64. });
  65. wss.on('error', (e) => {
  66. if (e.code !== 'EADDRINUSE') {
  67. console.error(chalk_1.default.red(`[vite] WebSocket server error:`));
  68. console.error(e);
  69. }
  70. });
  71. const send = (watcher.send = (payload) => {
  72. const stringified = JSON.stringify(payload, null, 2);
  73. exports.debugHmr(`update: ${stringified}`);
  74. wss.clients.forEach((client) => {
  75. if (client.readyState === ws_1.default.OPEN) {
  76. client.send(stringified);
  77. }
  78. });
  79. });
  80. const handleJSReload = (watcher.handleJSReload = (filePath, timestamp = Date.now()) => {
  81. // normal js file, but could be compiled from anything.
  82. // bust the vue cache in case this is a src imported file
  83. if (serverPluginVue_1.srcImportMap.has(filePath)) {
  84. exports.debugHmr(`busting Vue cache for ${filePath}`);
  85. serverPluginVue_1.vueCache.del(filePath);
  86. }
  87. const publicPath = resolver.fileToRequest(filePath);
  88. const importers = exports.importerMap.get(publicPath);
  89. if (importers || isHmrAccepted(publicPath, publicPath)) {
  90. const hmrBoundaries = new Set();
  91. const dirtyFiles = new Set();
  92. dirtyFiles.add(publicPath);
  93. const hasDeadEnd = walkImportChain(publicPath, importers || new Set(), hmrBoundaries, dirtyFiles);
  94. // record dirty files - this is used when HMR requests coming in with
  95. // timestamp to determine what files need to be force re-fetched
  96. exports.hmrDirtyFilesMap.set(String(timestamp), dirtyFiles);
  97. const relativeFile = '/' + slash_1.default(path_1.default.relative(root, filePath));
  98. if (hasDeadEnd) {
  99. send({
  100. type: 'full-reload',
  101. path: publicPath
  102. });
  103. console.log(chalk_1.default.green(`[vite] `) + `page reloaded.`);
  104. }
  105. else {
  106. const boundaries = [...hmrBoundaries];
  107. const file = boundaries.length === 1 ? boundaries[0] : `${boundaries.length} files`;
  108. console.log(chalk_1.default.green(`[vite:hmr] `) +
  109. `${file} hot updated due to change in ${relativeFile}.`);
  110. send({
  111. type: 'multi',
  112. updates: boundaries.map((boundary) => {
  113. return {
  114. type: boundary.endsWith('vue') ? 'vue-reload' : 'js-update',
  115. path: boundary,
  116. changeSrcPath: publicPath,
  117. timestamp
  118. };
  119. })
  120. });
  121. }
  122. }
  123. else {
  124. exports.debugHmr(`no importers for ${publicPath}.`);
  125. }
  126. });
  127. watcher.on('change', (file) => {
  128. if (!(file.endsWith('.vue') || cssUtils_1.isCSSRequest(file))) {
  129. // everything except plain .css are considered HMR dependencies.
  130. // plain css has its own HMR logic in ./serverPluginCss.ts.
  131. handleJSReload(file);
  132. }
  133. });
  134. };
  135. function walkImportChain(importee, importers, hmrBoundaries, dirtyFiles, currentChain = []) {
  136. if (exports.hmrDeclineSet.has(importee)) {
  137. // module explicitly declines HMR = dead end
  138. return true;
  139. }
  140. if (isHmrAccepted(importee, importee)) {
  141. // self-accepting module.
  142. hmrBoundaries.add(importee);
  143. dirtyFiles.add(importee);
  144. return false;
  145. }
  146. for (const importer of importers) {
  147. if (importer.endsWith('.vue') ||
  148. // explicitly accepted by this importer
  149. isHmrAccepted(importer, importee) ||
  150. // importer is a self accepting module
  151. isHmrAccepted(importer, importer)) {
  152. // vue boundaries are considered dirty for the reload
  153. if (importer.endsWith('.vue')) {
  154. dirtyFiles.add(importer);
  155. }
  156. hmrBoundaries.add(importer);
  157. currentChain.forEach((file) => dirtyFiles.add(file));
  158. }
  159. else {
  160. const parentImpoters = exports.importerMap.get(importer);
  161. if (!parentImpoters) {
  162. return true;
  163. }
  164. else if (!currentChain.includes(importer)) {
  165. if (walkImportChain(importer, parentImpoters, hmrBoundaries, dirtyFiles, currentChain.concat(importer))) {
  166. return true;
  167. }
  168. }
  169. }
  170. }
  171. return false;
  172. }
  173. function isHmrAccepted(importer, dep) {
  174. const deps = exports.hmrAcceptanceMap.get(importer);
  175. return deps ? deps.has(dep) : false;
  176. }
  177. function ensureMapEntry(map, key) {
  178. let entry = map.get(key);
  179. if (!entry) {
  180. entry = new Set();
  181. map.set(key, entry);
  182. }
  183. return entry;
  184. }
  185. exports.ensureMapEntry = ensureMapEntry;
  186. function rewriteFileWithHMR(root, source, importer, resolver, s) {
  187. let hasDeclined = false;
  188. const registerDep = (e) => {
  189. const deps = ensureMapEntry(exports.hmrAcceptanceMap, importer);
  190. const depPublicPath = serverPluginModuleRewrite_1.resolveImport(root, importer, e.value, resolver);
  191. deps.add(depPublicPath);
  192. exports.debugHmr(` ${importer} accepts ${depPublicPath}`);
  193. ensureMapEntry(exports.importerMap, depPublicPath).add(importer);
  194. s.overwrite(e.start, e.end, JSON.stringify(depPublicPath));
  195. };
  196. const checkHotCall = (node, isTopLevel, isDevBlock) => {
  197. if (node.type === 'CallExpression' &&
  198. node.callee.type === 'MemberExpression' &&
  199. isMetaHot(node.callee.object)) {
  200. if (isTopLevel) {
  201. const { generateCodeFrame } = utils_1.resolveCompiler(root);
  202. console.warn(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: import.meta.hot.accept() ` +
  203. `should be wrapped in \`if (import.meta.hot) {}\` conditional ` +
  204. `blocks so that they can be tree-shaken in production.`));
  205. console.warn(chalk_1.default.yellow(generateCodeFrame(source, node.start, node.end)));
  206. }
  207. const method = node.callee.property.type === 'Identifier' && node.callee.property.name;
  208. if (method === 'accept' || method === 'acceptDeps') {
  209. if (!isDevBlock) {
  210. console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: import.meta.hot.${method}() ` +
  211. `cannot be conditional except for \`if (import.meta.hot)\` check ` +
  212. `because the server relies on static analysis to construct the HMR graph.`));
  213. }
  214. // register the accepted deps
  215. const accepted = node.arguments[0];
  216. if (accepted && accepted.type === 'ArrayExpression') {
  217. if (method !== 'acceptDeps') {
  218. console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.accept() only accepts ` +
  219. `a single callback. Use hot.acceptDeps() to handle dep updates.`));
  220. }
  221. // import.meta.hot.accept(['./foo', './bar'], () => {})
  222. accepted.elements.forEach((e) => {
  223. if (e && e.type !== 'StringLiteral') {
  224. console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.accept() deps ` +
  225. `list can only contain string literals.`));
  226. }
  227. else if (e) {
  228. registerDep(e);
  229. }
  230. });
  231. }
  232. else if (accepted && accepted.type === 'StringLiteral') {
  233. if (method !== 'acceptDeps') {
  234. console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.accept() only accepts ` +
  235. `a single callback. Use hot.acceptDeps() to handle dep updates.`));
  236. }
  237. // import.meta.hot.accept('./foo', () => {})
  238. registerDep(accepted);
  239. }
  240. else if (!accepted || accepted.type.endsWith('FunctionExpression')) {
  241. if (method !== 'accept') {
  242. console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: hot.acceptDeps() ` +
  243. `expects a dependency or an array of dependencies. ` +
  244. `Use hot.accept() for handling self updates.`));
  245. }
  246. // self accepting
  247. // import.meta.hot.accept() OR import.meta.hot.accept(() => {})
  248. ensureMapEntry(exports.hmrAcceptanceMap, importer).add(importer);
  249. exports.debugHmr(`${importer} self accepts`);
  250. }
  251. else {
  252. console.error(chalk_1.default.yellow(`[vite] HMR syntax error in ${importer}: ` +
  253. `import.meta.hot.accept() expects a dep string, an array of ` +
  254. `deps, or a callback.`));
  255. }
  256. }
  257. if (method === 'decline') {
  258. hasDeclined = true;
  259. exports.hmrDeclineSet.add(importer);
  260. }
  261. }
  262. };
  263. const checkStatements = (node, isTopLevel, isDevBlock) => {
  264. if (node.type === 'ExpressionStatement') {
  265. // top level hot.accept() call
  266. checkHotCall(node.expression, isTopLevel, isDevBlock);
  267. }
  268. // if (import.meta.hot) ...
  269. if (node.type === 'IfStatement') {
  270. const isDevBlock = isMetaHot(node.test);
  271. if (node.consequent.type === 'BlockStatement') {
  272. node.consequent.body.forEach((s) => checkStatements(s, false, isDevBlock));
  273. }
  274. if (node.consequent.type === 'ExpressionStatement') {
  275. checkHotCall(node.consequent.expression, false, isDevBlock);
  276. }
  277. }
  278. };
  279. const ast = babelParse_1.parse(source);
  280. ast.forEach((s) => checkStatements(s, true, false));
  281. // inject import.meta.hot
  282. s.prepend(`import { createHotContext } from "${serverPluginClient_1.clientPublicPath}"; ` +
  283. `import.meta.hot = createHotContext(${JSON.stringify(importer)}); `);
  284. // clear decline state
  285. if (!hasDeclined) {
  286. exports.hmrDeclineSet.delete(importer);
  287. }
  288. }
  289. exports.rewriteFileWithHMR = rewriteFileWithHMR;
  290. function isMetaHot(node) {
  291. return (node.type === 'MemberExpression' &&
  292. node.object.type === 'MetaProperty' &&
  293. node.property.type === 'Identifier' &&
  294. node.property.name === 'hot');
  295. }
  296. //# sourceMappingURL=serverPluginHmr.js.map