Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: supports directive dts #442

Merged
merged 3 commits into from Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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