diff --git a/apps/admin/src/hooks/web/useLockScreen.ts b/apps/admin/src/hooks/web/useLockScreen.ts index a537ed28..a6e70168 100644 --- a/apps/admin/src/hooks/web/useLockScreen.ts +++ b/apps/admin/src/hooks/web/useLockScreen.ts @@ -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' diff --git a/configs/vite/package.json b/configs/vite/package.json index ce1817cc..f888f284 100644 --- a/configs/vite/package.json +++ b/configs/vite/package.json @@ -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", diff --git a/configs/vite/src/plugins/monorepo.ts b/configs/vite/src/plugins/monorepo.ts index 3b4aafab..a189769d 100644 --- a/configs/vite/src/plugins/monorepo.ts +++ b/configs/vite/src/plugins/monorepo.ts @@ -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 +/** Cache for package.json resolution and package.json contents */ +export type PackageCache = Map +export interface PackageData { + dir: string + componentCache: TreeNode + data: { + [field: string]: any + name: string + type: string + version: string + main: string + module: string + browser: string | Record + exports: string | Record | string[] + imports: Record + dependencies: Record + } +} + +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(), + 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) + } +} + + /** * 创建别名解析规则 * @@ -16,69 +134,77 @@ export type Options = Omit * @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}`) + 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`) } diff --git a/configs/vite/test/plugins/monorepo.test.ts b/configs/vite/test/plugins/monorepo.test.ts index 4e7b4b0b..3e857a5a 100644 --- a/configs/vite/test/plugins/monorepo.test.ts +++ b/configs/vite/test/plugins/monorepo.test.ts @@ -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 [ @@ -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' }, { @@ -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') }) }) diff --git a/configs/vite/test/plugins/testpkg/src/SingleVue.vue b/configs/vite/test/plugins/testpkg/src/SingleVue.vue new file mode 100644 index 00000000..e69de29b diff --git a/configs/vite/test/plugins/testpkg/src/justvue/index.vue b/configs/vite/test/plugins/testpkg/src/justvue/index.vue new file mode 100644 index 00000000..e5e6a79d --- /dev/null +++ b/configs/vite/test/plugins/testpkg/src/justvue/index.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/configs/vite/test/plugins/testpkg/src/notype/index b/configs/vite/test/plugins/testpkg/src/notype/index new file mode 100644 index 00000000..077404aa --- /dev/null +++ b/configs/vite/test/plugins/testpkg/src/notype/index @@ -0,0 +1,3 @@ +{ + +} \ 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 index e69de29b..6c8009c0 100644 --- a/configs/vite/test/plugins/testpkg/src/test.ts +++ b/configs/vite/test/plugins/testpkg/src/test.ts @@ -0,0 +1 @@ +import VueAndTs from '#/vue&ts' \ No newline at end of file diff --git a/configs/vite/test/plugins/testpkg/tsconfig.json b/configs/vite/test/plugins/testpkg/tsconfig.json new file mode 100644 index 00000000..10ad7ca6 --- /dev/null +++ b/configs/vite/test/plugins/testpkg/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "tsconfig/base.json", + "include": ["src"], + "exclude": ["dist", "node_modules"], + +}