From 32673307509175679a43bac2cf675fd2bd72075d Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Thu, 8 May 2025 14:57:39 +0000 Subject: [PATCH 1/4] feat: enhance TypeScript declaration generation --- src/core/context.ts | 2 +- src/core/declaration.ts | 138 ++++++++++++++++++++-------- src/core/options.ts | 20 ++-- src/core/unplugin.ts | 3 +- src/types.ts | 10 +- test/__snapshots__/dts.test.ts.snap | 24 +++-- test/dts.test.ts | 21 +++-- 7 files changed, 148 insertions(+), 70 deletions(-) diff --git a/src/core/context.ts b/src/core/context.ts index 499a9a3b..b654acfb 100644 --- a/src/core/context.ts +++ b/src/core/context.ts @@ -301,7 +301,7 @@ export class Context { return debug.declaration('generating dts') - return writeDeclaration(this, this.options.dts, removeUnused) + return writeDeclaration(this, removeUnused) } generateDeclaration(removeUnused = !this._server): void { diff --git a/src/core/declaration.ts b/src/core/declaration.ts index 4fd2c168..2240bf20 100644 --- a/src/core/declaration.ts +++ b/src/core/declaration.ts @@ -1,4 +1,4 @@ -import type { ComponentInfo, Options } from '../types' +import type { ComponentInfo, DtsConfigure, DtsDeclarationType, Options } from '../types' import type { Context } from './context' import { existsSync } from 'node:fs' import { mkdir, readFile, writeFile as writeFile_ } from 'node:fs/promises' @@ -39,31 +39,48 @@ export function parseDeclaration(code: string): DeclarationImports | undefined { } /** - * Converts `ComponentInfo` to an array + * Converts `ComponentInfo` to an import info. * - * `[name, "typeof import(path)[importName]"]` + * `{name, entry: "typeof import(path)[importName]", filepath}` */ -function stringifyComponentInfo(filepath: string, { from: path, as: name, name: importName }: ComponentInfo, importPathTransform?: Options['importPathTransform']): [string, string] | undefined { +function stringifyComponentInfo(dts: DtsConfigure, info: ComponentInfo, declarationType: DtsDeclarationType, importPathTransform?: Options['importPathTransform']): Record<'name' | 'entry' | 'filepath', string> | undefined { + const { from: path, as: name, name: importName } = info + if (!name) return undefined - path = getTransformedPath(path, importPathTransform) - const related = isAbsolute(path) - ? `./${relative(dirname(filepath), path)}` - : path + + const filepath = dts(info, declarationType) + if (!filepath) + return undefined + + const transformedPath = getTransformedPath(path, importPathTransform) + const related = isAbsolute(transformedPath) + ? `./${relative(dirname(filepath), transformedPath)}` + : transformedPath const entry = `typeof import('${slash(related)}')['${importName || 'default'}']` - return [name, entry] + return { name, entry, filepath } } /** - * Converts array of `ComponentInfo` to an import map + * Converts array of `ComponentInfo` to a filepath grouped import map. * - * `{ name: "typeof import(path)[importName]", ... }` + * `{ filepath: { name: "typeof import(path)[importName]", ... } }` */ -export function stringifyComponentsInfo(filepath: string, components: ComponentInfo[], importPathTransform?: Options['importPathTransform']): Record { - return Object.fromEntries( - components.map(info => stringifyComponentInfo(filepath, info, importPathTransform)) - .filter(notNullish), - ) +export function stringifyComponentsInfo(dts: DtsConfigure, components: ComponentInfo[], declarationType: DtsDeclarationType, importPathTransform?: Options['importPathTransform']): Record> { + const stringified = components.map(info => stringifyComponentInfo(dts, info, declarationType, importPathTransform)).filter(notNullish) + + const filepathMap: Record> = {} + + for (const info of stringified) { + const { name, entry, filepath } = info + + if (!filepathMap[filepath]) + filepathMap[filepath] = {} + + filepathMap[filepath][name] = entry + } + + return filepathMap } export interface DeclarationImports { @@ -71,27 +88,55 @@ export interface DeclarationImports { directive: Record } -export function getDeclarationImports(ctx: Context, filepath: string): DeclarationImports | undefined { - const component = stringifyComponentsInfo(filepath, [ +export function getDeclarationImports(ctx: Context): Record | undefined { + if (!ctx.options.dts) + return undefined + + const componentMap = stringifyComponentsInfo(ctx.options.dts, [ ...Object.values({ ...ctx.componentNameMap, ...ctx.componentCustomMap, }), ...resolveTypeImports(ctx.options.types), - ], ctx.options.importPathTransform) + ], 'component', ctx.options.importPathTransform) - const directive = stringifyComponentsInfo( - filepath, + const directiveMap = stringifyComponentsInfo( + ctx.options.dts, Object.values(ctx.directiveCustomMap), + 'directive', ctx.options.importPathTransform, ) - if ( - (Object.keys(component).length + Object.keys(directive).length) === 0 - ) - return + const declarationMap: Record = {} + + for (const [filepath, component] of Object.entries(componentMap)) { + if (!declarationMap[filepath]) + declarationMap[filepath] = { component: {}, directive: {} } + + declarationMap[filepath].component = { + ...declarationMap[filepath].component, + ...component, + } + } - return { component, directive } + for (const [filepath, directive] of Object.entries(directiveMap)) { + if (!declarationMap[filepath]) + declarationMap[filepath] = { component: {}, directive: {} } + + declarationMap[filepath].directive = { + ...declarationMap[filepath].directive, + ...directive, + } + } + + for (const [filepath, { component, directive }] of Object.entries(declarationMap)) { + if ( + (Object.keys(component).length + Object.keys(directive).length) === 0 + ) + delete declarationMap[filepath] + } + + return declarationMap } export function stringifyDeclarationImports(imports: Record) { @@ -104,11 +149,7 @@ export function stringifyDeclarationImports(imports: Record) { }) } -export function getDeclaration(ctx: Context, filepath: string, originalImports?: DeclarationImports) { - const imports = getDeclarationImports(ctx, filepath) - if (!imports) - return - +function getDeclaration(imports: DeclarationImports, originalImports?: DeclarationImports): string { const declarations = { component: stringifyDeclarationImports({ ...originalImports?.component, ...imports.component }), directive: stringifyDeclarationImports({ ...originalImports?.directive, ...imports.directive }), @@ -140,21 +181,40 @@ declare module 'vue' {` return code } +export async function getDeclarations(ctx: Context, removeUnused: boolean): Promise | undefined> { + const importsMap = getDeclarationImports(ctx) + if (!importsMap || !Object.keys(importsMap).length) + return undefined + + const results = await Promise.all(Object.entries(importsMap).map(async ([filepath, imports]) => { + const originalContent = existsSync(filepath) ? await readFile(filepath, 'utf-8') : '' + const originalImports = removeUnused ? undefined : parseDeclaration(originalContent) + + const code = getDeclaration(imports, originalImports) + + if (code !== originalContent) { + return [filepath, code] + } + })) + + return Object.fromEntries(results.filter(notNullish)) +} + async function writeFile(filePath: string, content: string) { await mkdir(dirname(filePath), { recursive: true }) return await writeFile_(filePath, content, 'utf-8') } -export async function writeDeclaration(ctx: Context, filepath: string, removeUnused = false) { - const originalContent = existsSync(filepath) ? await readFile(filepath, 'utf-8') : '' - const originalImports = removeUnused ? undefined : parseDeclaration(originalContent) - - const code = getDeclaration(ctx, filepath, originalImports) - if (!code) +export async function writeDeclaration(ctx: Context, removeUnused = false) { + const declarations = await getDeclarations(ctx, removeUnused) + if (!declarations || !Object.keys(declarations).length) return - if (code !== originalContent) - await writeFile(filepath, code) + await Promise.all( + Object.entries(declarations).map(async ([filepath, code]) => { + return writeFile(filepath, code) + }), + ) } export async function writeComponentsJson(ctx: Context, _removeUnused = false) { diff --git a/src/core/options.ts b/src/core/options.ts index 58ba6239..ee0d2cee 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -1,4 +1,4 @@ -import type { ComponentResolver, ComponentResolverObject, Options, ResolvedOptions } from '../types' +import type { ComponentResolver, ComponentResolverObject, DtsConfigure, Options, ResolvedOptions } from '../types' import { join, resolve } from 'node:path' import { slash, toArray } from '@antfu/utils' import { getPackageInfoSync, isPackageExists } from 'local-pkg' @@ -21,6 +21,8 @@ export const defaultOptions: Omit, 'include' | 'exclude' | 'ex importPathTransform: v => v, allowOverrides: false, + sourcemap: true, + dumpComponentsInfo: false, } function normalizeResolvers(resolvers: (ComponentResolver | ComponentResolver[])[]): ComponentResolverObject[] { @@ -78,14 +80,16 @@ export function resolveOptions(options: Options, root: string): ResolvedOptions return false }) - resolved.dts = !resolved.dts + const originalDts = resolved.dts + + resolved.dts = !originalDts ? false - : resolve( - root, - typeof resolved.dts === 'string' - ? resolved.dts - : 'components.d.ts', - ) + : ((...args) => { + const res = typeof originalDts === 'function' ? originalDts(...args) : originalDts + if (!res) + return false + return resolve(root, typeof res === 'string' ? res : 'components.d.ts') + }) as DtsConfigure if (!resolved.types && resolved.dts) resolved.types = detectTypeImports() diff --git a/src/core/unplugin.ts b/src/core/unplugin.ts index 39616787..084d5256 100644 --- a/src/core/unplugin.ts +++ b/src/core/unplugin.ts @@ -66,8 +66,7 @@ export default createUnplugin((options = {}) => { if (ctx.options.dts) { ctx.searchGlob() - if (!existsSync(ctx.options.dts)) - ctx.generateDeclaration() + ctx.generateDeclaration() } if (ctx.options.dumpComponentsInfo && ctx.dumpComponentsInfoPath) { diff --git a/src/types.ts b/src/types.ts index ac17d1e3..8ba20dc9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -46,6 +46,10 @@ export type Transformer = (code: string, id: string, path: string, query: Record export type SupportedTransformer = 'vue3' | 'vue2' +export type DtsDeclarationType = 'component' | 'directive' + +export type DtsConfigure = (info: ComponentInfo, declarationType: DtsDeclarationType) => string | false + export interface PublicPluginAPI { /** * Resolves a component using the configured resolvers. @@ -163,13 +167,13 @@ export interface Options { /** * Generate TypeScript declaration for global components * - * Accept boolean or a path related to project root + * Accept boolean, a path related to project root or a function that returns boolean or a path. * * @see https://github.com/vuejs/core/pull/3399 * @see https://github.com/johnsoncodehk/volar#using * @default true */ - dts?: boolean | string + dts?: boolean | string | DtsConfigure /** * Do not emit warning on component overriding @@ -227,7 +231,7 @@ export type ResolvedOptions = Omit< resolvedDirs: string[] globs: string[] globsExclude: string[] - dts: string | false + dts: false | DtsConfigure root: string } diff --git a/test/__snapshots__/dts.test.ts.snap b/test/__snapshots__/dts.test.ts.snap index 75752379..51c69003 100644 --- a/test/__snapshots__/dts.test.ts.snap +++ b/test/__snapshots__/dts.test.ts.snap @@ -1,7 +1,8 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`dts > components only 1`] = ` -"/* eslint-disable */ +[ + "/* eslint-disable */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 @@ -16,11 +17,13 @@ declare module 'vue' { TestComp: typeof import('test/component/TestComp')['default'] } } -" +", +] `; exports[`dts > directive only 1`] = ` -"/* eslint-disable */ +[ + "/* eslint-disable */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 @@ -33,11 +36,13 @@ declare module 'vue' { vLoading: typeof import('test/directive/Loading')['default'] } } -" +", +] `; exports[`dts > getDeclaration 1`] = ` -"/* eslint-disable */ +[ + "/* eslint-disable */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 @@ -55,7 +60,8 @@ declare module 'vue' { vLoading: typeof import('test/directive/Loading')['default'] } } -" +", +] `; exports[`dts > parseDeclaration - has icon component like 1`] = ` @@ -96,7 +102,8 @@ exports[`dts > parseDeclaration 1`] = ` `; exports[`dts > vue 2.7 components only 1`] = ` -"/* eslint-disable */ +[ + "/* eslint-disable */ // @ts-nocheck // Generated by unplugin-vue-components // Read more: https://github.com/vuejs/core/pull/3399 @@ -111,7 +118,8 @@ declare module 'vue' { TestComp: typeof import('test/component/TestComp')['default'] } } -" +", +] `; exports[`dts > writeDeclaration - keep unused 1`] = ` diff --git a/test/dts.test.ts b/test/dts.test.ts index b26ccba4..edc689b7 100644 --- a/test/dts.test.ts +++ b/test/dts.test.ts @@ -3,7 +3,7 @@ import { readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import { describe, expect, it } from 'vitest' import { Context } from '../src/core/context' -import { getDeclaration, parseDeclaration } from '../src/core/declaration' +import { getDeclarations, parseDeclaration } from '../src/core/declaration' const resolver: ComponentResolver[] = [ { @@ -27,8 +27,8 @@ const _component_test_comp = _resolveComponent("test-comp") const _directive_loading = _resolveDirective("loading")` await ctx.transform(code, '') - const declarations = getDeclaration(ctx, 'test.d.ts') - expect(declarations).toMatchSnapshot() + const declarations = await getDeclarations(ctx, false) + expect(Object.values(declarations ?? {})).toMatchSnapshot() }) it('writeDeclaration', async () => { @@ -87,38 +87,41 @@ const _directive_loading = _resolveDirective("loading")` const ctx = new Context({ resolvers: resolver, directives: true, + dts: 'test.d.ts', }) const code = 'const _component_test_comp = _resolveComponent("test-comp")' await ctx.transform(code, '') - const declarations = getDeclaration(ctx, 'test.d.ts') - expect(declarations).toMatchSnapshot() + const declarations = await getDeclarations(ctx, false) + expect(Object.values(declarations ?? {})).toMatchSnapshot() }) it('vue 2.7 components only', async () => { const ctx = new Context({ resolvers: resolver, directives: true, + dts: 'test.d.ts', version: 2.7, }) const code = 'const _component_test_comp = _c("test-comp")' await ctx.transform(code, '') - const declarations = getDeclaration(ctx, 'test.d.ts') - expect(declarations).toMatchSnapshot() + const declarations = await getDeclarations(ctx, false) + expect(Object.values(declarations ?? {})).toMatchSnapshot() }) it('directive only', async () => { const ctx = new Context({ resolvers: resolver, directives: true, + dts: 'test.d.ts', types: [], }) const code = 'const _directive_loading = _resolveDirective("loading")' await ctx.transform(code, '') - const declarations = getDeclaration(ctx, 'test.d.ts') - expect(declarations).toMatchSnapshot() + const declarations = await getDeclarations(ctx, false) + expect(Object.values(declarations ?? {})).toMatchSnapshot() }) it('parseDeclaration', async () => { From b66b287d82654e89286d2a67e566628540488fd9 Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 May 2025 01:59:49 +0000 Subject: [PATCH 2/4] feat: set paramter default value for getDeclarations --- src/core/declaration.ts | 2 +- test/dts.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/declaration.ts b/src/core/declaration.ts index 2240bf20..608ebdd4 100644 --- a/src/core/declaration.ts +++ b/src/core/declaration.ts @@ -181,7 +181,7 @@ declare module 'vue' {` return code } -export async function getDeclarations(ctx: Context, removeUnused: boolean): Promise | undefined> { +export async function getDeclarations(ctx: Context, removeUnused = false): Promise | undefined> { const importsMap = getDeclarationImports(ctx) if (!importsMap || !Object.keys(importsMap).length) return undefined diff --git a/test/dts.test.ts b/test/dts.test.ts index edc689b7..594a5327 100644 --- a/test/dts.test.ts +++ b/test/dts.test.ts @@ -27,7 +27,7 @@ const _component_test_comp = _resolveComponent("test-comp") const _directive_loading = _resolveDirective("loading")` await ctx.transform(code, '') - const declarations = await getDeclarations(ctx, false) + const declarations = await getDeclarations(ctx) expect(Object.values(declarations ?? {})).toMatchSnapshot() }) @@ -92,7 +92,7 @@ const _directive_loading = _resolveDirective("loading")` const code = 'const _component_test_comp = _resolveComponent("test-comp")' await ctx.transform(code, '') - const declarations = await getDeclarations(ctx, false) + const declarations = await getDeclarations(ctx) expect(Object.values(declarations ?? {})).toMatchSnapshot() }) @@ -106,7 +106,7 @@ const _directive_loading = _resolveDirective("loading")` const code = 'const _component_test_comp = _c("test-comp")' await ctx.transform(code, '') - const declarations = await getDeclarations(ctx, false) + const declarations = await getDeclarations(ctx) expect(Object.values(declarations ?? {})).toMatchSnapshot() }) @@ -120,7 +120,7 @@ const _directive_loading = _resolveDirective("loading")` const code = 'const _directive_loading = _resolveDirective("loading")' await ctx.transform(code, '') - const declarations = await getDeclarations(ctx, false) + const declarations = await getDeclarations(ctx) expect(Object.values(declarations ?? {})).toMatchSnapshot() }) From 035343c2471d64b2cf551eba060b6e021eeb225b Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 May 2025 02:31:00 +0000 Subject: [PATCH 3/4] feat: add tests for getDeclaration and writeDeclaration with multiple files support --- test/__snapshots__/dts.test.ts.snap | 140 ++++++++++++++++++++++++++++ test/dts.test.ts | 128 ++++++++++++++++++++++++- 2 files changed, 266 insertions(+), 2 deletions(-) diff --git a/test/__snapshots__/dts.test.ts.snap b/test/__snapshots__/dts.test.ts.snap index 51c69003..60de08e1 100644 --- a/test/__snapshots__/dts.test.ts.snap +++ b/test/__snapshots__/dts.test.ts.snap @@ -40,6 +40,110 @@ declare module 'vue' { ] `; +exports[`dts > getDeclaration - filter 1`] = ` +[ + "/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + TestComp: typeof import('test/component/TestComp')['default'] + } +} +", +] +`; + +exports[`dts > getDeclaration - function expression 1`] = ` +[ + "/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + TestComp: typeof import('test/component/TestComp')['default'] + } + export interface ComponentCustomProperties { + vLoading: typeof import('test/directive/Loading')['default'] + } +} +", +] +`; + +exports[`dts > getDeclaration - multiple files 1`] = ` +[ + "/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + TestComp: typeof import('test/component/TestComp')['default'] + } +} +", + "/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface ComponentCustomProperties { + vLoading: typeof import('test/directive/Loading')['default'] + } +} +", +] +`; + +exports[`dts > getDeclaration - return absolute path 1`] = ` +[ + "/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + TestComp: typeof import('test/component/TestComp')['default'] + } + export interface ComponentCustomProperties { + vLoading: typeof import('test/directive/Loading')['default'] + } +} +", +] +`; + exports[`dts > getDeclaration 1`] = ` [ "/* eslint-disable */ @@ -147,6 +251,42 @@ declare module 'vue' { " `; +exports[`dts > writeDeclaration - multiple files 1`] = ` +"/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + TestComp: typeof import('test/component/TestComp')['default'] + } +} +" +`; + +exports[`dts > writeDeclaration - multiple files 2`] = ` +"/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface ComponentCustomProperties { + vLoading: typeof import('test/directive/Loading')['default'] + } +} +" +`; + exports[`dts > writeDeclaration 1`] = ` "/* eslint-disable */ // @ts-nocheck diff --git a/test/dts.test.ts b/test/dts.test.ts index 594a5327..f8b78e36 100644 --- a/test/dts.test.ts +++ b/test/dts.test.ts @@ -1,7 +1,7 @@ -import type { ComponentResolver } from '../src' +import type { ComponentInfo, ComponentResolver, DtsDeclarationType } from '../src' import { readFile, writeFile } from 'node:fs/promises' import path from 'node:path' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Context } from '../src/core/context' import { getDeclarations, parseDeclaration } from '../src/core/declaration' @@ -31,6 +31,110 @@ const _directive_loading = _resolveDirective("loading")` expect(Object.values(declarations ?? {})).toMatchSnapshot() }) + it('getDeclaration - function expression', async () => { + const ctx = new Context({ + resolvers: resolver, + directives: true, + dts: () => 'test.d.ts', + }) + + const filepath = path.resolve(__dirname, '../test.d.ts') + + const code = ` +const _component_test_comp = _resolveComponent("test-comp") +const _directive_loading = _resolveDirective("loading")` + await ctx.transform(code, '') + + const declarations = await getDeclarations(ctx) + + expect(Object.keys(declarations ?? {})).toEqual([filepath]) + expect(Object.values(declarations ?? {})).toMatchSnapshot() + }) + + it('getDeclaration - return absolute path', async () => { + const filepath = path.resolve(__dirname, 'test.d.ts') + + const ctx = new Context({ + resolvers: resolver, + directives: true, + dts: () => filepath, + }) + + const code = ` +const _component_test_comp = _resolveComponent("test-comp") +const _directive_loading = _resolveDirective("loading")` + await ctx.transform(code, '') + + const declarations = await getDeclarations(ctx) + + expect(Object.keys(declarations ?? {})).toEqual([filepath]) + expect(Object.values(declarations ?? {})).toMatchSnapshot() + }) + + it('getDeclaration - return false', async () => { + const ctx = new Context({ + resolvers: resolver, + directives: true, + dts: () => false, + }) + + const code = ` +const _component_test_comp = _resolveComponent("test-comp") +const _directive_loading = _resolveDirective("loading")` + await ctx.transform(code, '') + const declarations = await getDeclarations(ctx) + expect(declarations).toBeUndefined() + }) + + it('getDeclaration - multiple files', async () => { + const fn = vi.fn().mockImplementation((_info: ComponentInfo, type: DtsDeclarationType) => { + return type === 'component' ? 'test.d.ts' : 'test2.d.ts' + }) + + const ctx = new Context({ + resolvers: resolver, + directives: true, + dts: fn, + }) + + const filepath = path.resolve(__dirname, '../test.d.ts') + const filepath2 = path.resolve(__dirname, '../test2.d.ts') + + const code = ` +const _component_test_comp = _resolveComponent("test-comp") +const _directive_loading = _resolveDirective("loading")` + + await ctx.transform(code, '') + + const declarations = await getDeclarations(ctx) + + expect(fn).toBeCalledTimes(4) + expect(fn).toBeCalledWith({ as: 'TestComp', from: 'test/component/TestComp' } satisfies ComponentInfo, 'component') + expect(fn).toBeCalledWith({ as: 'vLoading', from: 'test/directive/Loading' } satisfies ComponentInfo, 'directive') + expect(fn).toBeCalledWith({ from: 'vue-router', name: 'RouterView', as: 'RouterView' } satisfies ComponentInfo, 'component') + expect(fn).toBeCalledWith({ from: 'vue-router', name: 'RouterLink', as: 'RouterLink' } satisfies ComponentInfo, 'component') + + expect(Object.keys(declarations ?? {})).toEqual([filepath, filepath2]) + expect(Object.values(declarations ?? {})).toMatchSnapshot() + }) + + it('getDeclaration - filter', async () => { + const ctx = new Context({ + resolvers: resolver, + directives: true, + dts: (_, type) => type === 'component' ? 'test.d.ts' : false, + }) + + const code = ` +const _component_test_comp = _resolveComponent("test-comp") +const _directive_loading = _resolveDirective("loading")` + await ctx.transform(code, '') + + const declarations = await getDeclarations(ctx) + + expect(Object.values(declarations ?? {})).toMatchSnapshot() + }) + it('writeDeclaration', async () => { const filepath = path.resolve(__dirname, 'tmp/dts-test.d.ts') const ctx = new Context({ @@ -83,6 +187,26 @@ const _directive_loading = _resolveDirective("loading")` expect(contents).toContain('vSome') }) + it('writeDeclaration - multiple files', async () => { + const filepath = path.resolve(__dirname, 'tmp/dts-test.d.ts') + const filepath2 = path.resolve(__dirname, 'tmp/dts-test2.d.ts') + + const ctx = new Context({ + resolvers: resolver, + directives: true, + dts: (_, type) => (type === 'component' ? filepath : filepath2), + }) + + const code = ` +const _component_test_comp = _resolveComponent("test-comp") +const _directive_loading = _resolveDirective("loading")` + await ctx.transform(code, '') + await ctx._generateDeclaration() + + expect(await readFile(filepath, 'utf-8')).matchSnapshot() + expect(await readFile(filepath2, 'utf-8')).matchSnapshot() + }) + it('components only', async () => { const ctx = new Context({ resolvers: resolver, From 5f090190ebcca2b9296972f719cb30a784eb4150 Mon Sep 17 00:00:00 2001 From: Xingwang Liao Date: Fri, 9 May 2025 02:39:55 +0000 Subject: [PATCH 4/4] docs: update docs for new dts option --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdf4dfe4..ec1c3ac3 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,22 @@ Once the setup is done, a `components.d.ts` will be generated and updates automa > **Make sure you also add `components.d.ts` to your `tsconfig.json` under `include`.** +We also provide a way to generate multiple `d.ts` files for components or directives. You can pass a function to `dts` option, which will be called with the component info and type. You can return a string or a boolean to indicate whether to generate it to a file or not. + +```ts +Components({ + dts: (componentInfo, type) => { + if (type === 'component') { + return 'components.d.ts' + } + else if (type === 'directive') { + return 'directives.d.ts' + } + return false + }, +}) +``` + ## Importing from UI Libraries We have several built-in resolvers for popular UI libraries like **Vuetify**, **Ant Design Vue**, and **Element Plus**, where you can enable them by: @@ -371,7 +387,7 @@ Components({ resolvers: [], // generate `components.d.ts` global declarations, - // also accepts a path for custom filename + // also accepts a path, a custom filename or a function that returns a path or a boolean // default: `true` if package typescript is installed dts: false,