Skip to content

Commit

Permalink
feat: supports directive dts (#442)
Browse files Browse the repository at this point in the history
  • Loading branch information
sxzz committed Jul 1, 2022
1 parent f161f50 commit 0d0a939
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 67 deletions.
58 changes: 36 additions & 22 deletions src/core/context.ts
Expand Up @@ -8,7 +8,7 @@ import { DIRECTIVE_IMPORT_PREFIX } from './constants'
import { getNameFromFilePath, matchGlobs, normalizeComponetInfo, parseId, pascalCase, resolveAlias } from './utils'
import { resolveOptions } from './options'
import { searchComponents } from './fs/glob'
import { generateDeclaration } from './declaration'
import { writeDeclaration } from './declaration'
import transformer from './transformer'

const debug = {
Expand All @@ -27,6 +27,7 @@ export class Context {
private _componentNameMap: Record<string, ComponentInfo> = {}
private _componentUsageMap: Record<string, Set<string>> = {}
private _componentCustomMap: Record<string, ComponentInfo> = {}
private _directiveCustomMap: Record<string, ComponentInfo> = {}
private _server: ViteDevServer | undefined

root = process.cwd()
Expand All @@ -37,7 +38,10 @@ export class Context {
private rawOptions: Options,
) {
this.options = resolveOptions(rawOptions, this.root)
this.generateDeclaration = throttle(500, false, this.generateDeclaration.bind(this))
this.generateDeclaration
= throttle(500, false, this._generateDeclaration.bind(this)) as
// `throttle` will omit return value.
((removeUnused?: boolean) => void)
this.setTransformer(this.options.transformer)
}

Expand Down Expand Up @@ -147,6 +151,11 @@ export class Context {
this._componentCustomMap[info.as] = info
}

addCustomDirectives(info: ComponentInfo) {
if (info.as)
this._directiveCustomMap[info.as] = info
}

removeComponents(paths: string | string[]) {
debug.components('remove', paths)

Expand Down Expand Up @@ -220,24 +229,26 @@ export class Context {
continue

const result = await resolver.resolve(type === 'directive' ? name.slice(DIRECTIVE_IMPORT_PREFIX.length) : name)
if (result) {
if (typeof result === 'string') {
info = {
as: name,
from: result,
}
this.addCustomComponents(info)
return info
if (!result)
continue

if (typeof result === 'string') {
info = {
as: name,
from: result,
}
else {
info = {
as: name,
...normalizeComponetInfo(result),
}
this.addCustomComponents(info)
return info
}
else {
info = {
as: name,
...normalizeComponetInfo(result),
}
}
if (type === 'component')
this.addCustomComponents(info)
else if (type === 'directive')
this.addCustomDirectives(info)
return info
}

return undefined
Expand All @@ -260,9 +271,6 @@ export class Context {
* This search for components in with the given options.
* Will be called multiple times to ensure file loaded,
* should normally run only once.
*
* @param ctx
* @param force
*/
searchGlob() {
if (this._searched)
Expand All @@ -273,19 +281,25 @@ export class Context {
this._searched = true
}

generateDeclaration() {
_generateDeclaration(removeUnused = !this._server) {
if (!this.options.dts)
return

debug.decleration('generating')
generateDeclaration(this, this.options.root, this.options.dts, !this._server)
return writeDeclaration(this, this.options.dts, removeUnused)
}

generateDeclaration

get componentNameMap() {
return this._componentNameMap
}

get componentCustomMap() {
return this._componentCustomMap
}

get directiveCustomMap() {
return this._directiveCustomMap
}
}
157 changes: 117 additions & 40 deletions src/core/declaration.ts
@@ -1,76 +1,153 @@
import { dirname, isAbsolute, relative } from 'path'
import { existsSync, promises as fs } from 'fs'
import { existsSync } from 'fs'
import { readFile, writeFile } from 'fs/promises'
import { notNullish, slash } from '@antfu/utils'
import type { ComponentInfo } from '../../dist'
import type { Options } from '../types'
import type { Context } from './context'
import { getTransformedPath } from './utils'
import { resolveTypeImports } from './type-imports/detect'

export function parseDeclaration(code: string): Record<string, string> {
const multilineCommentsRE = /\/\*.*?\*\//gms
const singlelineCommentsRE = /\/\/.*$/gm

function extractImports(code: string) {
return Object.fromEntries(Array.from(code.matchAll(/['"]?([\S]+?)['"]?\s*:\s*(.+?)[,;\n]/g)).map(i => [i[1], i[2]]))
}

export function parseDeclaration(code: string): DeclarationImports | undefined {
if (!code)
return {}
return Object.fromEntries(Array.from(code.matchAll(/(?<!\/\/)\s+\s+['"]?(.+?)['"]?:\s(.+?)\n/g)).map(i => [i[1], i[2]]))
return

code = code
.replace(multilineCommentsRE, '')
.replace(singlelineCommentsRE, '')

const imports: DeclarationImports = {
component: {},
directive: {},
}
const componentDeclaration = /export\s+interface\s+GlobalComponents\s*{(.*?)}/s.exec(code)?.[0]
if (componentDeclaration)
imports.component = extractImports(componentDeclaration)

const directiveDeclaration = /export\s+interface\s+ComponentCustomProperties\s*{(.*?)}/s.exec(code)?.[0]
if (directiveDeclaration)
imports.directive = extractImports(directiveDeclaration)

return imports
}

/**
* Converts `ComponentInfo` to an array
*
* `[name, "typeof import(path)[importName]"]`
*/
function stringifyComponentInfo(filepath: string, { from: path, as: name, name: importName }: ComponentInfo, importPathTransform?: Options['importPathTransform']): [string, string] | undefined {
if (!name)
return undefined
path = getTransformedPath(path, importPathTransform)
const related = isAbsolute(path)
? `./${relative(dirname(filepath), path)}`
: path
const entry = `typeof import('${slash(related)}')['${importName || 'default'}']`
return [name, entry]
}

/**
* Converts array of `ComponentInfo` to an import map
*
* `{ name: "typeof import(path)[importName]", ... }`
*/
export function stringifyComponentsInfo(filepath: string, components: ComponentInfo[], importPathTransform?: Options['importPathTransform']): Record<string, string> {
return Object.fromEntries(
components.map(info => stringifyComponentInfo(filepath, info, importPathTransform))
.filter(notNullish),
)
}

export async function generateDeclaration(ctx: Context, root: string, filepath: string, removeUnused = false): Promise<void> {
const items = [
export interface DeclarationImports {
component: Record<string, string>
directive: Record<string, string>
}

export function getDeclarationImports(ctx: Context, filepath: string): DeclarationImports | undefined {
const component = stringifyComponentsInfo(filepath, [
...Object.values({
...ctx.componentNameMap,
...ctx.componentCustomMap,
}),
...resolveTypeImports(ctx.options.types),
]
const imports: Record<string, string> = Object.fromEntries(
items.map(({ from: path, as: name, name: importName }) => {
if (!name)
return undefined
path = getTransformedPath(path, ctx)
const related = isAbsolute(path)
? `./${relative(dirname(filepath), path)}`
: path

let entry = `typeof import('${slash(related)}')`
if (importName)
entry += `['${importName}']`
else
entry += '[\'default\']'
return [name, entry]
})
.filter(notNullish),
], ctx.options.importPathTransform)

const directive = stringifyComponentsInfo(
filepath,
Object.values(ctx.directiveCustomMap),
ctx.options.importPathTransform,
)

if (!Object.keys(imports).length)
if (
(Object.keys(component).length + Object.keys(directive).length) === 0
)
return

const originalContent = existsSync(filepath) ? await fs.readFile(filepath, 'utf-8') : ''

const originalImports = parseDeclaration(originalContent)
return { component, directive }
}

const lines = Object.entries({
...originalImports,
...imports,
})
.sort((a, b) => a[0].localeCompare(b[0]))
.filter(([name]) => removeUnused ? items.find(i => i.as === name) : true)
export function stringifyDeclarationImports(imports: Record<string, string>) {
return Object.entries(imports)
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, v]) => {
if (!/^\w+$/.test(name))
name = `'${name}'`
return `${name}: ${v}`
})
}

export function getDeclaration(ctx: Context, filepath: string, originalImports?: DeclarationImports) {
const imports = getDeclarationImports(ctx, filepath)
if (!imports)
return

const declarations = {
component: stringifyDeclarationImports({ ...originalImports?.component, ...imports.component }),
directive: stringifyDeclarationImports({ ...originalImports?.directive, ...imports.directive }),
}

const code = `// generated by unplugin-vue-components
let code = `// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
declare module '@vue/runtime-core' {
export {}
declare module '@vue/runtime-core' {`

if (Object.keys(declarations.component).length > 0) {
code += `
export interface GlobalComponents {
${lines.join('\n ')}
${declarations.component.join('\n ')}
}
`
}
if (Object.keys(declarations.directive).length > 0) {
code += `
export interface ComponentCustomProperties {
${declarations.directive.join('\n ')}
}`
}
code += '\n}\n'
return code
}

export {}
`
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)
return

if (code !== originalContent)
await fs.writeFile(filepath, code, 'utf-8')
await writeFile(filepath, code, 'utf-8')
}
10 changes: 5 additions & 5 deletions src/core/utils.ts
Expand Up @@ -6,7 +6,7 @@ import {
getPackageInfo,
isPackageExists,
} from 'local-pkg'
import type { ComponentInfo, ImportInfo, ImportInfoLegacy, ResolvedOptions } from '../types'
import type { ComponentInfo, ImportInfo, ImportInfoLegacy, Options, ResolvedOptions } from '../types'
import type { Context } from './context'
import { DISABLE_COMMENT } from './constants'

Expand Down Expand Up @@ -64,9 +64,9 @@ export function matchGlobs(filepath: string, globs: string[]) {
return false
}

export function getTransformedPath(path: string, ctx: Context): string {
if (ctx.options.importPathTransform) {
const result = ctx.options.importPathTransform(path)
export function getTransformedPath(path: string, importPathTransform?: Options['importPathTransform']): string {
if (importPathTransform) {
const result = importPathTransform(path)
if (result != null)
path = result
}
Expand Down Expand Up @@ -98,7 +98,7 @@ export function normalizeComponetInfo(info: ImportInfo | ImportInfoLegacy | Comp
}

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

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

0 comments on commit 0d0a939

Please sign in to comment.