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

527 lines
21 KiB
JavaScript

"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)) {
// #748 should re-use previous cached script if only template change
// so that the template is compiled with the correct binding metadata
if (prevDescriptor.scriptSetup && descriptor.scriptSetup) {
exports.vueCache.get(filePath).script = cacheEntry.script;
}
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) => {
if (file.endsWith('.vue')) {
handleVueReload(file);
}
});
};
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 += `\ntypeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)`;
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