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

修改monorepo插件的兼容问题 #71

Merged
merged 3 commits into from
Jun 13, 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
2 changes: 1 addition & 1 deletion apps/admin/src/hooks/web/useLockScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useThrottleFn } from '@vben/utils'
import { useLockStore } from '@/store/lock'
import { useConfigStore } from '@/store/config'
import { useUserStore } from '@/store/user'
import { useRootSetting } from '../setting/useRootSetting'
import { useRootSetting } from '#/hooks/setting/useRootSetting'
import { BASIC_LOCK_PATH } from '@vben/constants'
import { router } from '@/router'

Expand Down
2 changes: 1 addition & 1 deletion configs/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@vitejs/plugin-vue-jsx": "^3.0.1",
"dayjs": "^1.11.7",
"dotenv": "16.0.3",
"glob": "^10.2.7",
"fast-glob": "^3.2.12",
"less": "^4.1.3",
"lodash-es": "^4.17.21",
"picocolors": "^1.0.0",
Expand Down
236 changes: 181 additions & 55 deletions configs/vite/src/plugins/monorepo.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,130 @@
import type { Alias, ConfigEnv, Plugin, UserConfig } from 'vite';
import fs from 'fs'
import path from 'path'
import { globSync } from 'glob'
import glob from 'fast-glob'
import { TreeNode } from '@vben/utils';
import { split, slice, indexOf, isEmpty, find } from 'lodash-es';
import { find, split, join, isEmpty } from 'lodash-es';
import { bold, cyan, gray, green } from 'picocolors';

export type Options = Omit<Alias, 'customResolver'>
/** Cache for package.json resolution and package.json contents */
export type PackageCache = Map<string, PackageData>
export interface PackageData {
dir: string
componentCache: TreeNode<string>
data: {
[field: string]: any
name: string
type: string
version: string
main: string
module: string
browser: string | Record<string, string | false>
exports: string | Record<string, any> | string[]
imports: Record<string, any>
dependencies: Record<string, string>
}
}

export function loadPackageData(pkgPath: string): PackageData {
const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
const pkgDir = path.dirname(pkgPath)
const pkg: PackageData = {
dir: pkgDir,
componentCache: new TreeNode<string>(),
data,
}
return pkg
}

export function findNearestPackageData(
basedir: string,
packageCache?: PackageCache,
): PackageData | null {
const originalBasedir = basedir
while (basedir) {
if (packageCache) {
const cached = getFnpdCache(packageCache, basedir, originalBasedir)
if (cached) return cached
}

const pkgPath = path.join(basedir, 'package.json')
try {
if (fs.statSync(pkgPath, { throwIfNoEntry: false })?.isFile()) {
const pkgData = loadPackageData(pkgPath)

if (packageCache) {
setFnpdCache(packageCache, pkgData, basedir, originalBasedir)
}

return pkgData
}
} catch { }

const nextBasedir = path.dirname(basedir)
if (nextBasedir === basedir) break
basedir = nextBasedir
}

return null
}

/**
* Get cached `findNearestPackageData` value based on `basedir`. When one is found,
* and we've already traversed some directories between `basedir` and `originalBasedir`,
* we cache the value for those in-between directories as well.
*
* This makes it so the fs is only read once for a shared `basedir`.
*/
function getFnpdCache(
packageCache: PackageCache,
basedir: string,
originalBasedir: string,
) {
const cacheKey = getFnpdCacheKey(basedir)
const pkgData = packageCache.get(cacheKey)
if (pkgData) {
traverseBetweenDirs(originalBasedir, basedir, (dir) => {
packageCache.set(getFnpdCacheKey(dir), pkgData)
})
return pkgData
}
}

function setFnpdCache(
packageCache: PackageCache,
pkgData: PackageData,
basedir: string,
originalBasedir: string,
) {
packageCache.set(getFnpdCacheKey(basedir), pkgData)
traverseBetweenDirs(originalBasedir, basedir, (dir) => {
packageCache.set(getFnpdCacheKey(dir), pkgData)
})
}

// package cache key for `findNearestPackageData`
function getFnpdCacheKey(basedir: string) {
return `fnpd_${basedir}`
}

/**
* Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir.
* @param longerDir Longer dir path, e.g. `/User/foo/bar/baz`
* @param shorterDir Shorter dir path, e.g. `/User/foo`
*/
function traverseBetweenDirs(
longerDir: string,
shorterDir: string,
cb: (dir: string) => void,
) {
while (longerDir !== shorterDir) {
cb(longerDir)
longerDir = path.dirname(longerDir)
}
}


