Skip to content

Commit 5b6c3fe

Browse files
committed
Support component generics
Implement RFC vuejs/rfcs#437
1 parent 5dcab41 commit 5b6c3fe

28 files changed

+1555
-1408
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"Builtins",
44
"Codegen",
55
"deindent",
6+
"depromisify",
67
"endregion",
78
"hygen",
89
"lcfirst",

packages/compiler-tsx/src/template/generate.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -407,15 +407,11 @@ function genComponentNode(node: ComponentNode): void {
407407
ctx.write('/>', node.endTagLoc).newLine()
408408
return // done
409409
}
410-
writeLine('>')
411410

411+
ctx.write('$slots=')
412412
indent(() => {
413413
wrap('{', '}', () => {
414-
ctx.write(
415-
`${getRuntimeFn(ctx.typeIdentifier, 'checkSlots')}(${
416-
node.resolvedName ?? node.tag
417-
}, {`,
418-
)
414+
ctx.write(`{`)
419415
ctx.newLine()
420416
indent(() => {
421417
node.slots.forEach((slotNode) => {
@@ -447,9 +443,10 @@ function genComponentNode(node: ComponentNode): void {
447443
ctx.write('},').newLine()
448444
})
449445
})
450-
ctx.write('})')
446+
ctx.write('}')
451447
})
452448
})
449+
writeLine('>')
453450
ctx.newLine()
454451
ctx.write('</', node.endTagLoc)
455452
ctx.write(node.resolvedName ?? node.tag)

packages/compiler-tsx/src/vue/blocks/transformScriptSetup.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import type { TransformedCode } from '../../types/TransformedCode'
88
import type { TransformOptionsResolved } from '../../types/TransformOptions'
99

1010
export interface ScriptSetupBlockTransformResult extends TransformedCode {
11+
/** private component */
1112
exportIdentifier: string
13+
/** public component */
14+
componentIdentifier: string
1215
scopeIdentifier: string
13-
propsIdentifier: string
14-
emitsIdentifier: string
15-
exposeIdentifier: string
1616
identifiers: KnownIdentifier[]
1717
exports: Record<string, string>
1818
}
@@ -22,6 +22,7 @@ export function transformScriptSetup(
2222
options: TransformOptionsResolved,
2323
): ScriptSetupBlockTransformResult {
2424
const content = script?.content ?? ''
25+
const generic = script?.attrs?.['generic']
2526
const result = transform(content, {
2627
internalIdentifierPrefix: options.internalIdentifierPrefix,
2728
runtimeModuleName: options.runtimeModuleName,
@@ -30,6 +31,9 @@ export function transformScriptSetup(
3031
fileName: options.fileName,
3132
lib: options.typescript,
3233
cache: options.cache,
34+
attrsIdentifier: `${options.internalIdentifierPrefix}_attrs`,
35+
slotsIdentifier: `${options.internalIdentifierPrefix}_slots`,
36+
generic: typeof generic === 'string' ? generic : undefined,
3337
})
3438

3539
invariant(result.map != null)
@@ -38,11 +42,9 @@ export function transformScriptSetup(
3842
code: result.code,
3943
map: result.map,
4044
identifiers: result.identifiers,
41-
exportIdentifier: result.componentIdentifier,
45+
exportIdentifier: result.privateComponentIdentifier,
46+
componentIdentifier: result.publicComponentIdentifier,
4247
scopeIdentifier: result.scopeIdentifier,
43-
propsIdentifier: result.propsIdentifier,
44-
emitsIdentifier: result.emitsIdentifier,
45-
exposeIdentifier: result.exposeIdentifier,
4648
exports: result.exports,
4749
}
4850
}

packages/compiler-tsx/src/vue/compile.ts

+56-45
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
import {
88
Cache,
99
createCache,
10+
first,
11+
invariant,
1012
rebaseSourceMap,
1113
SourceTransformer,
1214
} from '@vuedx/shared'
@@ -15,6 +17,7 @@ import type {
1517
TransformOptionsResolved,
1618
} from '../types/TransformOptions'
1719
import { transformCustomBlock } from './blocks/transformCustomBlock'
20+
import { createProgram } from '@vuedx/transforms'
1821

1922
import type { RootNode } from '@vue/compiler-core'
2023
import type { RawSourceMap } from 'source-map'
@@ -219,64 +222,61 @@ export function compileWithDecodedSourceMap(
219222
})
220223

221224
const exported = [
222-
scriptSetup.exportIdentifier,
223-
scriptSetup.propsIdentifier,
224-
scriptSetup.emitsIdentifier,
225-
scriptSetup.exposeIdentifier,
226-
template.attrsIdentifier,
227-
template.slotsIdentifier,
228-
resolvedOptions.contextIdentifier,
225+
...(descriptor.scriptSetup == null
226+
? [template.attrsIdentifier, template.slotsIdentifier, contextIdentifier]
227+
: [scriptSetup.componentIdentifier]),
229228
...Object.values(scriptSetup.exports),
230229
].join(', ')
231230

232-
builder.append(`return {${exported}};});`)
231+
builder.append(`return {${exported}};};`)
233232
builder.nextLine()
234-
builder.append(`const {${exported}} = ${scriptSetup.scopeIdentifier};\n`)
233+
builder.append(`const {${exported}} = ${scriptSetup.scopeIdentifier}();\n`)
235234
Object.entries(scriptSetup.exports).forEach(([name, identifier]) => {
236235
builder.append(`export type ${name} = typeof ${identifier};\n`)
237236
})
238237

239238
region('public component definition', () => {
240-
const props = `${resolvedOptions.contextIdentifier}.$props`
241-
242-
const parentClassIfAny = ` extends ${name}Public`
243-
const type = `new () => typeof ${scriptSetup.exposeIdentifier}`
244-
if (resolvedOptions.isTypeScript) {
245-
builder.append(`const ${name}Public = null as unknown as ${type};`)
246-
builder.nextLine()
239+
if (descriptor.scriptSetup == null) {
240+
const props = `${resolvedOptions.contextIdentifier}.$props`
241+
const inheritAttrs =
242+
descriptor.template?.content.includes('@vue-attrs-target') === true ||
243+
script.inheritAttrs
244+
const propsType = `typeof ${props}`
245+
const attrsType = `typeof ${template.attrsIdentifier}`
246+
const slotsType = `${resolvedOptions.typeIdentifier}.internal.Slots<ReturnType<typeof ${template.slotsIdentifier}>>`
247+
builder.append(
248+
[
249+
`export default class ${name} {`,
250+
defineProperty(
251+
'$props',
252+
inheritAttrs
253+
? `${resolvedOptions.typeIdentifier}.internal.MergeAttrs<${propsType}, ${attrsType}> & {$slots: ${slotsType}}`
254+
: `${propsType} & {$slots: ${slotsType}}`,
255+
),
256+
`}`,
257+
].join('\n'),
258+
)
247259
} else {
260+
const generic =
261+
typeof descriptor.scriptSetup.attrs['generic'] === 'string'
262+
? descriptor.scriptSetup.attrs['generic']
263+
: ''
264+
const typeArgs = parseGenericArgNames(generic)
265+
266+
const component =
267+
typeArgs.length > 0
268+
? `(new (${scriptSetup.scopeIdentifier}<${typeArgs.join(', ')}>().${
269+
scriptSetup.componentIdentifier
270+
}<${typeArgs.join(', ')}>))`
271+
: `(new (${scriptSetup.scopeIdentifier}().${scriptSetup.componentIdentifier}))`
272+
273+
const genericExp = typeArgs.length > 0 ? `<${generic}>` : ''
274+
builder.append(`export default class ${name}${genericExp} {\n`)
248275
builder.append(
249-
`const ${name}Public = /** @type {${type}} */ (/** @type {unknown} */ (null));`,
276+
` $props = {...${component}.$props, $slots: ${component}.$slots };\n`,
250277
)
251-
builder.nextLine()
278+
builder.append(`}`)
252279
}
253-
254-
const inheritAttrs =
255-
descriptor.template?.content.includes('@vue-attrs-target') === true ||
256-
script.inheritAttrs
257-
258-
const propsType =
259-
descriptor.scriptSetup != null
260-
? `typeof ${props} & ${resolvedOptions.typeIdentifier}.internal.EmitsToProps<typeof ${scriptSetup.emitsIdentifier}>`
261-
: `typeof ${props}`
262-
const attrsType = `typeof ${template.attrsIdentifier}`
263-
264-
builder.append(
265-
[
266-
`export default class ${name}${parentClassIfAny} {`,
267-
defineProperty(
268-
'$props',
269-
inheritAttrs
270-
? `${resolvedOptions.typeIdentifier}.internal.MergeAttrs<${propsType}, ${attrsType}>`
271-
: propsType,
272-
),
273-
defineProperty(
274-
'$slots',
275-
`${resolvedOptions.typeIdentifier}.internal.Slots<ReturnType<typeof ${template.slotsIdentifier}>>`,
276-
),
277-
`}`,
278-
].join('\n'),
279-
)
280280
builder.nextLine()
281281
})
282282

@@ -303,6 +303,17 @@ export function compileWithDecodedSourceMap(
303303
? ` ${name} = null as unknown as ${type};`
304304
: ` ${name} = /** @type {${type}} */ (/** @type {unknown} */ (null));`
305305
}
306+
307+
function parseGenericArgNames(code: string): string[] {
308+
const ts = options.typescript
309+
const program = createProgram(ts, `function _<${code}>() {}`)
310+
const sourceFile = program.getSourceFile('input.ts')
311+
invariant(sourceFile != null, 'sourceFile should not be null')
312+
const decl = first(sourceFile.statements)
313+
invariant(ts.isFunctionDeclaration(decl))
314+
invariant(decl.typeParameters != null)
315+
return decl.typeParameters.map((p) => p.name.getText())
316+
}
306317
}
307318

308319
function runIfNeeded<R>(

0 commit comments

Comments
 (0)