|
1 | 1 | 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' |
3 | 4 | import { notNullish, slash } from '@antfu/utils'
|
| 5 | +import type { ComponentInfo } from '../../dist' |
| 6 | +import type { Options } from '../types' |
4 | 7 | import type { Context } from './context'
|
5 | 8 | import { getTransformedPath } from './utils'
|
6 | 9 | import { resolveTypeImports } from './type-imports/detect'
|
7 | 10 |
|
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 { |
9 | 19 | 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 | + ) |
12 | 67 | }
|
13 | 68 |
|
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, [ |
16 | 76 | ...Object.values({
|
17 | 77 | ...ctx.componentNameMap,
|
18 | 78 | ...ctx.componentCustomMap,
|
19 | 79 | }),
|
20 | 80 | ...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, |
39 | 87 | )
|
40 | 88 |
|
41 |
| - if (!Object.keys(imports).length) |
| 89 | + if ( |
| 90 | + (Object.keys(component).length + Object.keys(directive).length) === 0 |
| 91 | + ) |
42 | 92 | return
|
43 | 93 |
|
44 |
| - const originalContent = existsSync(filepath) ? await fs.readFile(filepath, 'utf-8') : '' |
45 |
| - |
46 |
| - const originalImports = parseDeclaration(originalContent) |
| 94 | + return { component, directive } |
| 95 | +} |
47 | 96 |
|
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)) |
54 | 100 | .map(([name, v]) => {
|
55 | 101 | if (!/^\w+$/.test(name))
|
56 | 102 | name = `'${name}'`
|
57 | 103 | return `${name}: ${v}`
|
58 | 104 | })
|
| 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 | + } |
59 | 116 |
|
60 |
| - const code = `// generated by unplugin-vue-components |
| 117 | + let code = `// generated by unplugin-vue-components |
61 | 118 | // We suggest you to commit this file into source control
|
62 | 119 | // Read more: https://github.com/vuejs/core/pull/3399
|
63 | 120 | import '@vue/runtime-core'
|
64 | 121 |
|
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 += ` |
66 | 128 | export interface GlobalComponents {
|
67 |
| - ${lines.join('\n ')} |
| 129 | + ${declarations.component.join('\n ')} |
| 130 | + } |
| 131 | +` |
68 | 132 | }
|
| 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 |
69 | 141 | }
|
70 | 142 |
|
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 |
73 | 150 |
|
74 | 151 | if (code !== originalContent)
|
75 |
| - await fs.writeFile(filepath, code, 'utf-8') |
| 152 | + await writeFile(filepath, code, 'utf-8') |
76 | 153 | }
|
0 commit comments