Skip to content

Instantly share code, notes, and snippets.

@x-N0
Created September 18, 2020 20:33
Show Gist options
  • Save x-N0/997e9cb89279d81e1110d8201f390bbb to your computer and use it in GitHub Desktop.
Save x-N0/997e9cb89279d81e1110d8201f390bbb to your computer and use it in GitHub Desktop.
The purpose of this gist is to solve an issue ocurring in Vite V1 Rc4 that troubles when you use Visual Studio Code (VSC) because of the saving system that VSC uses (it replaces the file), so there's a work around, there I am just adding a TimeOut of 100ms to the file watcher;
watcher.on('change', (file) => {
setTimeout(()=>{
if (file.endsWith('.vue')) {
handleVueReload(file);
}
},100)
});
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.vuePlugin = exports.vueCache = exports.srcImportMap = void 0;
const querystring_1 = __importDefault(require("querystring"));
const chalk_1 = __importDefault(require("chalk"));
const path_1 = __importDefault(require("path"));
const compiler_sfc_1 = require("@vue/compiler-sfc");
const resolveVue_1 = require("../utils/resolveVue");
const hash_sum_1 = __importDefault(require("hash-sum"));
const lru_cache_1 = __importDefault(require("lru-cache"));
const serverPluginHmr_1 = require("./serverPluginHmr");
const utils_1 = require("../utils");
const esbuildService_1 = require("../esbuildService");
const resolver_1 = require("../resolver");
const serverPluginServeStatic_1 = require("./serverPluginServeStatic");
const serverPluginCss_1 = require("./serverPluginCss");
const cssUtils_1 = require("../utils/cssUtils");
const serverPluginModuleRewrite_1 = require("./serverPluginModuleRewrite");
const serverPluginSourceMap_1 = require("./serverPluginSourceMap");
const debug = require('debug')('vite:sfc');
const getEtag = require('etag');
exports.srcImportMap = new Map();
exports.vueCache = new lru_cache_1.default({
max: 65535
});
exports.vuePlugin = ({ root, app, resolver, watcher, config }) => {
const etagCacheCheck = (ctx) => {
ctx.etag = getEtag(ctx.body);
ctx.status =
serverPluginServeStatic_1.seenUrls.has(ctx.url) && ctx.etag === ctx.get('If-None-Match') ? 304 : 200;
serverPluginServeStatic_1.seenUrls.add(ctx.url);
};
app.use(async (ctx, next) => {
// ctx.vue is set by other tools like vitepress so that vite knows to treat
// non .vue files as vue files.
if (!ctx.path.endsWith('.vue') && !ctx.vue) {
return next();
}
const query = ctx.query;
const publicPath = ctx.path;
let filePath = resolver.requestToFile(publicPath);
// upstream plugins could've already read the file
const descriptor = await parseSFC(root, filePath, ctx.body);
if (!descriptor) {
return next();
}
if (!query.type) {
// watch potentially out of root vue file since we do a custom read here
utils_1.watchFileIfOutOfRoot(watcher, root, filePath);
if (descriptor.script && descriptor.script.src) {
filePath = await resolveSrcImport(root, descriptor.script, ctx, resolver);
}
ctx.type = 'js';
const { code, map } = await compileSFCMain(descriptor, filePath, publicPath, root);
ctx.body = code;
ctx.map = map;
return etagCacheCheck(ctx);
}
if (query.type === 'template') {
const templateBlock = descriptor.template;
if (templateBlock.src) {
filePath = await resolveSrcImport(root, templateBlock, ctx, resolver);
}
ctx.type = 'js';
const cached = exports.vueCache.get(filePath);
const bindingMetadata = cached && cached.script && cached.script.bindings;
const vueSpecifier = resolver_1.resolveBareModuleRequest(root, 'vue', publicPath, resolver);
const { code, map } = compileSFCTemplate(root, templateBlock, filePath, publicPath, descriptor.styles.some((s) => s.scoped), bindingMetadata, vueSpecifier, config);
ctx.body = code;
ctx.map = map;
return etagCacheCheck(ctx);
}
if (query.type === 'style') {
const index = Number(query.index);
const styleBlock = descriptor.styles[index];
if (styleBlock.src) {
filePath = await resolveSrcImport(root, styleBlock, ctx, resolver);
}
const id = hash_sum_1.default(publicPath);
const result = await compileSFCStyle(root, styleBlock, index, filePath, publicPath, config);
ctx.type = 'js';
ctx.body = serverPluginCss_1.codegenCss(`${id}-${index}`, result.code, result.modules);
return etagCacheCheck(ctx);
}
if (query.type === 'custom') {
const index = Number(query.index);
const customBlock = descriptor.customBlocks[index];
if (customBlock.src) {
filePath = await resolveSrcImport(root, customBlock, ctx, resolver);
}
const result = resolveCustomBlock(customBlock, index, filePath, publicPath);
ctx.type = 'js';
ctx.body = result;
return etagCacheCheck(ctx);
}
});
const handleVueReload = (watcher.handleVueReload = async (filePath, timestamp = Date.now(), content) => {
const publicPath = resolver.fileToRequest(filePath);
const cacheEntry = exports.vueCache.get(filePath);
const { send } = watcher;
serverPluginHmr_1.debugHmr(`busting Vue cache for ${filePath}`);
exports.vueCache.del(filePath);
const descriptor = await parseSFC(root, filePath, content);
if (!descriptor) {
// read failed
return;
}
const prevDescriptor = cacheEntry && cacheEntry.descriptor;
if (!prevDescriptor) {
// the file has never been accessed yet
serverPluginHmr_1.debugHmr(`no existing descriptor found for ${filePath}`);
return;
}
// check which part of the file changed
let needRerender = false;
const sendReload = () => {
send({
type: 'vue-reload',
path: publicPath,
changeSrcPath: publicPath,
timestamp
});
console.log(chalk_1.default.green(`[vite:hmr] `) +
`${path_1.default.relative(root, filePath)} updated. (reload)`);
};
if (!isEqualBlock(descriptor.script, prevDescriptor.script) ||
!isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup)) {
return sendReload();
}
if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {
needRerender = true;
}
let didUpdateStyle = false;
const styleId = hash_sum_1.default(publicPath);
const prevStyles = prevDescriptor.styles || [];
const nextStyles = descriptor.styles || [];
// css modules update causes a reload because the $style object is changed
// and it may be used in JS. It also needs to trigger a vue-style-update
// event so the client busts the sw cache.
if (prevStyles.some((s) => s.module != null) ||
nextStyles.some((s) => s.module != null)) {
return sendReload();
}
// force reload if CSS vars injection changed
if (prevStyles.some((s, i) => {
const next = nextStyles[i];
if (s.attrs.vars && (!next || next.attrs.vars !== s.attrs.vars)) {
return true;
}
})) {
return sendReload();
}
// force reload if scoped status has changed
if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
return sendReload();
}
// only need to update styles if not reloading, since reload forces
// style updates as well.
nextStyles.forEach((_, i) => {
if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {
didUpdateStyle = true;
const path = `${publicPath}?type=style&index=${i}`;
send({
type: 'style-update',
path,
changeSrcPath: path,
timestamp
});
}
});
// stale styles always need to be removed
prevStyles.slice(nextStyles.length).forEach((_, i) => {
didUpdateStyle = true;
send({
type: 'style-remove',
path: publicPath,
id: `${styleId}-${i + nextStyles.length}`
});
});
const prevCustoms = prevDescriptor.customBlocks || [];
const nextCustoms = descriptor.customBlocks || [];
// custom blocks update causes a reload
// because the custom block contents is changed and it may be used in JS.
if (nextCustoms.some((_, i) => !prevCustoms[i] || !isEqualBlock(prevCustoms[i], nextCustoms[i]))) {
return sendReload();
}
if (needRerender) {
send({
type: 'vue-rerender',
path: publicPath,
changeSrcPath: publicPath,
timestamp
});
}
let updateType = [];
if (needRerender) {
updateType.push(`template`);
}
if (didUpdateStyle) {
updateType.push(`style`);
}
if (updateType.length) {
console.log(chalk_1.default.green(`[vite:hmr] `) +
`${path_1.default.relative(root, filePath)} updated. (${updateType.join(' & ')})`);
}
});
watcher.on('change', (file) => {
setTimeout(()=>{
if (file.endsWith('.vue')) {
handleVueReload(file);
}
},100)
});
};
function isEqualBlock(a, b) {
if (!a && !b)
return true;
if (!a || !b)
return false;
// src imports will trigger their own updates
if (a.src && b.src && a.src === b.src)
return true;
if (a.content !== b.content)
return false;
const keysA = Object.keys(a.attrs);
const keysB = Object.keys(b.attrs);
if (keysA.length !== keysB.length) {
return false;
}
return keysA.every((key) => a.attrs[key] === b.attrs[key]);
}
async function resolveSrcImport(root, block, ctx, resolver) {
const importer = ctx.path;
const importee = utils_1.cleanUrl(serverPluginModuleRewrite_1.resolveImport(root, importer, block.src, resolver));
const filePath = resolver.requestToFile(importee);
block.content = (await ctx.read(filePath)).toString();
// register HMR import relationship
serverPluginHmr_1.debugHmr(` ${importer} imports ${importee}`);
serverPluginHmr_1.ensureMapEntry(serverPluginHmr_1.importerMap, importee).add(ctx.path);
exports.srcImportMap.set(filePath, ctx.url);
return filePath;
}
async function parseSFC(root, filePath, content) {
let cached = exports.vueCache.get(filePath);
if (cached && cached.descriptor) {
debug(`${filePath} parse cache hit`);
return cached.descriptor;
}
if (!content) {
try {
content = await utils_1.cachedRead(null, filePath);
}
catch (e) {
return;
}
}
if (typeof content !== 'string') {
content = content.toString();
}
const start = Date.now();
const { parse } = resolveVue_1.resolveCompiler(root);
const { descriptor, errors } = parse(content, {
filename: filePath,
sourceMap: true
});
if (errors.length) {
console.error(chalk_1.default.red(`\n[vite] SFC parse error: `));
errors.forEach((e) => {
logError(e, filePath, content);
});
}
cached = cached || { styles: [], customs: [] };
cached.descriptor = descriptor;
exports.vueCache.set(filePath, cached);
debug(`${filePath} parsed in ${Date.now() - start}ms.`);
return descriptor;
}
async function compileSFCMain(descriptor, filePath, publicPath, root) {
let cached = exports.vueCache.get(filePath);
if (cached && cached.script) {
return cached.script;
}
const id = hash_sum_1.default(publicPath);
let code = ``;
let content = ``;
let map;
let script = descriptor.script;
const compiler = resolveVue_1.resolveCompiler(root);
if ((descriptor.script || descriptor.scriptSetup) && compiler.compileScript) {
try {
script = compiler.compileScript(descriptor);
}
catch (e) {
console.error(chalk_1.default.red(`\n[vite] SFC <script setup> compilation error:\n${chalk_1.default.dim(chalk_1.default.white(filePath))}`));
console.error(chalk_1.default.yellow(e.message));
}
}
if (script) {
content = script.content;
map = script.map;
if (script.lang === 'ts') {
const res = await esbuildService_1.transform(content, publicPath, {
loader: 'ts'
});
content = res.code;
map = serverPluginSourceMap_1.mergeSourceMap(map, JSON.parse(res.map));
}
}
code += compiler_sfc_1.rewriteDefault(content, '__script');
let hasScoped = false;
let hasCSSModules = false;
if (descriptor.styles) {
descriptor.styles.forEach((s, i) => {
const styleRequest = publicPath + `?type=style&index=${i}`;
if (s.scoped)
hasScoped = true;
if (s.module) {
if (!hasCSSModules) {
code += `\nconst __cssModules = __script.__cssModules = {}`;
hasCSSModules = true;
}
const styleVar = `__style${i}`;
const moduleName = typeof s.module === 'string' ? s.module : '$style';
code += `\nimport ${styleVar} from ${JSON.stringify(styleRequest + '&module')}`;
code += `\n__cssModules[${JSON.stringify(moduleName)}] = ${styleVar}`;
}
else {
code += `\nimport ${JSON.stringify(styleRequest)}`;
}
});
if (hasScoped) {
code += `\n__script.__scopeId = "data-v-${id}"`;
}
}
if (descriptor.customBlocks) {
descriptor.customBlocks.forEach((c, i) => {
const attrsQuery = attrsToQuery(c.attrs, c.lang);
const blockTypeQuery = `&blockType=${querystring_1.default.escape(c.type)}`;
let customRequest = publicPath + `?type=custom&index=${i}${blockTypeQuery}${attrsQuery}`;
const customVar = `block${i}`;
code += `\nimport ${customVar} from ${JSON.stringify(customRequest)}\n`;
code += `if (typeof ${customVar} === 'function') ${customVar}(__script)\n`;
});
}
if (descriptor.template) {
const templateRequest = publicPath + `?type=template`;
code += `\nimport { render as __render } from ${JSON.stringify(templateRequest)}`;
code += `\n__script.render = __render`;
}
code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}`;
code += `\n__script.__file = ${JSON.stringify(filePath)}`;
code += `\nexport default __script`;
const result = {
code,
map,
bindings: script ? script.bindings : undefined
};
cached = cached || { styles: [], customs: [] };
cached.script = result;
exports.vueCache.set(filePath, cached);
return result;
}
function compileSFCTemplate(root, template, filePath, publicPath, scoped, bindingMetadata, vueSpecifier, { vueCompilerOptions, vueTransformAssetUrls = {}, vueTemplatePreprocessOptions = {} }) {
let cached = exports.vueCache.get(filePath);
if (cached && cached.template) {
debug(`${publicPath} template cache hit`);
return cached.template;
}
const start = Date.now();
const { compileTemplate } = resolveVue_1.resolveCompiler(root);
if (typeof vueTransformAssetUrls === 'object') {
vueTransformAssetUrls = {
base: path_1.default.posix.dirname(publicPath),
...vueTransformAssetUrls
};
}
const preprocessLang = template.lang;
let preprocessOptions = preprocessLang && vueTemplatePreprocessOptions[preprocessLang];
if (preprocessLang === 'pug') {
preprocessOptions = {
doctype: 'html',
...preprocessOptions
};
}
const { code, map, errors } = compileTemplate({
source: template.content,
filename: filePath,
inMap: template.map,
transformAssetUrls: vueTransformAssetUrls,
compilerOptions: {
...vueCompilerOptions,
scopeId: scoped ? `data-v-${hash_sum_1.default(publicPath)}` : null,
bindingMetadata,
runtimeModuleName: vueSpecifier
},
preprocessLang,
preprocessOptions,
preprocessCustomRequire: (id) => require(utils_1.resolveFrom(root, id))
});
if (errors.length) {
console.error(chalk_1.default.red(`\n[vite] SFC template compilation error: `));
errors.forEach((e) => {
if (typeof e === 'string') {
console.error(e);
}
else {
logError(e, filePath, template.map.sourcesContent[0]);
}
});
}
const result = {
code,
map: map
};
cached = cached || { styles: [], customs: [] };
cached.template = result;
exports.vueCache.set(filePath, cached);
debug(`${publicPath} template compiled in ${Date.now() - start}ms.`);
return result;
}
async function compileSFCStyle(root, style, index, filePath, publicPath, { cssPreprocessOptions, cssModuleOptions }) {
let cached = exports.vueCache.get(filePath);
const cachedEntry = cached && cached.styles && cached.styles[index];
if (cachedEntry) {
debug(`${publicPath} style cache hit`);
return cachedEntry;
}
const start = Date.now();
const { generateCodeFrame } = resolveVue_1.resolveCompiler(root);
const resource = filePath + `?type=style&index=${index}`;
const result = (await cssUtils_1.compileCss(root, publicPath, {
source: style.content,
filename: resource,
id: ``,
scoped: style.scoped != null,
vars: style.vars != null,
modules: style.module != null,
preprocessLang: style.lang,
preprocessOptions: cssPreprocessOptions,
modulesOptions: cssModuleOptions
}));
cssUtils_1.recordCssImportChain(result.dependencies, resource);
if (result.errors.length) {
console.error(chalk_1.default.red(`\n[vite] SFC style compilation error: `));
result.errors.forEach((e) => {
if (typeof e === 'string') {
console.error(e);
}
else {
const lineOffset = style.loc.start.line - 1;
if (e.line && e.column) {
console.log(chalk_1.default.underline(`${filePath}:${e.line + lineOffset}:${e.column}`));
}
else {
console.log(chalk_1.default.underline(filePath));
}
const filePathRE = new RegExp('.*' +
path_1.default.basename(filePath).replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') +
'(:\\d+:\\d+:\\s*)?');
const cleanMsg = e.message.replace(filePathRE, '');
console.error(chalk_1.default.yellow(cleanMsg));
if (e.line && e.column && cleanMsg.split(/\n/g).length === 1) {
const original = style.map.sourcesContent[0];
const offset = original
.split(/\r?\n/g)
.slice(0, e.line + lineOffset - 1)
.map((l) => l.length)
.reduce((total, l) => total + l + 1, 0) +
e.column -
1;
console.error(generateCodeFrame(original, offset, offset + 1)) + `\n`;
}
}
});
}
result.code = await cssUtils_1.rewriteCssUrls(result.code, publicPath);
cached = cached || { styles: [], customs: [] };
cached.styles[index] = result;
exports.vueCache.set(filePath, cached);
debug(`${publicPath} style compiled in ${Date.now() - start}ms`);
return result;
}
function resolveCustomBlock(custom, index, filePath, publicPath) {
let cached = exports.vueCache.get(filePath);
const cachedEntry = cached && cached.customs && cached.customs[index];
if (cachedEntry) {
debug(`${publicPath} custom block cache hit`);
return cachedEntry;
}
const result = custom.content;
cached = cached || { styles: [], customs: [] };
cached.customs[index] = result;
exports.vueCache.set(filePath, cached);
return result;
}
// these are built-in query parameters so should be ignored
// if the user happen to add them as attrs
const ignoreList = ['id', 'index', 'src', 'type'];
function attrsToQuery(attrs, langFallback) {
let query = ``;
for (const name in attrs) {
const value = attrs[name];
if (!ignoreList.includes(name)) {
query += `&${querystring_1.default.escape(name)}=${value ? querystring_1.default.escape(String(value)) : ``}`;
}
}
if (langFallback && !(`lang` in attrs)) {
query += `&lang=${langFallback}`;
}
return query;
}
function logError(e, file, src) {
const locString = e.loc ? `:${e.loc.start.line}:${e.loc.start.column}` : ``;
console.error(chalk_1.default.underline(file + locString));
console.error(chalk_1.default.yellow(e.message));
if (e.loc) {
console.error(compiler_sfc_1.generateCodeFrame(src, e.loc.start.offset, e.loc.end.offset) + `\n`);
}
}
//# sourceMappingURL=serverPluginVue.js.map
@x-N0
Copy link
Author

x-N0 commented Sep 18, 2020

Go Further:
vitejs/vite#610

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment