Skip to content

Commit

Permalink
Support component generics
Browse files Browse the repository at this point in the history
Implement RFC vuejs/rfcs#437
  • Loading branch information
znck committed Oct 17, 2022
1 parent 5dcab41 commit 5b6c3fe
Show file tree
Hide file tree
Showing 28 changed files with 1,555 additions and 1,408 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"Builtins",
"Codegen",
"deindent",
"depromisify",
"endregion",
"hygen",
"lcfirst",
Expand Down
11 changes: 4 additions & 7 deletions packages/compiler-tsx/src/template/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,15 +407,11 @@ function genComponentNode(node: ComponentNode): void {
ctx.write('/>', node.endTagLoc).newLine()
return // done
}
writeLine('>')

ctx.write('$slots=')
indent(() => {
wrap('{', '}', () => {
ctx.write(
`${getRuntimeFn(ctx.typeIdentifier, 'checkSlots')}(${
node.resolvedName ?? node.tag
}, {`,
)
ctx.write(`{`)
ctx.newLine()
indent(() => {
node.slots.forEach((slotNode) => {
Expand Down Expand Up @@ -447,9 +443,10 @@ function genComponentNode(node: ComponentNode): void {
ctx.write('},').newLine()
})
})
ctx.write('})')
ctx.write('}')
})
})
writeLine('>')
ctx.newLine()
ctx.write('</', node.endTagLoc)
ctx.write(node.resolvedName ?? node.tag)
Expand Down
16 changes: 9 additions & 7 deletions packages/compiler-tsx/src/vue/blocks/transformScriptSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import type { TransformedCode } from '../../types/TransformedCode'
import type { TransformOptionsResolved } from '../../types/TransformOptions'

export interface ScriptSetupBlockTransformResult extends TransformedCode {
/** private component */
exportIdentifier: string
/** public component */
componentIdentifier: string
scopeIdentifier: string
propsIdentifier: string
emitsIdentifier: string
exposeIdentifier: string
identifiers: KnownIdentifier[]
exports: Record<string, string>
}
Expand All @@ -22,6 +22,7 @@ export function transformScriptSetup(
options: TransformOptionsResolved,
): ScriptSetupBlockTransformResult {
const content = script?.content ?? ''
const generic = script?.attrs?.['generic']
const result = transform(content, {
internalIdentifierPrefix: options.internalIdentifierPrefix,
runtimeModuleName: options.runtimeModuleName,
Expand All @@ -30,6 +31,9 @@ export function transformScriptSetup(
fileName: options.fileName,
lib: options.typescript,
cache: options.cache,
attrsIdentifier: `${options.internalIdentifierPrefix}_attrs`,
slotsIdentifier: `${options.internalIdentifierPrefix}_slots`,
generic: typeof generic === 'string' ? generic : undefined,
})

invariant(result.map != null)
Expand All @@ -38,11 +42,9 @@ export function transformScriptSetup(
code: result.code,
map: result.map,
identifiers: result.identifiers,
exportIdentifier: result.componentIdentifier,
exportIdentifier: result.privateComponentIdentifier,
componentIdentifier: result.publicComponentIdentifier,
scopeIdentifier: result.scopeIdentifier,
propsIdentifier: result.propsIdentifier,
emitsIdentifier: result.emitsIdentifier,
exposeIdentifier: result.exposeIdentifier,
exports: result.exports,
}
}
101 changes: 56 additions & 45 deletions packages/compiler-tsx/src/vue/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
import {
Cache,
createCache,
first,
invariant,
rebaseSourceMap,
SourceTransformer,
} from '@vuedx/shared'
Expand All @@ -15,6 +17,7 @@ import type {
TransformOptionsResolved,
} from '../types/TransformOptions'
import { transformCustomBlock } from './blocks/transformCustomBlock'
import { createProgram } from '@vuedx/transforms'

import type { RootNode } from '@vue/compiler-core'
import type { RawSourceMap } from 'source-map'
Expand Down Expand Up @@ -219,64 +222,61 @@ export function compileWithDecodedSourceMap(
})

const exported = [
scriptSetup.exportIdentifier,
scriptSetup.propsIdentifier,
scriptSetup.emitsIdentifier,
scriptSetup.exposeIdentifier,
template.attrsIdentifier,
template.slotsIdentifier,
resolvedOptions.contextIdentifier,
...(descriptor.scriptSetup == null
? [template.attrsIdentifier, template.slotsIdentifier, contextIdentifier]
: [scriptSetup.componentIdentifier]),
...Object.values(scriptSetup.exports),
].join(', ')

builder.append(`return {${exported}};});`)
builder.append(`return {${exported}};};`)
builder.nextLine()
builder.append(`const {${exported}} = ${scriptSetup.scopeIdentifier};\n`)
builder.append(`const {${exported}} = ${scriptSetup.scopeIdentifier}();\n`)
Object.entries(scriptSetup.exports).forEach(([name, identifier]) => {
builder.append(`export type ${name} = typeof ${identifier};\n`)
})

region('public component definition', () => {
const props = `${resolvedOptions.contextIdentifier}.$props`

const parentClassIfAny = ` extends ${name}Public`
const type = `new () => typeof ${scriptSetup.exposeIdentifier}`
if (resolvedOptions.isTypeScript) {
builder.append(`const ${name}Public = null as unknown as ${type};`)
builder.nextLine()
if (descriptor.scriptSetup == null) {
const props = `${resolvedOptions.contextIdentifier}.$props`
const inheritAttrs =
descriptor.template?.content.includes('@vue-attrs-target') === true ||
script.inheritAttrs
const propsType = `typeof ${props}`
const attrsType = `typeof ${template.attrsIdentifier}`
const slotsType = `${resolvedOptions.typeIdentifier}.internal.Slots<ReturnType<typeof ${template.slotsIdentifier}>>`
builder.append(
[
`export default class ${name} {`,
defineProperty(
'$props',
inheritAttrs
? `${resolvedOptions.typeIdentifier}.internal.MergeAttrs<${propsType}, ${attrsType}> & {$slots: ${slotsType}}`
: `${propsType} & {$slots: ${slotsType}}`,
),
`}`,
].join('\n'),
)
} else {
const generic =
typeof descriptor.scriptSetup.attrs['generic'] === 'string'
? descriptor.scriptSetup.attrs['generic']
: ''
const typeArgs = parseGenericArgNames(generic)

const component =
typeArgs.length > 0
? `(new (${scriptSetup.scopeIdentifier}<${typeArgs.join(', ')}>().${
scriptSetup.componentIdentifier
}<${typeArgs.join(', ')}>))`
: `(new (${scriptSetup.scopeIdentifier}().${scriptSetup.componentIdentifier}))`

const genericExp = typeArgs.length > 0 ? `<${generic}>` : ''
builder.append(`export default class ${name}${genericExp} {\n`)
builder.append(
`const ${name}Public = /** @type {${type}} */ (/** @type {unknown} */ (null));`,
` $props = {...${component}.$props, $slots: ${component}.$slots };\n`,
)
builder.nextLine()
builder.append(`}`)
}

const inheritAttrs =
descriptor.template?.content.includes('@vue-attrs-target') === true ||
script.inheritAttrs

const propsType =
descriptor.scriptSetup != null
? `typeof ${props} & ${resolvedOptions.typeIdentifier}.internal.EmitsToProps<typeof ${scriptSetup.emitsIdentifier}>`
: `typeof ${props}`
const attrsType = `typeof ${template.attrsIdentifier}`

builder.append(
[
`export default class ${name}${parentClassIfAny} {`,
defineProperty(
'$props',
inheritAttrs
? `${resolvedOptions.typeIdentifier}.internal.MergeAttrs<${propsType}, ${attrsType}>`
: propsType,
),
defineProperty(
'$slots',
`${resolvedOptions.typeIdentifier}.internal.Slots<ReturnType<typeof ${template.slotsIdentifier}>>`,
),
`}`,
].join('\n'),
)
builder.nextLine()
})

Expand All @@ -303,6 +303,17 @@ export function compileWithDecodedSourceMap(
? ` ${name} = null as unknown as ${type};`
: ` ${name} = /** @type {${type}} */ (/** @type {unknown} */ (null));`
}

function parseGenericArgNames(code: string): string[] {
const ts = options.typescript
const program = createProgram(ts, `function _<${code}>() {}`)
const sourceFile = program.getSourceFile('input.ts')
invariant(sourceFile != null, 'sourceFile should not be null')
const decl = first(sourceFile.statements)
invariant(ts.isFunctionDeclaration(decl))
invariant(decl.typeParameters != null)
return decl.typeParameters.map((p) => p.name.getText())
}
}

function runIfNeeded<R>(
Expand Down

0 comments on commit 5b6c3fe

Please sign in to comment.