serverPluginVue.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. "use strict";
  2. var __importDefault = (this && this.__importDefault) || function (mod) {
  3. return (mod && mod.__esModule) ? mod : { "default": mod };
  4. };
  5. Object.defineProperty(exports, "__esModule", { value: true });
  6. exports.vuePlugin = exports.vueCache = exports.srcImportMap = void 0;
  7. const querystring_1 = __importDefault(require("querystring"));
  8. const chalk_1 = __importDefault(require("chalk"));
  9. const path_1 = __importDefault(require("path"));
  10. const compiler_sfc_1 = require("@vue/compiler-sfc");
  11. const resolveVue_1 = require("../utils/resolveVue");
  12. const hash_sum_1 = __importDefault(require("hash-sum"));
  13. const lru_cache_1 = __importDefault(require("lru-cache"));
  14. const serverPluginHmr_1 = require("./serverPluginHmr");
  15. const utils_1 = require("../utils");
  16. const esbuildService_1 = require("../esbuildService");
  17. const resolver_1 = require("../resolver");
  18. const serverPluginServeStatic_1 = require("./serverPluginServeStatic");
  19. const serverPluginCss_1 = require("./serverPluginCss");
  20. const cssUtils_1 = require("../utils/cssUtils");
  21. const serverPluginModuleRewrite_1 = require("./serverPluginModuleRewrite");
  22. const serverPluginSourceMap_1 = require("./serverPluginSourceMap");
  23. const debug = require('debug')('vite:sfc');
  24. const getEtag = require('etag');
  25. exports.srcImportMap = new Map();
  26. exports.vueCache = new lru_cache_1.default({
  27. max: 65535
  28. });
  29. exports.vuePlugin = ({ root, app, resolver, watcher, config }) => {
  30. const etagCacheCheck = (ctx) => {
  31. ctx.etag = getEtag(ctx.body);
  32. ctx.status =
  33. serverPluginServeStatic_1.seenUrls.has(ctx.url) && ctx.etag === ctx.get('If-None-Match') ? 304 : 200;
  34. serverPluginServeStatic_1.seenUrls.add(ctx.url);
  35. };
  36. app.use(async (ctx, next) => {
  37. // ctx.vue is set by other tools like vitepress so that vite knows to treat
  38. // non .vue files as vue files.
  39. if (!ctx.path.endsWith('.vue') && !ctx.vue) {
  40. return next();
  41. }
  42. const query = ctx.query;
  43. const publicPath = ctx.path;
  44. let filePath = resolver.requestToFile(publicPath);
  45. // upstream plugins could've already read the file
  46. const descriptor = await parseSFC(root, filePath, ctx.body);
  47. if (!descriptor) {
  48. return next();
  49. }
  50. if (!query.type) {
  51. // watch potentially out of root vue file since we do a custom read here
  52. utils_1.watchFileIfOutOfRoot(watcher, root, filePath);
  53. if (descriptor.script && descriptor.script.src) {
  54. filePath = await resolveSrcImport(root, descriptor.script, ctx, resolver);
  55. }
  56. ctx.type = 'js';
  57. const { code, map } = await compileSFCMain(descriptor, filePath, publicPath, root);
  58. ctx.body = code;
  59. ctx.map = map;
  60. return etagCacheCheck(ctx);
  61. }
  62. if (query.type === 'template') {
  63. const templateBlock = descriptor.template;
  64. if (templateBlock.src) {
  65. filePath = await resolveSrcImport(root, templateBlock, ctx, resolver);
  66. }
  67. ctx.type = 'js';
  68. const cached = exports.vueCache.get(filePath);
  69. const bindingMetadata = cached && cached.script && cached.script.bindings;
  70. const vueSpecifier = resolver_1.resolveBareModuleRequest(root, 'vue', publicPath, resolver);
  71. const { code, map } = compileSFCTemplate(root, templateBlock, filePath, publicPath, descriptor.styles.some((s) => s.scoped), bindingMetadata, vueSpecifier, config);
  72. ctx.body = code;
  73. ctx.map = map;
  74. return etagCacheCheck(ctx);
  75. }
  76. if (query.type === 'style') {
  77. const index = Number(query.index);
  78. const styleBlock = descriptor.styles[index];
  79. if (styleBlock.src) {
  80. filePath = await resolveSrcImport(root, styleBlock, ctx, resolver);
  81. }
  82. const id = hash_sum_1.default(publicPath);
  83. const result = await compileSFCStyle(root, styleBlock, index, filePath, publicPath, config);
  84. ctx.type = 'js';
  85. ctx.body = serverPluginCss_1.codegenCss(`${id}-${index}`, result.code, result.modules);
  86. return etagCacheCheck(ctx);
  87. }
  88. if (query.type === 'custom') {
  89. const index = Number(query.index);
  90. const customBlock = descriptor.customBlocks[index];
  91. if (customBlock.src) {
  92. filePath = await resolveSrcImport(root, customBlock, ctx, resolver);
  93. }
  94. const result = resolveCustomBlock(customBlock, index, filePath, publicPath);
  95. ctx.type = 'js';
  96. ctx.body = result;
  97. return etagCacheCheck(ctx);
  98. }
  99. });
  100. const handleVueReload = (watcher.handleVueReload = async (filePath, timestamp = Date.now(), content) => {
  101. const publicPath = resolver.fileToRequest(filePath);
  102. const cacheEntry = exports.vueCache.get(filePath);
  103. const { send } = watcher;
  104. serverPluginHmr_1.debugHmr(`busting Vue cache for ${filePath}`);
  105. exports.vueCache.del(filePath);
  106. const descriptor = await parseSFC(root, filePath, content);
  107. if (!descriptor) {
  108. // read failed
  109. return;
  110. }
  111. const prevDescriptor = cacheEntry && cacheEntry.descriptor;
  112. if (!prevDescriptor) {
  113. // the file has never been accessed yet
  114. serverPluginHmr_1.debugHmr(`no existing descriptor found for ${filePath}`);
  115. return;
  116. }
  117. // check which part of the file changed
  118. let needRerender = false;
  119. const sendReload = () => {
  120. send({
  121. type: 'vue-reload',
  122. path: publicPath,
  123. changeSrcPath: publicPath,
  124. timestamp
  125. });
  126. console.log(chalk_1.default.green(`[vite:hmr] `) +
  127. `${path_1.default.relative(root, filePath)} updated. (reload)`);
  128. };
  129. if (!isEqualBlock(descriptor.script, prevDescriptor.script) ||
  130. !isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup)) {
  131. return sendReload();
  132. }
  133. if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
  134. // #748 should re-use previous cached script if only template change
  135. // so that the template is compiled with the correct binding metadata
  136. if (prevDescriptor.scriptSetup && descriptor.scriptSetup) {
  137. exports.vueCache.get(filePath).script = cacheEntry.script;
  138. }
  139. needRerender = true;
  140. }
  141. let didUpdateStyle = false;
  142. const styleId = hash_sum_1.default(publicPath);
  143. const prevStyles = prevDescriptor.styles || [];
  144. const nextStyles = descriptor.styles || [];
  145. // css modules update causes a reload because the $style object is changed
  146. // and it may be used in JS. It also needs to trigger a vue-style-update
  147. // event so the client busts the sw cache.
  148. if (prevStyles.some((s) => s.module != null) ||
  149. nextStyles.some((s) => s.module != null)) {
  150. return sendReload();
  151. }
  152. // force reload if CSS vars injection changed
  153. if (prevStyles.some((s, i) => {
  154. const next = nextStyles[i];
  155. if (s.attrs.vars && (!next || next.attrs.vars !== s.attrs.vars)) {
  156. return true;
  157. }
  158. })) {
  159. return sendReload();
  160. }
  161. // force reload if scoped status has changed
  162. if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
  163. return sendReload();
  164. }
  165. // only need to update styles if not reloading, since reload forces
  166. // style updates as well.
  167. nextStyles.forEach((_, i) => {
  168. if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {
  169. didUpdateStyle = true;
  170. const path = `${publicPath}?type=style&index=${i}`;
  171. send({
  172. type: 'style-update',
  173. path,
  174. changeSrcPath: path,
  175. timestamp
  176. });
  177. }
  178. });
  179. // stale styles always need to be removed
  180. prevStyles.slice(nextStyles.length).forEach((_, i) => {
  181. didUpdateStyle = true;
  182. send({
  183. type: 'style-remove',
  184. path: publicPath,
  185. id: `${styleId}-${i + nextStyles.length}`
  186. });
  187. });
  188. const prevCustoms = prevDescriptor.customBlocks || [];
  189. const nextCustoms = descriptor.customBlocks || [];
  190. // custom blocks update causes a reload
  191. // because the custom block contents is changed and it may be used in JS.
  192. if (nextCustoms.some((_, i) => !prevCustoms[i] || !isEqualBlock(prevCustoms[i], nextCustoms[i]))) {
  193. return sendReload();
  194. }
  195. if (needRerender) {
  196. send({
  197. type: 'vue-rerender',
  198. path: publicPath,
  199. changeSrcPath: publicPath,
  200. timestamp
  201. });
  202. }
  203. let updateType = [];
  204. if (needRerender) {
  205. updateType.push(`template`);
  206. }
  207. if (didUpdateStyle) {
  208. updateType.push(`style`);
  209. }
  210. if (updateType.length) {
  211. console.log(chalk_1.default.green(`[vite:hmr] `) +
  212. `${path_1.default.relative(root, filePath)} updated. (${updateType.join(' & ')})`);
  213. }
  214. });
  215. watcher.on('change', (file) => {
  216. if (file.endsWith('.vue')) {
  217. handleVueReload(file);
  218. }
  219. });
  220. };
  221. function isEqualBlock(a, b) {
  222. if (!a && !b)
  223. return true;
  224. if (!a || !b)
  225. return false;
  226. // src imports will trigger their own updates
  227. if (a.src && b.src && a.src === b.src)
  228. return true;
  229. if (a.content !== b.content)
  230. return false;
  231. const keysA = Object.keys(a.attrs);
  232. const keysB = Object.keys(b.attrs);
  233. if (keysA.length !== keysB.length) {
  234. return false;
  235. }
  236. return keysA.every((key) => a.attrs[key] === b.attrs[key]);
  237. }
  238. async function resolveSrcImport(root, block, ctx, resolver) {
  239. const importer = ctx.path;
  240. const importee = utils_1.cleanUrl(serverPluginModuleRewrite_1.resolveImport(root, importer, block.src, resolver));
  241. const filePath = resolver.requestToFile(importee);
  242. block.content = (await ctx.read(filePath)).toString();
  243. // register HMR import relationship
  244. serverPluginHmr_1.debugHmr(` ${importer} imports ${importee}`);
  245. serverPluginHmr_1.ensureMapEntry(serverPluginHmr_1.importerMap, importee).add(ctx.path);
  246. exports.srcImportMap.set(filePath, ctx.url);
  247. return filePath;
  248. }
  249. async function parseSFC(root, filePath, content) {
  250. let cached = exports.vueCache.get(filePath);
  251. if (cached && cached.descriptor) {
  252. debug(`${filePath} parse cache hit`);
  253. return cached.descriptor;
  254. }
  255. if (!content) {
  256. try {
  257. content = await utils_1.cachedRead(null, filePath);
  258. }
  259. catch (e) {
  260. return;
  261. }
  262. }
  263. if (typeof content !== 'string') {
  264. content = content.toString();
  265. }
  266. const start = Date.now();
  267. const { parse } = resolveVue_1.resolveCompiler(root);
  268. const { descriptor, errors } = parse(content, {
  269. filename: filePath,
  270. sourceMap: true
  271. });
  272. if (errors.length) {
  273. console.error(chalk_1.default.red(`\n[vite] SFC parse error: `));
  274. errors.forEach((e) => {
  275. logError(e, filePath, content);
  276. });
  277. }
  278. cached = cached || { styles: [], customs: [] };
  279. cached.descriptor = descriptor;
  280. exports.vueCache.set(filePath, cached);
  281. debug(`${filePath} parsed in ${Date.now() - start}ms.`);
  282. return descriptor;
  283. }
  284. async function compileSFCMain(descriptor, filePath, publicPath, root) {
  285. let cached = exports.vueCache.get(filePath);
  286. if (cached && cached.script) {
  287. return cached.script;
  288. }
  289. const id = hash_sum_1.default(publicPath);
  290. let code = ``;
  291. let content = ``;
  292. let map;
  293. let script = descriptor.script;
  294. const compiler = resolveVue_1.resolveCompiler(root);
  295. if ((descriptor.script || descriptor.scriptSetup) && compiler.compileScript) {
  296. try {
  297. script = compiler.compileScript(descriptor);
  298. }
  299. catch (e) {
  300. console.error(chalk_1.default.red(`\n[vite] SFC <script setup> compilation error:\n${chalk_1.default.dim(chalk_1.default.white(filePath))}`));
  301. console.error(chalk_1.default.yellow(e.message));
  302. }
  303. }
  304. if (script) {
  305. content = script.content;
  306. map = script.map;
  307. if (script.lang === 'ts') {
  308. const res = await esbuildService_1.transform(content, publicPath, {
  309. loader: 'ts'
  310. });
  311. content = res.code;
  312. map = serverPluginSourceMap_1.mergeSourceMap(map, JSON.parse(res.map));
  313. }
  314. }
  315. code += compiler_sfc_1.rewriteDefault(content, '__script');
  316. let hasScoped = false;
  317. let hasCSSModules = false;
  318. if (descriptor.styles) {
  319. descriptor.styles.forEach((s, i) => {
  320. const styleRequest = publicPath + `?type=style&index=${i}`;
  321. if (s.scoped)
  322. hasScoped = true;
  323. if (s.module) {
  324. if (!hasCSSModules) {
  325. code += `\nconst __cssModules = __script.__cssModules = {}`;
  326. hasCSSModules = true;
  327. }
  328. const styleVar = `__style${i}`;
  329. const moduleName = typeof s.module === 'string' ? s.module : '$style';
  330. code += `\nimport ${styleVar} from ${JSON.stringify(styleRequest + '&module')}`;
  331. code += `\n__cssModules[${JSON.stringify(moduleName)}] = ${styleVar}`;
  332. }
  333. else {
  334. code += `\nimport ${JSON.stringify(styleRequest)}`;
  335. }
  336. });
  337. if (hasScoped) {
  338. code += `\n__script.__scopeId = "data-v-${id}"`;
  339. }
  340. }
  341. if (descriptor.customBlocks) {
  342. descriptor.customBlocks.forEach((c, i) => {
  343. const attrsQuery = attrsToQuery(c.attrs, c.lang);
  344. const blockTypeQuery = `&blockType=${querystring_1.default.escape(c.type)}`;
  345. let customRequest = publicPath + `?type=custom&index=${i}${blockTypeQuery}${attrsQuery}`;
  346. const customVar = `block${i}`;
  347. code += `\nimport ${customVar} from ${JSON.stringify(customRequest)}\n`;
  348. code += `if (typeof ${customVar} === 'function') ${customVar}(__script)\n`;
  349. });
  350. }
  351. if (descriptor.template) {
  352. const templateRequest = publicPath + `?type=template`;
  353. code += `\nimport { render as __render } from ${JSON.stringify(templateRequest)}`;
  354. code += `\n__script.render = __render`;
  355. }
  356. code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}`;
  357. code += `\ntypeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)`;
  358. code += `\n__script.__file = ${JSON.stringify(filePath)}`;
  359. code += `\nexport default __script`;
  360. const result = {
  361. code,
  362. map,
  363. bindings: script ? script.bindings : undefined
  364. };
  365. cached = cached || { styles: [], customs: [] };
  366. cached.script = result;
  367. exports.vueCache.set(filePath, cached);
  368. return result;
  369. }
  370. function compileSFCTemplate(root, template, filePath, publicPath, scoped, bindingMetadata, vueSpecifier, { vueCompilerOptions, vueTransformAssetUrls = {}, vueTemplatePreprocessOptions = {} }) {
  371. let cached = exports.vueCache.get(filePath);
  372. if (cached && cached.template) {
  373. debug(`${publicPath} template cache hit`);
  374. return cached.template;
  375. }
  376. const start = Date.now();
  377. const { compileTemplate } = resolveVue_1.resolveCompiler(root);
  378. if (typeof vueTransformAssetUrls === 'object') {
  379. vueTransformAssetUrls = {
  380. base: path_1.default.posix.dirname(publicPath),
  381. ...vueTransformAssetUrls
  382. };
  383. }
  384. const preprocessLang = template.lang;
  385. let preprocessOptions = preprocessLang && vueTemplatePreprocessOptions[preprocessLang];
  386. if (preprocessLang === 'pug') {
  387. preprocessOptions = {
  388. doctype: 'html',
  389. ...preprocessOptions
  390. };
  391. }
  392. const { code, map, errors } = compileTemplate({
  393. source: template.content,
  394. filename: filePath,
  395. inMap: template.map,
  396. transformAssetUrls: vueTransformAssetUrls,
  397. compilerOptions: {
  398. ...vueCompilerOptions,
  399. scopeId: scoped ? `data-v-${hash_sum_1.default(publicPath)}` : null,
  400. bindingMetadata,
  401. runtimeModuleName: vueSpecifier
  402. },
  403. preprocessLang,
  404. preprocessOptions,
  405. preprocessCustomRequire: (id) => require(utils_1.resolveFrom(root, id))
  406. });
  407. if (errors.length) {
  408. console.error(chalk_1.default.red(`\n[vite] SFC template compilation error: `));
  409. errors.forEach((e) => {
  410. if (typeof e === 'string') {
  411. console.error(e);
  412. }
  413. else {
  414. logError(e, filePath, template.map.sourcesContent[0]);
  415. }
  416. });
  417. }
  418. const result = {
  419. code,
  420. map: map
  421. };
  422. cached = cached || { styles: [], customs: [] };
  423. cached.template = result;
  424. exports.vueCache.set(filePath, cached);
  425. debug(`${publicPath} template compiled in ${Date.now() - start}ms.`);
  426. return result;
  427. }
  428. async function compileSFCStyle(root, style, index, filePath, publicPath, { cssPreprocessOptions, cssModuleOptions }) {
  429. let cached = exports.vueCache.get(filePath);
  430. const cachedEntry = cached && cached.styles && cached.styles[index];
  431. if (cachedEntry) {
  432. debug(`${publicPath} style cache hit`);
  433. return cachedEntry;
  434. }
  435. const start = Date.now();
  436. const { generateCodeFrame } = resolveVue_1.resolveCompiler(root);
  437. const resource = filePath + `?type=style&index=${index}`;
  438. const result = (await cssUtils_1.compileCss(root, publicPath, {
  439. source: style.content,
  440. filename: resource,
  441. id: ``,
  442. scoped: style.scoped != null,
  443. vars: style.vars != null,
  444. modules: style.module != null,
  445. preprocessLang: style.lang,
  446. preprocessOptions: cssPreprocessOptions,
  447. modulesOptions: cssModuleOptions
  448. }));
  449. cssUtils_1.recordCssImportChain(result.dependencies, resource);
  450. if (result.errors.length) {
  451. console.error(chalk_1.default.red(`\n[vite] SFC style compilation error: `));
  452. result.errors.forEach((e) => {
  453. if (typeof e === 'string') {
  454. console.error(e);
  455. }
  456. else {
  457. const lineOffset = style.loc.start.line - 1;
  458. if (e.line && e.column) {
  459. console.log(chalk_1.default.underline(`${filePath}:${e.line + lineOffset}:${e.column}`));
  460. }
  461. else {
  462. console.log(chalk_1.default.underline(filePath));
  463. }
  464. const filePathRE = new RegExp('.*' +
  465. path_1.default.basename(filePath).replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') +
  466. '(:\\d+:\\d+:\\s*)?');
  467. const cleanMsg = e.message.replace(filePathRE, '');
  468. console.error(chalk_1.default.yellow(cleanMsg));
  469. if (e.line && e.column && cleanMsg.split(/\n/g).length === 1) {
  470. const original = style.map.sourcesContent[0];
  471. const offset = original
  472. .split(/\r?\n/g)
  473. .slice(0, e.line + lineOffset - 1)
  474. .map((l) => l.length)
  475. .reduce((total, l) => total + l + 1, 0) +
  476. e.column -
  477. 1;
  478. console.error(generateCodeFrame(original, offset, offset + 1)) + `\n`;
  479. }
  480. }
  481. });
  482. }
  483. result.code = await cssUtils_1.rewriteCssUrls(result.code, publicPath);
  484. cached = cached || { styles: [], customs: [] };
  485. cached.styles[index] = result;
  486. exports.vueCache.set(filePath, cached);
  487. debug(`${publicPath} style compiled in ${Date.now() - start}ms`);
  488. return result;
  489. }
  490. function resolveCustomBlock(custom, index, filePath, publicPath) {
  491. let cached = exports.vueCache.get(filePath);
  492. const cachedEntry = cached && cached.customs && cached.customs[index];
  493. if (cachedEntry) {
  494. debug(`${publicPath} custom block cache hit`);
  495. return cachedEntry;
  496. }
  497. const result = custom.content;
  498. cached = cached || { styles: [], customs: [] };
  499. cached.customs[index] = result;
  500. exports.vueCache.set(filePath, cached);
  501. return result;
  502. }
  503. // these are built-in query parameters so should be ignored
  504. // if the user happen to add them as attrs
  505. const ignoreList = ['id', 'index', 'src', 'type'];
  506. function attrsToQuery(attrs, langFallback) {
  507. let query = ``;
  508. for (const name in attrs) {
  509. const value = attrs[name];
  510. if (!ignoreList.includes(name)) {
  511. query += `&${querystring_1.default.escape(name)}=${value ? querystring_1.default.escape(String(value)) : ``}`;
  512. }
  513. }
  514. if (langFallback && !(`lang` in attrs)) {
  515. query += `&lang=${langFallback}`;
  516. }
  517. return query;
  518. }
  519. function logError(e, file, src) {
  520. const locString = e.loc ? `:${e.loc.start.line}:${e.loc.start.column}` : ``;
  521. console.error(chalk_1.default.underline(file + locString));
  522. console.error(chalk_1.default.yellow(e.message));
  523. if (e.loc) {
  524. console.error(compiler_sfc_1.generateCodeFrame(src, e.loc.start.offset, e.loc.end.offset) + `\n`);
  525. }
  526. }
  527. //# sourceMappingURL=serverPluginVue.js.map