mirror of
https://github.com/pure-admin/vue-pure-admin.git
synced 2025-06-08 09:27:19 +08:00
527 lines
21 KiB
JavaScript
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
|