Skip to content

Commit 0d0a939

Browse files
authored
feat: supports directive dts (#442)
1 parent f161f50 commit 0d0a939

File tree

6 files changed

+315
-67
lines changed

6 files changed

+315
-67
lines changed

src/core/context.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { DIRECTIVE_IMPORT_PREFIX } from './constants'
88
import { getNameFromFilePath, matchGlobs, normalizeComponetInfo, parseId, pascalCase, resolveAlias } from './utils'
99
import { resolveOptions } from './options'
1010
import { searchComponents } from './fs/glob'
11-
import { generateDeclaration } from './declaration'
11+
import { writeDeclaration } from './declaration'
1212
import transformer from './transformer'
1313

1414
const debug = {
@@ -27,6 +27,7 @@ export class Context {
2727
private _componentNameMap: Record<string, ComponentInfo> = {}
2828
private _componentUsageMap: Record<string, Set<string>> = {}
2929
private _componentCustomMap: Record<string, ComponentInfo> = {}
30+
private _directiveCustomMap: Record<string, ComponentInfo> = {}
3031
private _server: ViteDevServer | undefined
3132

3233
root = process.cwd()
@@ -37,7 +38,10 @@ export class Context {
3738
private rawOptions: Options,
3839
) {
3940
this.options = resolveOptions(rawOptions, this.root)
40-
this.generateDeclaration = throttle(500, false, this.generateDeclaration.bind(this))
41+
this.generateDeclaration
42+
= throttle(500, false, this._generateDeclaration.bind(this)) as
43+
// `throttle` will omit return value.
44+
((removeUnused?: boolean) => void)
4145
this.setTransformer(this.options.transformer)
4246
}
4347

@@ -147,6 +151,11 @@ export class Context {
147151
this._componentCustomMap[info.as] = info
148152
}
149153

154+
addCustomDirectives(info: ComponentInfo) {
155+
if (info.as)
156+
this._directiveCustomMap[info.as] = info
157+
}
158+
150159
removeComponents(paths: string | string[]) {
151160
debug.components('remove', paths)
152161

@@ -220,24 +229,26 @@ export class Context {
220229
continue
221230

222231
const result = await resolver.resolve(type === 'directive' ? name.slice(DIRECTIVE_IMPORT_PREFIX.length) : name)
223-
if (result) {
224-
if (typeof result === 'string') {
225-
info = {
226-
as: name,
227-
from: result,
228-
}
229-
this.addCustomComponents(info)
230-
return info
232+
if (!result)
233+
continue
234+
235+
if (typeof result === 'string') {
236+
info = {
237+
as: name,
238+
from: result,
231239
}
232-
else {
233-
info = {
234-
as: name,
235-
...normalizeComponetInfo(result),
236-
}
237-
this.addCustomComponents(info)
238-
return info
240+
}
241+
else {
242+
info = {
243+
as: name,
244+
...normalizeComponetInfo(result),
239245
}
240246
}
247+
if (type === 'component')
248+
this.addCustomComponents(info)
249+
else if (type === 'directive')
250+
this.addCustomDirectives(info)
251+
return info
241252
}
242253

243254
return undefined
@@ -260,9 +271,6 @@ export class Context {
260271
* This search for components in with the given options.
261272
* Will be called multiple times to ensure file loaded,
262273
* should normally run only once.
263-
*
264-
* @param ctx
265-
* @param force
266274
*/
267275
searchGlob() {
268276
if (this._searched)
@@ -273,19 +281,25 @@ export class Context {
273281
this._searched = true
274282
}
275283

276-
generateDeclaration() {
284+
_generateDeclaration(removeUnused = !this._server) {
277285
if (!this.options.dts)
278286
return
279287

280288
debug.decleration('generating')
281-
generateDeclaration(this, this.options.root, this.options.dts, !this._server)
289+
return writeDeclaration(this, this.options.dts, removeUnused)
282290
}
283291

292+
generateDeclaration
293+
284294
get componentNameMap() {
285295
return this._componentNameMap
286296
}
287297

288298
get componentCustomMap() {
289299
return this._componentCustomMap
290300
}
301+
302+
get directiveCustomMap() {
303+
return this._directiveCustomMap
304+
}
291305
}

src/core/declaration.ts

Lines changed: 117 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,153 @@
11
import { dirname, isAbsolute, relative } from 'path'
2-
import { existsSync, promises as fs } from 'fs'
2+
import { existsSync } from 'fs'
3+
import { readFile, writeFile } from 'fs/promises'
34
import { notNullish, slash } from '@antfu/utils'
5+
import type { ComponentInfo } from '../../dist'
6+
import type { Options } from '../types'
47
import type { Context } from './context'
58
import { getTransformedPath } from './utils'
69
import { resolveTypeImports } from './type-imports/detect'
710

8-
export function parseDeclaration(code: string): Record<string, string> {
11+
const multilineCommentsRE = /\/\*.*?\*\//gms
12+
const singlelineCommentsRE = /\/\/.*$/gm
13+
14+
function extractImports(code: string) {
15+
return Object.fromEntries(Array.from(code.matchAll(/['"]?([\S]+?)['"]?\s*:\s*(.+?)[,;\n]/g)).map(i => [i[1], i[2]]))
16+
}
17+
18+
export function parseDeclaration(code: string): DeclarationImports | undefined {
919
if (!code)
10-
return {}
11-
return Object.fromEntries(Array.from(code.matchAll(/(?<!\/\/)\s+\s+['"]?(.+?)['"]?:\s(.+?)\n/g)).map(i => [i[1], i[2]]))
20+
return
21+
22+
code = code
23+
.replace(multilineCommentsRE, '')
24+
.replace(singlelineCommentsRE, '')
25+
26+
const imports: DeclarationImports = {
27+
component: {},
28+
directive: {},
29+
}
30+
const componentDeclaration = /export\s+interface\s+GlobalComponents\s*{(.*?)}/s.exec(code)?.[0]
31+
if (componentDeclaration)
32+
imports.component = extractImports(componentDeclaration)
33+
34+
const directiveDeclaration = /export\s+interface\s+ComponentCustomProperties\s*{(.*?)}/s.exec(code)?.[0]
35+
if (directiveDeclaration)
36+
imports.directive = extractImports(directiveDeclaration)
37+
38+
return imports
39+
}
40+
41+
/**
42+
* Converts `ComponentInfo` to an array
43+
*
44+
* `[name, "typeof import(path)[importName]"]`
45+
*/
46+
function stringifyComponentInfo(filepath: string, { from: path, as: name, name: importName }: ComponentInfo, importPathTransform?: Options['importPathTransform']): [string, string] | undefined {
47+
if (!name)
48+
return undefined
49+
path = getTransformedPath(path, importPathTransform)
50+
const related = isAbsolute(path)
51+
? `./${relative(dirname(filepath), path)}`
52+
: path
53+
const entry = `typeof import('${slash(related)}')['${importName || 'default'}']`
54+
return [name, entry]
55+
}
56+
57+
/**
58+
* Converts array of `ComponentInfo` to an import map
59+
*
60+
* `{ name: "typeof import(path)[importName]", ... }`
61+
*/
62+
export function stringifyComponentsInfo(filepath: string, components: ComponentInfo[], importPathTransform?: Options['importPathTransform']): Record<string, string> {
63+
return Object.fromEntries(
64+
components.map(info => stringifyComponentInfo(filepath, info, importPathTransform))
65+
.filter(notNullish),
66+
)
1267
}
1368

14-
export async function generateDeclaration(ctx: Context, root: string, filepath: string, removeUnused = false): Promise<void> {
15-
const items = [
69+
export interface DeclarationImports {
70+
component: Record<string, string>
71+
directive: Record<string, string>
72+
}
73+
74+
export function getDeclarationImports(ctx: Context, filepath: string): DeclarationImports | undefined {
75+
const component = stringifyComponentsInfo(filepath, [
1676
...Object.values({
1777
...ctx.componentNameMap,
1878
...ctx.componentCustomMap,
1979
}),
2080
...resolveTypeImports(ctx.options.types),
21-
]
22-
const imports: Record<string, string> = Object.fromEntries(
23-
items.map(({ from: path, as: name, name: importName }) => {
24-
if (!name)
25-
return undefined
26-
path = getTransformedPath(path, ctx)
27-
const related = isAbsolute(path)
28-
? `./${relative(dirname(filepath), path)}`
29-
: path
30-
31-
let entry = `typeof import('${slash(related)}')`
32-
if (importName)
33-
entry += `['${importName}']`
34-
else
35-
entry += '[\'default\']'
36-
return [name, entry]
37-
})
38-
.filter(notNullish),
81+
], ctx.options.importPathTransform)
82+
83+
const directive = stringifyComponentsInfo(
84+
filepath,
85+
Object.values(ctx.directiveCustomMap),
86+
ctx.options.importPathTransform,
3987
)
4088

41-
if (!Object.keys(imports).length)
89+
if (
90+
(Object.keys(component).length + Object.keys(directive).length) === 0
91+
)
4292
return
4393

44-
const originalContent = existsSync(filepath) ? await fs.readFile(filepath, 'utf-8') : ''
45-
46-
const originalImports = parseDeclaration(originalContent)
94+
return { component, directive }
95+
}
4796

48-
const lines = Object.entries({
49-
...originalImports,
50-
...imports,
51-
})
52-
.sort((a, b) => a[0].localeCompare(b[0]))
53-
.filter(([name]) => removeUnused ? items.find(i => i.as === name) : true)
97+
export function stringifyDeclarationImports(imports: Record<string, string>) {
98+
return Object.entries(imports)
99+
.sort(([a], [b]) => a.localeCompare(b))
54100
.map(([name, v]) => {
55101
if (!/^\w+$/.test(name))
56102
name = `'${name}'`
57103
return `${name}: ${v}`
58104
})
105+
}
106+
107+
export function getDeclaration(ctx: Context, filepath: string, originalImports?: DeclarationImports) {
108+
const imports = getDeclarationImports(ctx, filepath)
109+
if (!imports)
110+
return
111+
112+
const declarations = {
113+
component: stringifyDeclarationImports({ ...originalImports?.component, ...imports.component }),
114+
directive: stringifyDeclarationImports({ ...originalImports?.directive, ...imports.directive }),
115+
}
59116

60-
const code = `// generated by unplugin-vue-components
117+
let code = `// generated by unplugin-vue-components
61118
// We suggest you to commit this file into source control
62119
// Read more: https://github.com/vuejs/core/pull/3399
63120
import '@vue/runtime-core'
64121
65-
declare module '@vue/runtime-core' {
122+
export {}
123+
124+
declare module '@vue/runtime-core' {`
125+
126+
if (Object.keys(declarations.component).length > 0) {
127+
code += `
66128
export interface GlobalComponents {
67-
${lines.join('\n ')}
129+
${declarations.component.join('\n ')}
130+
}
131+
`
68132
}
133+
if (Object.keys(declarations.directive).length > 0) {
134+
code += `
135+
export interface ComponentCustomProperties {
136+
${declarations.directive.join('\n ')}
137+
}`
138+
}
139+
code += '\n}\n'
140+
return code
69141
}
70142

71-
export {}
72-
`
143+
export async function writeDeclaration(ctx: Context, filepath: string, removeUnused = false) {
144+
const originalContent = existsSync(filepath) ? await readFile(filepath, 'utf-8') : ''
145+
const originalImports = removeUnused ? undefined : parseDeclaration(originalContent)
146+
147+
const code = getDeclaration(ctx, filepath, originalImports)
148+
if (!code)
149+
return
73150

74151
if (code !== originalContent)
75-
await fs.writeFile(filepath, code, 'utf-8')
152+
await writeFile(filepath, code, 'utf-8')
76153
}

src/core/utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
getPackageInfo,
77
isPackageExists,
88
} from 'local-pkg'
9-
import type { ComponentInfo, ImportInfo, ImportInfoLegacy, ResolvedOptions } from '../types'
9+
import type { ComponentInfo, ImportInfo, ImportInfoLegacy, Options, ResolvedOptions } from '../types'
1010
import type { Context } from './context'
1111
import { DISABLE_COMMENT } from './constants'
1212

@@ -64,9 +64,9 @@ export function matchGlobs(filepath: string, globs: string[]) {
6464
return false
6565
}
6666

67-
export function getTransformedPath(path: string, ctx: Context): string {
68-
if (ctx.options.importPathTransform) {
69-
const result = ctx.options.importPathTransform(path)
67+
export function getTransformedPath(path: string, importPathTransform?: Options['importPathTransform']): string {
68+
if (importPathTransform) {
69+
const result = importPathTransform(path)
7070
if (result != null)
7171
path = result
7272
}
@@ -98,7 +98,7 @@ export function normalizeComponetInfo(info: ImportInfo | ImportInfoLegacy | Comp
9898
}
9999

100100
export function stringifyComponentImport({ as: name, from: path, name: importName, sideEffects }: ComponentInfo, ctx: Context) {
101-
path = getTransformedPath(path, ctx)
101+
path = getTransformedPath(path, ctx.options.importPathTransform)
102102

103103
const imports = [
104104
stringifyImport({ as: name, from: path, name: importName }),

0 commit comments

Comments
 (0)