@uni-ku/root
Version:
借助 Vite 模拟出虚拟的全局组件,解决 Uniapp 无法使用全局共享组件问题
210 lines (209 loc) • 8.82 kB
JavaScript
import { dirname, extname, join, relative, resolve } from "node:path";
import process from "node:process";
import chokidar from "chokidar";
import { createFilter, normalizePath } from "vite";
import { MagicString, parse } from "@vue/compiler-sfc";
import { existsSync, readFileSync } from "node:fs";
import { parse as parse$1 } from "jsonc-parser";
//#region src/utils.ts
async function parseSFC(code) {
try {
return parse(code, { pad: "space" }).descriptor || parse({ source: code });
} catch {
throw new Error("[@uni-ku/root] Vue's version must be 3.2.13 or higher.");
}
}
const PAGE_FILE_EXTS = [".vue", ".nvue"];
function formatPagePaths(root, path) {
const joinedPath = join(root, path);
if (extname(joinedPath)) return [normalizePath(joinedPath)];
const pageFilePaths = PAGE_FILE_EXTS.map((fileExt) => `${joinedPath}${fileExt}`).filter((filePath) => existsSync(filePath));
if (pageFilePaths.length) return pageFilePaths.map((filePath) => normalizePath(filePath));
return [normalizePath(`${joinedPath}.vue`)];
}
function loadPagePaths(path, rootPath) {
const pagesJson = parse$1(readFileSync(path, "utf-8"));
const { pages = [] } = pagesJson;
const subPackages = pagesJson.subPackages ?? pagesJson.subpackages ?? [];
return [...pages.flatMap((page) => formatPagePaths(rootPath, page.path)), ...subPackages.map(({ pages = [], root = "" }) => {
return pages.flatMap((page) => formatPagePaths(join(rootPath, root), page.path));
}).flat()];
}
function toKebabCase(str) {
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
}
function toPascalCase(str) {
return str.replace(/(^\w|-+\w)/g, (match) => match.toUpperCase().replace(/-/g, ""));
}
function findNode(sfc, rawTagName) {
const templateSource = sfc.template?.content;
if (!templateSource) return;
let tagName = "";
if (templateSource.includes(`<${toKebabCase(rawTagName)}`)) tagName = toKebabCase(rawTagName);
else if (templateSource.includes(`<${toPascalCase(rawTagName)}`)) tagName = toPascalCase(rawTagName);
if (!tagName) return;
const nodeAst = sfc.template?.ast;
if (!nodeAst) return;
const traverse = (nodes) => {
for (const node of nodes) if (node.type === 1) {
if (node.tag === tagName) return node;
if (node.children?.length) {
const found = traverse(node.children);
if (found) return found;
}
}
};
return traverse(nodeAst.children);
}
const platform = process.env.UNI_PLATFORM;
function normalizePlatformPath(id) {
if (extname(id) !== ".vue") return id;
if (!id.includes(`.${platform}.`)) return id;
return id.replace(`.${platform}.`, ".");
}
function toArray(value) {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
function getRelativePath(fromFile, toFile) {
const importPath = normalizePath(relative(dirname(fromFile), toFile));
return importPath.startsWith(".") ? importPath : `./${importPath}`;
}
//#endregion
//#region src/page.ts
async function transformPage(code, enabledGlobalRef = false) {
const sfc = await parseSFC(code);
const ms = new MagicString(code);
wrapTemplate(ms, sfc, "global-ku-root", getPageRootRefSource(sfc, enabledGlobalRef));
return ms;
}
async function transformNvuePage(code, componentImportPath, enabledGlobalRef = false) {
const sfc = await parseSFC(code);
const ms = new MagicString(code);
wrapTemplate(ms, sfc, "GlobalKuRoot", getPageRootRefSource(sfc, enabledGlobalRef));
if (sfc.scriptSetup) ms.appendLeft(sfc.scriptSetup.loc.start.offset, getNvueRootImport(componentImportPath));
else if (sfc.script) {
ms.appendLeft(sfc.script.loc.start.offset, getNvueRootImport(componentImportPath));
ensureNvueOptionsComponent(ms, sfc.script);
} else ms.append(`\n<script>\n${getNvueRootImport(componentImportPath)}export default {\n components: { GlobalKuRoot },\n}\n<\/script>\n`);
return ms;
}
function getPageRootRefSource(sfc, enabledGlobalRef = false) {
const pageTempAttrs = sfc.template?.attrs;
if (pageTempAttrs && pageTempAttrs.root) return `ref="${pageTempAttrs.root}"`;
return enabledGlobalRef ? "ref=\"uniKuRoot\"" : "";
}
function wrapTemplate(ms, sfc, tagName, rootRefSource = "") {
const pageTempStart = sfc.template?.loc.start.offset;
const pageTempEnd = sfc.template?.loc.end.offset;
let pageMetaSource = "";
const pageMetaNode = findNode(sfc, "PageMeta");
if (pageMetaNode) {
pageMetaSource = pageMetaNode.loc.source;
const metaTempStart = pageMetaNode.loc.start.offset;
const metaTempEnd = pageMetaNode.loc.end.offset;
ms.remove(metaTempStart, metaTempEnd);
}
if (pageTempStart != null && pageTempEnd != null) {
const refSource = rootRefSource ? ` ${rootRefSource}` : "";
const pageMetaPrefix = pageMetaSource ? `\n${pageMetaSource}\n` : "\n";
ms.appendLeft(pageTempStart, `${pageMetaPrefix}<${tagName}${refSource}>`);
ms.appendRight(pageTempEnd, `\n</${tagName}>\n`);
}
}
function getNvueRootImport(componentImportPath) {
return `import GlobalKuRoot from '${componentImportPath}'\n`;
}
function findExportDefaultObjectStart(scriptContent) {
const exportDefaultIndex = scriptContent.indexOf("export default");
if (exportDefaultIndex < 0) return -1;
const afterExportDefault = scriptContent.slice(exportDefaultIndex + 14);
const defineComponentMatch = afterExportDefault.match(/^\s*defineComponent\s*\(\s*\{/);
if (defineComponentMatch) return exportDefaultIndex + 14 + defineComponentMatch[0].length;
const objectMatch = afterExportDefault.match(/^\s*\{/);
if (objectMatch) return exportDefaultIndex + 14 + objectMatch[0].length;
return -1;
}
function ensureNvueOptionsComponent(ms, script) {
const componentsMatch = script.content.match(/components\s*:\s*\{/);
if (componentsMatch && componentsMatch.index != null) {
ms.appendLeft(componentsMatch.index + componentsMatch[0].length, " GlobalKuRoot,");
return;
}
const exportDefaultObjectStart = findExportDefaultObjectStart(script.content);
if (exportDefaultObjectStart >= 0) {
ms.appendLeft(exportDefaultObjectStart, "\n components: { GlobalKuRoot },");
return;
}
ms.appendLeft(script.loc.end.offset, "\nexport default {\n components: { GlobalKuRoot },\n}\n");
}
//#endregion
//#region src/root.ts
async function registerKuApp(code, fileName = "App.ku") {
const ms = new MagicString(code);
const importCode = `import GlobalKuRoot from "./${fileName}.vue";`;
ms.prepend(`${importCode}\n`).replace(/(createApp[\s\S]*?)(return\s\{\s*app)/, `$1app.component("global-ku-root", GlobalKuRoot);\n$2`);
return ms;
}
async function rebuildKuApp(code, enabledVirtualHost = false) {
const ms = new MagicString(code);
ms.replace(/<(KuRootView|ku-root-view)(?:\s*\/>|><\/\1>)/, "<slot />");
if (enabledVirtualHost) {
const sfc = await parseSFC(code);
if (sfc.script) return ms;
const langType = sfc.scriptSetup?.lang;
ms.append(`<script ${langType ? `lang="${langType}"` : ""}>
export default {
options: {
virtualHost: true,
}
}\n<\/script>`);
}
return ms;
}
//#endregion
//#region src/index.ts
function UniKuRoot(options) {
options = {
enabledVirtualHost: false,
enabledGlobalRef: false,
rootFileName: "App.ku",
...options
};
const rootPath = normalizePath(process.env.UNI_INPUT_DIR || resolve(process.env.INIT_CWD || process.cwd(), "src"));
const appKuPath = normalizePath(resolve(rootPath, `${options.rootFileName}.vue`));
const pagesPath = normalizePath(resolve(rootPath, "pages.json"));
const excludedPaths = toArray(options.excludePages).filter(Boolean).map((path) => normalizePath(resolve(rootPath, path)));
const mainFiles = [normalizePath(resolve(rootPath, "main.ts")), normalizePath(resolve(rootPath, "main.js"))];
let pagePaths = loadPagePaths(pagesPath, rootPath);
let watcher = null;
let hasPlatformPlugin = false;
return {
name: "vite-plugin-uni-root",
enforce: "pre",
configResolved({ plugins }) {
hasPlatformPlugin = plugins.some((v) => v.name === "vite-plugin-uni-platform");
},
buildStart() {
watcher = chokidar.watch(pagesPath).on("all", (event) => {
if (["add", "change"].includes(event)) pagePaths = loadPagePaths(pagesPath, rootPath);
});
},
async transform(code, id) {
let ms = null;
if (createFilter(mainFiles)(id)) ms = await registerKuApp(code, options.rootFileName);
if (createFilter(appKuPath)(id)) ms = await rebuildKuApp(code, options.enabledVirtualHost);
const pageId = hasPlatformPlugin ? normalizePlatformPath(id) : id;
if (createFilter(pagePaths, excludedPaths)(pageId)) ms = id.endsWith(".nvue") ? await transformNvuePage(code, getRelativePath(id, appKuPath), options.enabledGlobalRef) : await transformPage(code, options.enabledGlobalRef);
if (ms) return {
code: ms.toString(),
map: ms.generateMap({ hires: true })
};
},
buildEnd() {
if (watcher) watcher.close();
}
};
}
//#endregion
export { UniKuRoot as default };