UNPKG

@uni-ku/root

Version:

借助 Vite 模拟出虚拟的全局组件,解决 Uniapp 无法使用全局共享组件问题

210 lines (209 loc) 8.82 kB
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 };