/**
* 创建别名解析规则
*
Expand All @@ -16,69 +134,77 @@ export type Options = Omit<Alias, 'customResolver'>
* @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}`)
customResolver(updatedId, importerId, _resolveOptions) {
const pkgData = findNearestPackageData(importerId || '')
if (!pkgData) {
throw new Error(`MonoRepoResolverPlugin can not resolve Module from: ${importerId}`)
}
// 组件包路径
let pkgPath = pkgData.dir;
const dirPath = path.parse(pkgPath)
let baseRoot = dirPath.root;
if (baseRoot) {
// 处理Root路径
if (process.platform === 'win32') {
// E://aaa/bbb/
// baseRoot = E:
baseRoot = baseRoot.replace(path.sep, '')
// //aaa//bbb
pkgPath = pkgPath.replace(baseRoot, '')
// baseRoot = E:/
baseRoot = baseRoot+ '/'
}
// 分割别名对应的相对路径路径。代码实际导入的时候都会使用'/',不需要使用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}.*`)
}
// Pkg的根路径分割结果
const paths = split(pkgPath, path.sep).filter(p => p !== '')
// 分割别名对应的相对路径路径。代码实际导入的时候都会使用'/',不需要使用Path.seg
const componentPaths = split(updatedId, '/').filter(p => p !== '')
const componentNode = pkgData.componentCache.findByPath(componentPaths, true);
if (componentNode) {
if (isEmpty(componentNode.val)) {
let realPath
const componentPath = baseRoot + join([...paths, ...componentPaths], '/')
if (fs.existsSync(componentPath)) {
// import路径存在,确定是文件还是文件夹,分别处理
if (fs.statSync(componentPath).isDirectory()) {
// 如果导入的是文件夹,文件加载应该有index.xxx的入口文件
const components = glob.sync(`${componentPath}/index.*`, { onlyFiles: true, deep: 1 })
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}`
);
// vue和(js|ts)同时存在优先(js|ts)
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: ${componentPath}/index.(ts|js), please check it. components: ${components}`
);
}
}
} else {
// 如果导入的是文件,直接使用
realPath = componentPath
}
} else {
// import文件不存在,需要进一步处理,尝试直接搜索相关文件
const components = glob.sync(`${componentPath}.*`, { onlyFiles: true, deep: 1 })
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: ${componentPath}, 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`)
componentNode.val = realPath;
console.debug(`${bold(cyan('[MonoRepoResolverPlugin]'))} ${green(`resolve Component from "${updatedId}" to ${realPath} at:`)} ${gray(importerId)}`);
}
return componentNode.val;
} else {
throw new Error(`MonoRepoResolverPlugin can not resolve Module at: ${importerId}, cache module tree is empty`)
}
Expand Down
45 changes: 40 additions & 5 deletions configs/vite/test/plugins/monorepo.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { expect, test, describe } from 'vitest';
import path from 'path';
import { createAlias } from '../../src/plugins/monorepo';
import { createAlias, findNearestPackageData } 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 [
Expand All @@ -12,6 +11,14 @@ const buildTestPkgPath = (pkgRoot: string = process.cwd()) => {
]
}

describe('findNearestPackageData无法获取包', async () => {
test('findNearestPackageData无法获取包', () => {
const importerId = path.resolve('/', './not_exist')
const ret = findNearestPackageData(importerId);
expect(ret).toBeNull()
})
})

describe('【测试MonoRepoResolverPlugin】', () => {
test(`正常创建alias`, async () => {
const aliasConfig = createAlias({}, { command: 'serve', mode: 'test' }, {
Expand All @@ -29,18 +36,46 @@ describe('【测试MonoRepoResolverPlugin】', () => {
const customResolver = aliasConfig.customResolver as ResolverFunction

describe('测试customResolver解析组件路径', () => {
test(`正常解析testpkg/src/test.ts`, async () => {
test(`正常解析testpkg/src/test的形式`, async () => {
const [, importerId] = buildTestPkgPath()
expect(customResolver.call(null, 'src/test', importerId, null)).contains('test/plugins/testpkg/src/test.ts')
})

test(`正常解析testpkg/src/test.ts的形式`, async () => {
const [, importerId] = buildTestPkgPath()
expect(customResolver.call(null, 'src/test.ts', importerId, null)).contains('test/plugins/testpkg/src/test.ts')
})

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')
})

test('单个Vue文件的解析', async () => {
const [, importerId] = buildTestPkgPath()
expect(customResolver.call(null, 'src/SingleVue', importerId)).contains('test/plugins/testpkg/src/SingleVue.vue')
})

test('文件夹下index.vue文件的解析', async () => {
const [, importerId] = buildTestPkgPath()
expect(customResolver.call(null, 'src/justvue', importerId)).contains('test/plugins/testpkg/src/justvue/index.vue')
})

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 () => {

test(`找到不认识格式的组件`, async () => {
const [, importerId] = buildTestPkgPath()
expect(() => customResolver.call(null, 'src/notype', importerId)).toThrowError(/\/index.\(ts\|js\), please check it./)
})
})

describe('测试包内互相引用', () => {
test('测试test.ts中解析vue&ts的index', () => {
const [demoPkgPath] = buildTestPkgPath()
const importerId = path.resolve(demoPkgPath, './src/test.ts')
expect(customResolver.call(null, 'src/vue&ts', importerId)).contains('testpkg/src/vue&ts/index.ts')
})
})
Expand Down
Empty file.
3 changes: 3 additions & 0 deletions configs/vite/test/plugins/testpkg/src/justvue/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div></div>
</template>
3 changes: 3 additions & 0 deletions configs/vite/test/plugins/testpkg/src/notype/index
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{

}
1 change: 1 addition & 0 deletions configs/vite/test/plugins/testpkg/src/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import VueAndTs from '#/vue&ts'
6 changes: 6 additions & 0 deletions configs/vite/test/plugins/testpkg/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "tsconfig/base.json",
"include": ["src"],
"exclude": ["dist", "node_modules"],

}