UNPKG

@uni-ku/root

Version:

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

195 lines (189 loc) 5.9 kB
import { join, extname, resolve } from 'node:path'; import process from 'node:process'; import chokidar from 'chokidar'; import { normalizePath, createFilter } from 'vite'; import { parse as parse$1, MagicString } from '@vue/compiler-sfc'; import { readFileSync } from 'node:fs'; import { parse } from 'jsonc-parser'; async function parseSFC(code) { try { return parse$1(code, { pad: "space" }).descriptor || parse$1({ source: code }); } catch { throw new Error( "[@uni-ku/root] Vue's version must be 3.2.13 or higher." ); } } function formatPagePath(root, path) { return normalizePath(`${join(root, path)}.vue`); } function loadPagesJson(path, rootPath) { const pagesJsonRaw = readFileSync(path, "utf-8"); const { pages = [], subPackages = [] } = parse(pagesJsonRaw); return [ ...pages.map((page) => formatPagePath(rootPath, page.path)), ...subPackages.map(({ pages: pages2 = {}, root = "" }) => { return pages2.map((page) => formatPagePath(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 void 0; }; return traverse(nodeAst.children); } const platform = process.env.UNI_PLATFORM; function normalizePlatformPath(id) { const idExt = extname(id); if (idExt !== ".vue") { return id; } if (!id.includes(`.${platform}.`)) { return id; } return id.replace(`.${platform}.`, "."); } async function transformPage(code, enabledGlobalRef = false) { const sfc = await parseSFC(code); const ms = new MagicString(code); 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); } const pageTempAttrs = sfc.template?.attrs; let pageRootRefSource = enabledGlobalRef ? 'ref="uniKuRoot"' : ""; if (pageTempAttrs && pageTempAttrs.root) { pageRootRefSource = `ref="${pageTempAttrs.root}"`; } if (pageTempStart && pageTempEnd) { ms.appendLeft(pageTempStart, ` ${pageMetaSource} <global-ku-root ${pageRootRefSource}>`); ms.appendRight(pageTempEnd, ` </global-ku-root> `); } return ms; } async function registerKuApp(code, fileName = "App.ku") { const ms = new MagicString(code); const importCode = `import GlobalKuRoot from "./${fileName}.vue";`; const vueUseComponentCode = `app.component("global-ku-root", GlobalKuRoot);`; ms.prepend(`${importCode} `).replace( /(createApp[\s\S]*?)(return\s\{\s*app)/, `$1${vueUseComponentCode} $2` ); return ms; } async function rebuildKuApp(code, enabledVirtualHost = false) { const ms = new MagicString(code); const rootTagNameRE = /<(KuRootView|ku-root-view)(\s*\/>|><\/\1>)/; ms.replace(rootTagNameRE, "<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, } } <\/script>`); } return ms; } function UniKuRoot(options = { rootFileName: "App.ku" }) { const rootPath = process.env.UNI_INPUT_DIR || `${process.env.INIT_CWD}\\src`; const appKuPath = resolve(rootPath, `${options.rootFileName}.vue`); const pagesPath = resolve(rootPath, "pages.json"); let pagesJson = loadPagesJson(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)) { pagesJson = loadPagesJson(pagesPath, rootPath); } }); }, async transform(code, id) { let ms = null; const filterMain = createFilter(`${rootPath}/main.(ts|js)`); if (filterMain(id)) { ms = await registerKuApp(code, options.rootFileName); } const filterKuRoot = createFilter(appKuPath); if (filterKuRoot(id)) { ms = await rebuildKuApp(code, options.enabledVirtualHost); } const pageId = hasPlatformPlugin ? normalizePlatformPath(id) : id; const filterPage = createFilter(pagesJson); if (filterPage(pageId)) { ms = await transformPage(code, options.enabledGlobalRef); } if (ms) { return { code: ms.toString(), map: ms.generateMap({ hires: true }) }; } }, buildEnd() { if (watcher) { watcher.close(); } } }; } export { UniKuRoot as default };