Skip to content
This repository has been archived by the owner on Aug 28, 2024. It is now read-only.

Commit

Permalink
增加monorepo中单个模块的路径别名解析插件 (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
RandyZ authored Jun 8, 2023
1 parent a565ff9 commit f5cc67b
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 3 deletions.
6 changes: 4 additions & 2 deletions configs/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 0 additions & 1 deletion configs/vite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions configs/vite/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
110 changes: 110 additions & 0 deletions configs/vite/src/plugins/monorepo.ts
Original file line number Diff line number Diff line change
@@ -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<Alias, 'customResolver'>
/**
* 创建别名解析规则
*
* @param config
* @param env
* @param options
* @returns
*/
export const createAlias = (config: UserConfig, env: ConfigEnv, options: Options): Alias => {
const componentCache = new TreeNode<string>();
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),
],
},
}),
};
}
47 changes: 47 additions & 0 deletions configs/vite/test/plugins/monorepo.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
});
Empty file.
4 changes: 4 additions & 0 deletions configs/vite/test/plugins/testpkg/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "monorepo-test",
"version": "0.0.0"
}
Empty file.
1 change: 1 addition & 0 deletions configs/vite/test/plugins/testpkg/src/vue&ts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const aa = { test: true }
3 changes: 3 additions & 0 deletions configs/vite/test/plugins/testpkg/src/vue&ts/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div></div>
</template>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ export {
// @ts-ignore
import Sortable from 'sortablejs'
export { Sortable }

export * from './src/datastructure'
9 changes: 9 additions & 0 deletions packages/utils/src/datastructure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export * from './tree/index';
/**
* 用于存储固定类型的数据字典
*
* 比如:DataDictionary<Component>,用于存储组件清单
*/
export type DataDictionary<T> = {
[key: string]: T;
};
93 changes: 93 additions & 0 deletions packages/utils/src/datastructure/tree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { find, isEmpty } from 'lodash-es';

// 多叉树的类型定义
export class TreeNode<T> {
// 节点值
val?: T;
// 节点路径
path: string;
// 树路径
treePath: string[];
// 子节点
children: TreeNode<T>[];
constructor(path?: string, val?: T, parentNode?:TreeNode<T>) {
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<T>) {
this.children.push(child);
}
// 删除子节点
removeChild(child: TreeNode<T>) {
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<T> | 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<T>): TreeNode<T> | 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<T>(currentPath, undefined, this);
this.addChild(childNode);
if (isEmpty(nextPaths)) {
return childNode;
} else {
return childNode.findByPath(nextPaths, createWhenNotFound);
}
} else {
return null;
}
}
return null;
}
}

0 comments on commit f5cc67b

Please sign in to comment.