From eaeba6652da39aca2579176d295e1123e73b0816 Mon Sep 17 00:00:00 2001 From: RandyZhang Date: Thu, 8 Jun 2023 17:59:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0monorepo=E4=B8=AD=E5=8D=95?= =?UTF-8?q?=E4=B8=AA=E6=A8=A1=E5=9D=97=E7=9A=84=E8=B7=AF=E5=BE=84=E5=88=AB?= =?UTF-8?q?=E5=90=8D=E8=A7=A3=E6=9E=90=E6=8F=92=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/vite/package.json | 6 +- configs/vite/src/index.ts | 1 - configs/vite/src/plugins/index.ts | 3 + configs/vite/src/plugins/monorepo.ts | 110 ++++++++++++++++++ configs/vite/test/plugins/monorepo.test.ts | 47 ++++++++ configs/vite/test/plugins/testpkg/index.ts | 0 .../vite/test/plugins/testpkg/package.json | 4 + configs/vite/test/plugins/testpkg/src/test.ts | 0 .../test/plugins/testpkg/src/vue&ts/index.ts | 1 + .../test/plugins/testpkg/src/vue&ts/index.vue | 3 + package.json | 1 + packages/utils/index.ts | 2 + packages/utils/src/datastructure/index.ts | 9 ++ .../utils/src/datastructure/tree/index.ts | 93 +++++++++++++++ 14 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 configs/vite/src/plugins/monorepo.ts create mode 100644 configs/vite/test/plugins/monorepo.test.ts create mode 100644 configs/vite/test/plugins/testpkg/index.ts create mode 100644 configs/vite/test/plugins/testpkg/package.json create mode 100644 configs/vite/test/plugins/testpkg/src/test.ts create mode 100644 configs/vite/test/plugins/testpkg/src/vue&ts/index.ts create mode 100644 configs/vite/test/plugins/testpkg/src/vue&ts/index.vue create mode 100644 packages/utils/src/datastructure/index.ts create mode 100644 packages/utils/src/datastructure/tree/index.ts diff --git a/configs/vite/package.json b/configs/vite/package.json index 054d5f83..ce1817cc 100644 --- a/configs/vite/package.json +++ b/configs/vite/package.json @@ -23,7 +23,9 @@ "@vitejs/plugin-vue-jsx": "^3.0.1", "dayjs": "^1.11.7", "dotenv": "16.0.3", + "glob": "^10.2.7", "less": "^4.1.3", + "lodash-es": "^4.17.21", "picocolors": "^1.0.0", "pkg-types": "^1.0.1", "rollup-plugin-visualizer": "^5.8.3", @@ -35,9 +37,9 @@ "vite-plugin-html": "^3.2.0", "vite-plugin-imagemin": "^0.6.1", "vite-plugin-mock": "2.9.6", + "vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-purge-icons": "^0.9.2", - "vite-plugin-svg-icons": "^2.0.1", - "vite-plugin-monaco-editor": "^1.1.0" + "vite-plugin-svg-icons": "^2.0.1" }, "devDependencies": { "mockjs": "^1.1.0", diff --git a/configs/vite/src/index.ts b/configs/vite/src/index.ts index 13129ba2..0b072c38 100644 --- a/configs/vite/src/index.ts +++ b/configs/vite/src/index.ts @@ -45,7 +45,6 @@ export async function createViteConfig( resolve: { alias: { '@': `${resolve(root, 'src')}`, - '#': `${resolve(root, '../../packages/vbenComponents/src')}`, // layouts: `${resolve(root, '../../packages/layouts/src')}`, 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', vue: 'vue/dist/vue.esm-bundler.js', diff --git a/configs/vite/src/plugins/index.ts b/configs/vite/src/plugins/index.ts index fd197e94..0b4eabf3 100644 --- a/configs/vite/src/plugins/index.ts +++ b/configs/vite/src/plugins/index.ts @@ -14,6 +14,7 @@ import { configUnocssPlugin } from './unocss' import { createConfigPlugin } from './config' import { configHttpsPlugin } from './https' import monacoEditorPlugin from 'vite-plugin-monaco-editor' +import MonoRepoAliasPlugin from './monorepo' export async function configVitePlugins( root: string, viteEnv: ViteEnv, @@ -61,6 +62,8 @@ export async function configVitePlugins( vitePlugins.push(configHttpsPlugin(viteEnv)) // monacoEditorPlugin vitePlugins.push(monacoEditorPlugin({})) + // MonorepoSupport + vitePlugins.push(MonoRepoAliasPlugin()) // The following plugins only work in the production environment if (isBuild) { diff --git a/configs/vite/src/plugins/monorepo.ts b/configs/vite/src/plugins/monorepo.ts new file mode 100644 index 00000000..3b4aafab --- /dev/null +++ b/configs/vite/src/plugins/monorepo.ts @@ -0,0 +1,110 @@ +import type { Alias, ConfigEnv, Plugin, UserConfig } from 'vite'; +import fs from 'fs' +import path from 'path' +import { globSync } from 'glob' +import { TreeNode } from '@vben/utils'; +import { split, slice, indexOf, isEmpty, find } from 'lodash-es'; +import { bold, cyan, gray, green } from 'picocolors'; + +export type Options = Omit +/** + * 创建别名解析规则 + * + * @param config + * @param env + * @param options + * @returns + */ +export const createAlias = (config: UserConfig, env: ConfigEnv, options: Options): Alias => { + const componentCache = new TreeNode(); + return { + ...options, + customResolver(updatedId, importerId, resolveOptions) { + const pathSegs = split(importerId, path.sep).filter(p => p !== '') + const paths = slice(pathSegs, 0, indexOf(pathSegs, 'src')) + const cacheNode = componentCache.findByPath([...paths], true); + if (cacheNode) { + const pkgBasePath = path.resolve(path.sep, path.join(...[...cacheNode.treePath, cacheNode.path])) + const pkgPath = path.join(pkgBasePath, 'package.json') + if (!fs.existsSync(pkgPath)) { + throw new Error(`MonoRepoResolverPlugin can not resolve Module at: ${pkgBasePath}`) + } + // 分割别名对应的相对路径路径。代码实际导入的时候都会使用'/',不需要使用Path.seg + const relPaths = slice(split(updatedId, '/').filter(p => p !== '')) + // 从缓存的包路径节点继续向下查找对应的组件路径 + const pkgNode = cacheNode.findByPath(relPaths, true); + if (pkgNode) { + if (isEmpty(pkgNode.val)) { + // 如果缓存的路径节点没有值,说明还没有解析过,需要解析 + const parsedComPath = path.join(...[pkgBasePath, ...relPaths]) + let realPath + if (fs.existsSync(parsedComPath)) { + // import路径存在,确定是文件还是文件夹,分别处理 + if (fs.statSync(parsedComPath).isDirectory()) { + // 如果导入的是文件夹,文件加载应该有index.xxx的入口文件 + const components = globSync(`${parsedComPath}${path.sep}index.*`) + if (components.length === 1) { + realPath = components[0] + } else { + const fileTsOrJs = find(components, (c) => c.endsWith('.ts') || c.endsWith('.js')) + if (fileTsOrJs) { + realPath = fileTsOrJs + } else { + throw new Error( + `MonoRepoResolverPlugin can not resolve Module <${updatedId}> at: ${importerId}, find ${components.length === 0 ? 'none' : 'multiple' + } files at: ${parsedComPath}, please check it. components: ${components}` + ); + } + } + } else { + // 如果导入的是文件,直接使用 + realPath = parsedComPath + } + } else { + // import文件不存在,需要进一步处理,尝试直接搜索相关文件 + const components = globSync(`${parsedComPath}.*`) + if (components.length === 1) { + realPath = components[0] + } else { + throw new Error( + `MonoRepoResolverPlugin can not resolve Module <${updatedId}> at: ${importerId}, find ${components.length === 0 ? 'none' : 'multiple' + } files at: ${parsedComPath}, please check it. components: ${components}` + ); + } + } + pkgNode.val = realPath + console.debug(`${bold(cyan('[MonoRepoResolverPlugin]'))} ${green(`resolve Component from "${updatedId}" to ${pkgNode?.val} at:`)} ${gray(importerId)}`); + } + return pkgNode.val; + } else { + throw new Error(`MonoRepoResolverPlugin can not resolve Module <${updatedId}> at: ${importerId}, cache module subtree is empty`) + } + } else { + throw new Error(`MonoRepoResolverPlugin can not resolve Module at: ${importerId}, cache module tree is empty`) + } + }, + } +} +/** + * 导出Vite插件 + * + * @param rawOptions + * @returns + */ +export default function configMonoRepoResolverPlugin( + rawOptions: Options = { + find: '#', + replacement: 'src' + } +): Plugin { + return { + name: 'MonoRepoResolver', + config: (config, env) => ({ + resolve: { + alias: [ + createAlias(config, env, rawOptions), + ], + }, + }), + }; +} diff --git a/configs/vite/test/plugins/monorepo.test.ts b/configs/vite/test/plugins/monorepo.test.ts new file mode 100644 index 00000000..4e7b4b0b --- /dev/null +++ b/configs/vite/test/plugins/monorepo.test.ts @@ -0,0 +1,47 @@ +import { expect, test, describe } from 'vitest'; +import path from 'path'; +import { createAlias } from '../../src/plugins/monorepo'; +import { ResolverFunction } from 'vite'; + +const TEST_TITLE = '【测试MonoRepoResolverPlugin】' +const buildTestPkgPath = (pkgRoot: string = process.cwd()) => { + const demoPkgPath = path.resolve(pkgRoot, './configs/vite/test/plugins/testpkg') + return [ + demoPkgPath, + path.resolve(demoPkgPath, './index.ts') + ] +} + +describe('【测试MonoRepoResolverPlugin】', () => { + test(`正常创建alias`, async () => { + const aliasConfig = createAlias({}, { command: 'serve', mode: 'test' }, { + find: '#', + replacement: 'src' + }); + expect(aliasConfig).toBeTruthy() + expect(aliasConfig.customResolver).toBeTypeOf('function') + }) + + const aliasConfig = createAlias({}, { command: 'serve', mode: 'test' }, { + find: '#', + replacement: 'src' + }); + const customResolver = aliasConfig.customResolver as ResolverFunction + + describe('测试customResolver解析组件路径', () => { + test(`正常解析testpkg/src/test.ts`, async () => { + const [, importerId] = buildTestPkgPath() + expect(customResolver.call(null, 'src/test', importerId, null)).contains('test/plugins/testpkg/src/test.ts') + }) + + test(`找不到组件`, async () => { + const [, importerId] = buildTestPkgPath() + expect(() => customResolver.call(null, 'src/notfound', importerId)).toThrowError(/find none files at/) + }) + + test(`组件中存在index.ts和index.vue, 解析到index.ts`, async () => { + const [, importerId] = buildTestPkgPath() + expect(customResolver.call(null, 'src/vue&ts', importerId)).contains('testpkg/src/vue&ts/index.ts') + }) + }) +}); \ No newline at end of file diff --git a/configs/vite/test/plugins/testpkg/index.ts b/configs/vite/test/plugins/testpkg/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/configs/vite/test/plugins/testpkg/package.json b/configs/vite/test/plugins/testpkg/package.json new file mode 100644 index 00000000..c893e523 --- /dev/null +++ b/configs/vite/test/plugins/testpkg/package.json @@ -0,0 +1,4 @@ +{ + "name": "monorepo-test", + "version": "0.0.0" +} \ No newline at end of file diff --git a/configs/vite/test/plugins/testpkg/src/test.ts b/configs/vite/test/plugins/testpkg/src/test.ts new file mode 100644 index 00000000..e69de29b diff --git a/configs/vite/test/plugins/testpkg/src/vue&ts/index.ts b/configs/vite/test/plugins/testpkg/src/vue&ts/index.ts new file mode 100644 index 00000000..983065df --- /dev/null +++ b/configs/vite/test/plugins/testpkg/src/vue&ts/index.ts @@ -0,0 +1 @@ +export const aa = { test: true } \ No newline at end of file diff --git a/configs/vite/test/plugins/testpkg/src/vue&ts/index.vue b/configs/vite/test/plugins/testpkg/src/vue&ts/index.vue new file mode 100644 index 00000000..e5e6a79d --- /dev/null +++ b/configs/vite/test/plugins/testpkg/src/vue&ts/index.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/package.json b/package.json index 42a8d3c6..fc1481ff 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "changeset": "changeset", "clean": "pnpm turbo run clean && rimraf node_modules", "dev": "pnpm --filter scripts dev", + "serve": "pnpm --filter @apps/admin dev", "format": "pnpm run lint:eslint & pnpm run lint:stylelint & pnpm run lint:prettier", "preinstall": "npx only-allow pnpm", "postinstall": "pnpm run stub", diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 41e86323..fd09f22b 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -16,3 +16,5 @@ export { // @ts-ignore import Sortable from 'sortablejs' export { Sortable } + +export * from './src/datastructure' diff --git a/packages/utils/src/datastructure/index.ts b/packages/utils/src/datastructure/index.ts new file mode 100644 index 00000000..8ff4d4c5 --- /dev/null +++ b/packages/utils/src/datastructure/index.ts @@ -0,0 +1,9 @@ +export * from './tree/index'; +/** + * 用于存储固定类型的数据字典 + * + * 比如:DataDictionary,用于存储组件清单 + */ +export type DataDictionary = { + [key: string]: T; +}; diff --git a/packages/utils/src/datastructure/tree/index.ts b/packages/utils/src/datastructure/tree/index.ts new file mode 100644 index 00000000..4ea5149e --- /dev/null +++ b/packages/utils/src/datastructure/tree/index.ts @@ -0,0 +1,93 @@ +import { find, isEmpty } from 'lodash-es'; + +// 多叉树的类型定义 +export class TreeNode { + // 节点值 + val?: T; + // 节点路径 + path: string; + // 树路径 + treePath: string[]; + // 子节点 + children: TreeNode[]; + constructor(path?: string, val?: T, parentNode?:TreeNode) { + this.val = val; + this.path = path || ''; + this.children = []; + if (parentNode) { + if (parentNode.isRoot()) { + this.treePath = []; + } else { + this.treePath = [...parentNode.treePath, parentNode.path]; + } + } else { + this.treePath = [] + } + } + + // 是否是根节点 + isRoot(): boolean { + return this.treePath.length === 0 && this.path === ''; + } + + // 添加子节点 + addChild(child: TreeNode) { + this.children.push(child); + } + // 删除子节点 + removeChild(child: TreeNode) { + const index = this.children.indexOf(child); + if (index > -1) { + this.children.splice(index, 1); + } + } + // 是否是目标节点 + isNodeValEqual(val: T): boolean { + return this.val === val; + } + + // 深度优先查找 + dfs(val: T): TreeNode | null { + if (this.isNodeValEqual(val)) { + return this; + } + for (let i = 0; i < this.children.length; i++) { + const node = this.children[i].dfs(val); + if (node) { + return node; + } + } + return null; + } + + /** + * 按路径查询 + * @param paths 路径数组 + * @param createWhenNotFound 路径数组 + * @returns + */ + findByPath(paths: string[], createWhenNotFound = false, _parentNode?:TreeNode): TreeNode | null { + for (let i = 0; i < paths.length; i++) { + const [currentPath, ...nextPaths] = paths; + const child = find(this.children, (node) => node.path === currentPath); + if (child) { + if (isEmpty(nextPaths)) { + return child; + } else { + return child.findByPath(nextPaths, createWhenNotFound); + } + } else if (createWhenNotFound) { + const childNode = new TreeNode(currentPath, undefined, this); + this.addChild(childNode); + if (isEmpty(nextPaths)) { + return childNode; + } else { + return childNode.findByPath(nextPaths, createWhenNotFound); + } + } else { + return null; + } + } + return null; + } +}