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

增加monorepo中单个模块的路径别名解析插件 #70

Merged
merged 1 commit into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}