From 902416f5761463aa71d8237593ff655d99b17b10 Mon Sep 17 00:00:00 2001 From: No Two <1244476905@qq.com> Date: Fri, 13 Dec 2024 13:50:47 +0800 Subject: [PATCH] feat: support to import the types from the `dirs` (#544) Co-authored-by: Anthony Fu --- README.md | 21 +++++++-- examples/vite-react/src/types/index.ts | 1 + examples/vite-react/src/views/PageA.tsx | 2 + examples/vite-react/src/views/PageB.tsx | 2 + package.json | 5 ++- pnpm-lock.yaml | 20 ++++++--- src/core/ctx.ts | 51 ++++++++------------- src/core/unplugin.ts | 4 +- src/types.ts | 26 ++++++++++- test/search.test.ts | 59 ++++++++++++++++++++++++- test/transform.test.ts | 6 +-- vitest.config.ts | 2 +- 12 files changed, 146 insertions(+), 53 deletions(-) create mode 100644 examples/vite-react/src/types/index.ts diff --git a/README.md b/README.md index c5b9065e..56ccf54a 100644 --- a/README.md +++ b/README.md @@ -273,12 +273,27 @@ AutoImport({ // Enable auto import by filename for default module exports under directories defaultExportByFilename: false, + // Options for scanning directories for auto import + dirsScanOptions: { + types: true // Enable auto import the types under the directories + }, + // Auto import for module exports under directories // by default it only scan one level of modules under the directory dirs: [ - // './hooks', - // './composables' // only root modules - // './composables/**', // all nested modules + './hooks', + './composables', // only root modules + './composables/**', // all nested modules + // ... + + { + glob: './hooks', + types: true // enable import the types + }, + { + glob: './composables', + types: false // If top level dirsScanOptions.types importing enabled, just only disable this directory + } // ... ], diff --git a/examples/vite-react/src/types/index.ts b/examples/vite-react/src/types/index.ts new file mode 100644 index 00000000..67a28161 --- /dev/null +++ b/examples/vite-react/src/types/index.ts @@ -0,0 +1 @@ +export type SpecialType = string diff --git a/examples/vite-react/src/views/PageA.tsx b/examples/vite-react/src/views/PageA.tsx index 04171138..baffa9cb 100644 --- a/examples/vite-react/src/views/PageA.tsx +++ b/examples/vite-react/src/views/PageA.tsx @@ -23,4 +23,6 @@ function PageA() { ) } +export type TypeA = number + export default PageA diff --git a/examples/vite-react/src/views/PageB.tsx b/examples/vite-react/src/views/PageB.tsx index a6a52744..a827ad44 100644 --- a/examples/vite-react/src/views/PageB.tsx +++ b/examples/vite-react/src/views/PageB.tsx @@ -32,4 +32,6 @@ function PageB() { ) } +export type TypeB = number + export default PageB diff --git a/package.json b/package.json index e4a5955d..ab92769b 100644 --- a/package.json +++ b/package.json @@ -165,10 +165,9 @@ "dependencies": { "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.3", - "fast-glob": "^3.3.2", "local-pkg": "^0.5.1", "magic-string": "^0.30.15", - "minimatch": "^9.0.5", + "picomatch": "^4.0.2", "unimport": "^3.14.5", "unplugin": "^2.1.0" }, @@ -178,11 +177,13 @@ "@nuxt/kit": "^3.14.1592", "@svgr/plugin-jsx": "^8.1.0", "@types/node": "^22.10.2", + "@types/picomatch": "^3.0.1", "@types/resolve": "^1.20.6", "@vueuse/metadata": "^12.0.0", "bumpp": "^9.9.1", "eslint": "^9.16.0", "esno": "^4.8.0", + "fast-glob": "^3.3.2", "publint": "^0.2.12", "rollup": "^4.28.1", "tsup": "^8.3.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8e6a91c..53a4dd88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,18 +17,15 @@ importers: '@vueuse/core': specifier: '*' version: 12.0.0(typescript@5.7.2) - fast-glob: - specifier: ^3.3.2 - version: 3.3.2 local-pkg: specifier: ^0.5.1 version: 0.5.1 magic-string: specifier: ^0.30.15 version: 0.30.15 - minimatch: - specifier: ^9.0.5 - version: 9.0.5 + picomatch: + specifier: ^4.0.2 + version: 4.0.2 unimport: specifier: ^3.14.5 version: 3.14.5(rollup@4.28.1) @@ -51,6 +48,9 @@ importers: '@types/node': specifier: ^22.10.2 version: 22.10.2 + '@types/picomatch': + specifier: ^3.0.1 + version: 3.0.1 '@types/resolve': specifier: ^1.20.6 version: 1.20.6 @@ -66,6 +66,9 @@ importers: esno: specifier: ^4.8.0 version: 4.8.0 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 publint: specifier: ^0.2.12 version: 0.2.12 @@ -1532,6 +1535,9 @@ packages: '@types/normalize-package-data@2.4.1': resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} + '@types/picomatch@3.0.1': + resolution: {integrity: sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==} + '@types/prop-types@15.7.5': resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} @@ -5898,6 +5904,8 @@ snapshots: '@types/normalize-package-data@2.4.1': {} + '@types/picomatch@3.0.1': {} + '@types/prop-types@15.7.5': {} '@types/react-dom@18.3.1': diff --git a/src/core/ctx.ts b/src/core/ctx.ts index 5b4f34b0..240b64e6 100644 --- a/src/core/ctx.ts +++ b/src/core/ctx.ts @@ -1,48 +1,29 @@ import type { Import, InlinePreset } from 'unimport' import type { BiomeLintrc, ESLintGlobalsPropValue, ESLintrc, ImportExtended, Options } from '../types' import { existsSync, promises as fs } from 'node:fs' -import { dirname, isAbsolute, join, relative, resolve } from 'node:path' +import { dirname, isAbsolute, relative, resolve } from 'node:path' import process from 'node:process' import { slash, throttle, toArray } from '@antfu/utils' import { createFilter } from '@rollup/pluginutils' -import fg from 'fast-glob' import { isPackageExists } from 'local-pkg' import MagicString from 'magic-string' -import { createUnimport, resolvePreset, scanExports } from 'unimport' +import { createUnimport, resolvePreset } from 'unimport' import { presets } from '../presets' import { generateBiomeLintConfigs } from './biomelintrc' import { generateESLintConfigs } from './eslintrc' import { resolversAddon } from './resolvers' -function resolveGlobsExclude(root: string, glob: string) { - const excludeReg = /^!/ - return `${excludeReg.test(glob) ? '!' : ''}${resolve(root, glob.replace(excludeReg, ''))}` -} - -async function scanDirExports(dirs: string[], root: string) { - const result = await fg(dirs, { - absolute: true, - cwd: root, - onlyFiles: true, - followSymbolicLinks: true, - }) - - const files = Array.from(new Set(result.flat())).map(slash) - return (await Promise.all(files.map(i => scanExports(i, false)))).flat() -} - export function createContext(options: Options = {}, root = process.cwd()) { root = slash(root) const { dts: preferDTS = isPackageExists('typescript'), + dirsScanOptions, + dirs, vueDirectives, vueTemplate, } = options - const dirs = options.dirs?.concat(options.dirs.map(dir => join(dir, '*.{tsx,jsx,ts,js,mjs,cjs,mts,cts}'))) - .map(dir => slash(resolveGlobsExclude(root, dir))) - const eslintrc: ESLintrc = options.eslintrc || {} eslintrc.enabled = eslintrc.enabled === undefined ? false : eslintrc.enabled eslintrc.filepath = eslintrc.filepath || './.eslintrc-auto-import.json' @@ -64,6 +45,11 @@ export function createContext(options: Options = {}, root = process.cwd()) { const unimport = createUnimport({ imports: [], presets: options.packagePresets?.map(p => typeof p === 'string' ? { package: p } : p) ?? [], + dirsScanOptions: { + ...dirsScanOptions, + cwd: root, + }, + dirs, injectAtEnd, parser: options.parser, addons: { @@ -266,16 +252,15 @@ ${dts}`.trim()}\n` } async function scanDirs() { - if (dirs?.length) { - await unimport.modifyDynamicImports(async (imports) => { - const exports_ = await scanDirExports(dirs, root) as ImportExtended[] - exports_.forEach(i => i.__source = 'dir') - return modifyDefaultExportsAlias([ - ...imports.filter((i: ImportExtended) => i.__source !== 'dir'), - ...exports_, - ], options) - }) - } + await unimport.modifyDynamicImports(async (imports) => { + const exports_ = await unimport.scanImportsFromDir() as ImportExtended[] + exports_.forEach(i => i.__source = 'dir') + return modifyDefaultExportsAlias([ + ...imports.filter((i: ImportExtended) => i.__source !== 'dir'), + ...exports_, + ], options) + }) + writeConfigFilesThrottled() } diff --git a/src/core/unplugin.ts b/src/core/unplugin.ts index e311914f..0e2361ce 100644 --- a/src/core/unplugin.ts +++ b/src/core/unplugin.ts @@ -1,7 +1,7 @@ import type { Options } from '../types' import { slash } from '@antfu/utils' import { isPackageExists } from 'local-pkg' -import { minimatch } from 'minimatch' +import pm from 'picomatch' import { createUnplugin } from 'unplugin' import { createContext } from './ctx' @@ -41,7 +41,7 @@ export default createUnplugin((options) => { } }, async handleHotUpdate({ file }) { - if (ctx.dirs?.some(glob => minimatch(slash(file), slash(glob)))) + if (ctx.dirs?.some(dir => pm.isMatch(slash(file), slash(typeof dir === 'string' ? dir : dir.glob)))) await ctx.scanDirs() }, async configResolved(config) { diff --git a/src/types.ts b/src/types.ts index 94d4add1..60ab70e6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,6 +51,25 @@ export type Resolver = ResolverFunction | ResolverResultObject */ export type ImportsMap = Record +export interface ScanDirExportsOptions { + /** + * Register type exports + * + * @default true + */ + types?: boolean +} + +/** + * Directory to search for import + */ +export interface ScanDir { + glob: string + types?: boolean +} + +export type NormalizedScanDir = Required + export type ESLintGlobalsPropValue = boolean | 'readonly' | 'readable' | 'writable' | 'writeable' export interface ESLintrc { @@ -118,10 +137,15 @@ export interface Options { */ injectAtEnd?: boolean + /** + * Options for scanning directories for auto import + */ + dirsScanOptions?: ScanDirExportsOptions + /** * Path for directories to be auto imported */ - dirs?: string[] + dirs?: (string | ScanDir)[] /** * Pass a custom function to resolve the component importing path from the component name. diff --git a/test/search.test.ts b/test/search.test.ts index 8f836b4a..56255a84 100644 --- a/test/search.test.ts +++ b/test/search.test.ts @@ -1,5 +1,5 @@ import { resolve } from 'node:path' -import { describe, it } from 'vitest' +import { describe, expect, it } from 'vitest' import { createContext } from '../src/core/ctx' const root = resolve(__dirname, '../examples/vite-react') @@ -19,7 +19,7 @@ describe('search', () => { expect(data).toContain('PageB') }) - it('should dir excude work', async () => { + it('should dir exclude work', async () => { const ctx = createContext({ dts: false, dirs: [ @@ -34,3 +34,58 @@ describe('search', () => { expect(data).not.toContain('PageB') }) }) + +describe('import the types from the dirs', () => { + it('should top level types enable work', async () => { + const ctx = createContext({ + dts: false, + dirsScanOptions: { types: true }, + dirs: ['src/**'], + }, root) + + await ctx.scanDirs() + const data = await ctx.generateDTS('') + expect(data).toContain('TypeA') + expect(data).toContain('TypeB') + expect(data).toContain('SpecialType') + }) + + it('should specific dirs types enable work', async () => { + const ctx = createContext({ + dts: false, + dirsScanOptions: { types: true }, + dirs: [ + { + glob: 'src/views', + types: true, + }, + ], + }, root) + + await ctx.scanDirs() + const data = await ctx.generateDTS('') + expect(data).toContain('TypeA') + expect(data).toContain('TypeB') + expect(data).not.toContain('SpecialType') + }) + + it('should specific dirs types disable work', async () => { + const ctx = createContext({ + dts: false, + dirsScanOptions: { types: true }, + dirs: [ + 'src/types', + { + glob: 'src/views', + types: false, + }, + ], + }, root) + + await ctx.scanDirs() + const data = await ctx.generateDTS('') + expect(data).not.toContain('TypeA') + expect(data).not.toContain('TypeB') + expect(data).toContain('SpecialType') + }) +}) diff --git a/test/transform.test.ts b/test/transform.test.ts index 7f96fca2..514999ad 100644 --- a/test/transform.test.ts +++ b/test/transform.test.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'node:fs' import { resolve } from 'node:path' -import fg from 'fast-glob' +import glob from 'fast-glob' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import { describe, expect, it } from 'vitest' import { createContext } from '../src/core/ctx' @@ -61,7 +61,7 @@ describe('transform', async () => { }) const root = resolve(__dirname, 'fixtures') - const files = await fg('*', { + const files = await glob('*', { cwd: root, onlyFiles: true, }) @@ -85,7 +85,7 @@ describe('transform-vue-macro', async () => { }) const root = resolve(__dirname, 'fixtures-vue-macro') - const files = await fg('*', { + const files = await glob('*', { cwd: root, onlyFiles: true, }) diff --git a/vitest.config.ts b/vitest.config.ts index 5b624a8c..127b3d40 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite' +import { defineConfig } from 'vitest/config' import AutoImport from './src/vite' export default defineConfig({