From bcf6ad40238e70a949d8c71e089e6156f5196809 Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Mon, 15 Dec 2025 15:32:21 +0800 Subject: [PATCH 1/3] refactor(component-meta): redundant logic between deduplication and language-core --- packages/component-meta/lib/base.ts | 934 ++++++++---------- packages/component-meta/tests/index.spec.ts | 4 +- packages/language-core/index.ts | 1 + .../language-core/lib/codegen/script/index.ts | 8 +- .../lib/codegen/script/template.ts | 8 +- .../language-core/lib/parsers/scriptRanges.ts | 198 ++-- .../lib/parsers/scriptSetupRanges.ts | 84 +- packages/language-core/lib/parsers/utils.ts | 3 +- packages/language-core/lib/plugins/vue-tsx.ts | 4 +- packages/language-core/lib/types.ts | 3 +- packages/language-core/lib/utils/shared.ts | 7 +- .../lib/plugins/vue-extract-file.ts | 16 +- 12 files changed, 582 insertions(+), 688 deletions(-) diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index b5b08884ff..e653b78641 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -18,63 +18,6 @@ export * from './types'; const windowsPathReg = /\\/g; -// Utility function to get the component node from an AST -function getComponentNodeFromAst( - ast: ts.SourceFile, - exportName: string, - ts: typeof import('typescript'), -): ts.Node | undefined { - let result: ts.Node | undefined; - - if (exportName === 'default') { - ast.forEachChild(child => { - if (ts.isExportAssignment(child)) { - result = child.expression; - } - }); - } - else { - ast.forEachChild(child => { - if ( - ts.isVariableStatement(child) - && child.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword) - ) { - for (const dec of child.declarationList.declarations) { - if (dec.name.getText(ast) === exportName) { - result = dec.initializer; - } - } - } - }); - } - - return result; -} - -// Utility function to get the component options node from a component node -function getComponentOptionsNodeFromComponent( - component: ts.Node | undefined, - ts: typeof import('typescript'), -): ts.ObjectLiteralExpression | undefined { - if (component) { - // export default { ... } - if (ts.isObjectLiteralExpression(component)) { - return component; - } - // export default defineComponent({ ... }) - else if (ts.isCallExpression(component)) { - if (component.arguments.length) { - const arg = component.arguments[0]!; - if (ts.isObjectLiteralExpression(arg)) { - return arg; - } - } - } - } - - return undefined; -} - export function createCheckerByJsonConfigBase( ts: typeof import('typescript'), rootDir: string, @@ -157,6 +100,7 @@ function baseCreate( let fileNamesSet = new Set(fileNames.map(path => path.replace(windowsPathReg, '/'))); let projectVersion = 0; + const scriptRangesCache = new WeakMap(); const projectHost: TypeScriptProjectHost = { getCurrentDirectory: () => rootPath, getProjectVersion: () => projectVersion.toString(), @@ -229,6 +173,7 @@ function baseCreate( ); const { languageServiceHost } = createLanguageServiceHost(ts, ts.sys, language, s => s, projectHost); const tsLs = ts.createLanguageService(languageServiceHost); + const printer = ts.createPrinter(checkerOptions.printer); if (checkerOptions.forceUseTs) { const getScriptKind = languageServiceHost.getScriptKind?.bind(languageServiceHost); @@ -275,12 +220,6 @@ function baseCreate( getProgram() { return tsLs.getProgram(); }, - /** - * @deprecated use `getProgram()` instead - */ - __internal__: { - tsLs, - }, }; function isMetaFileName(fileName: string) { @@ -399,7 +338,7 @@ interface ComponentMeta { .map(prop => { const { resolveNestedProperties, - } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions, ts, language); + } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions); return resolveNestedProperties(prop); }) @@ -415,35 +354,42 @@ interface ComponentMeta { } // fill defaults - const printer = ts.createPrinter(checkerOptions.printer); - const sourceScript = language.scripts.get(componentPath)!; - const { snapshot } = sourceScript; - - const vueFile = sourceScript.generated?.root; - const vueDefaults = vueFile && exportName === 'default' - ? (vueFile instanceof core.VueVirtualCode ? readVueComponentDefaultProps(vueFile, printer, ts) : {}) - : {}; - const tsDefaults = !vueFile - ? readTsComponentDefaultProps( - ts.createSourceFile( - '/tmp.' + componentPath.slice(componentPath.lastIndexOf('.') + 1), // ts | js | tsx | jsx - snapshot.getText(0, snapshot.getLength()), - ts.ScriptTarget.Latest, - ), - exportName, - printer, + const sourceScript = language.scripts.get(componentPath); + const sourceFile = program.getSourceFile(componentPath); + const scriptRanges = sourceFile ? getScriptRanges(sourceFile) : undefined; + const vueFile = sourceScript?.generated?.root; + const defaults = sourceFile && scriptRanges + ? readDefaultsFromScript( ts, + printer, + sourceFile, + scriptRanges, + exportName, ) : {}; + const virtualCode = vueFile ? getVirtualCode(componentPath) : undefined; + const scriptSetupRanges = virtualCode ? getScriptSetupRanges(virtualCode) : undefined; + + if (virtualCode?.sfc.scriptSetup && scriptSetupRanges) { + Object.assign( + defaults, + readDefaultsFromScriptSetup( + ts, + printer, + virtualCode.sfc.scriptSetup.ast, + scriptSetupRanges, + ), + ); + } for ( - const [propName, defaultExp] of Object.entries({ - ...vueDefaults, - ...tsDefaults, - }) + const [propName, defaultExp] of Object.entries(defaults) ) { const prop = result.find(p => p.name === propName); if (prop) { + if (!defaultExp) { + debugger; + } prop.default = defaultExp.default; if (defaultExp.required !== undefined) { @@ -469,7 +415,7 @@ interface ComponentMeta { return calls.map(call => { const { resolveEventSignature, - } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions, ts, language); + } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions); return resolveEventSignature(call); }).filter(event => event.name); @@ -488,7 +434,7 @@ interface ComponentMeta { return properties.map(prop => { const { resolveSlotProperties, - } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions, ts, language); + } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions); return resolveSlotProperties(prop); }); @@ -517,7 +463,7 @@ interface ComponentMeta { return properties.map(prop => { const { resolveExposedProperties, - } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions, ts, language); + } = createSchemaResolvers(typeChecker, symbolNode, checkerOptions); return resolveExposedProperties(prop); }); @@ -527,38 +473,21 @@ interface ComponentMeta { } function getName() { - // Try to get name from component options - const sourceScript = language.scripts.get(componentPath)!; - const { snapshot } = sourceScript; - const vueFile = sourceScript.generated?.root; - - if (vueFile && exportName === 'default' && vueFile instanceof core.VueVirtualCode) { - // For Vue SFC, check the script section - const { sfc } = vueFile; - if (sfc.script) { - const name = readComponentName(sfc.script.ast, exportName, ts); - if (name) { - return name; - } + const sourceFile = program.getSourceFile(componentPath); + if (sourceFile) { + const scriptRanges = getScriptRanges(sourceFile); + const name = scriptRanges?.exports[exportName]?.options?.name; + if (name && ts.isStringLiteral(name.node)) { + return name.node.text; } } - else if (!vueFile) { - // For TS/JS files - const ast = ts.createSourceFile( - '/tmp.' + componentPath.slice(componentPath.lastIndexOf('.') + 1), - snapshot.getText(0, snapshot.getLength()), - ts.ScriptTarget.Latest, - ); - return readComponentName(ast, exportName, ts); - } - - return undefined; } function getDescription() { const sourceFile = program.getSourceFile(componentPath); if (sourceFile) { - return readComponentDescription(sourceFile, exportName, ts, typeChecker); + const scriptRanges = getScriptRanges(sourceFile); + return readComponentDescription(ts, scriptRanges, exportName, typeChecker); } } } @@ -602,456 +531,438 @@ interface ComponentMeta { exports, }; } -} - -function createSchemaResolvers( - typeChecker: ts.TypeChecker, - symbolNode: ts.Expression, - { rawType, schema: options, noDeclarations }: MetaCheckerOptions, - ts: typeof import('typescript'), - language: core.Language, -) { - const visited = new Set(); - - function shouldIgnore(subtype: ts.Type) { - const name = getFullyQualifiedName(subtype); - if (name === 'any') { - return true; - } - if (visited.has(subtype)) { - return true; + function getScriptRanges(sourceFile: ts.SourceFile) { + let scriptRanges = scriptRangesCache.get(sourceFile); + if (!scriptRanges) { + scriptRanges = core.parseScriptRanges(ts, sourceFile, vueOptions); + scriptRangesCache.set(sourceFile, scriptRanges); } - - if (typeof options === 'object') { - for (const item of options.ignore ?? []) { - if (typeof item === 'function') { - const result = item(name, subtype, typeChecker); - if (typeof result === 'boolean') { - return result; - } - } - else if (name === item) { - return true; - } - } - } - - return false; + return scriptRanges; } - function reducer(acc: any, cur: any) { - acc[cur.name] = cur; - return acc; + function getVirtualCode(fileName: string) { + const sourceScript = language.scripts.get(fileName); + const vueFile = sourceScript?.generated?.root; + if (vueFile instanceof core.VueVirtualCode) { + return vueFile; + } } - function getJsDocTags(target: ts.Symbol | ts.Signature) { - return target.getJsDocTags(typeChecker).map(tag => ({ - name: tag.name, - text: tag.text !== undefined ? ts.displayPartsToString(tag.text) : undefined, - })); + function getScriptSetupRanges(virtualCode: core.VueVirtualCode) { + const { sfc } = virtualCode; + const codegen = core.tsCodegen.get(sfc); + return codegen?.getScriptSetupRanges(); } - function resolveNestedProperties(prop: ts.Symbol): PropertyMeta { - const subtype = typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode); - let schema: PropertyMetaSchema | undefined; - let declarations: Declaration[] | undefined; - - return { - name: prop.getEscapedName().toString(), - global: false, - description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), - tags: getJsDocTags(prop), - required: !(prop.flags & ts.SymbolFlags.Optional), - type: getFullyQualifiedName(subtype), - get declarations() { - return declarations ??= getDeclarations(prop.declarations ?? []); - }, - get schema() { - return schema ??= resolveSchema(subtype); - }, - rawType: rawType ? subtype : undefined, - getTypeObject() { - return subtype; - }, - }; - } - function resolveSlotProperties(prop: ts.Symbol): SlotMeta { - const propType = typeChecker.getNonNullableType(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode)); - const signatures = propType.getCallSignatures(); - const paramType = signatures[0]?.parameters[0]; - const subtype = paramType ? typeChecker.getTypeOfSymbolAtLocation(paramType, symbolNode) : typeChecker.getAnyType(); - let schema: PropertyMetaSchema | undefined; - let declarations: Declaration[] | undefined; + function createSchemaResolvers( + typeChecker: ts.TypeChecker, + symbolNode: ts.Expression, + { rawType, schema: options, noDeclarations }: MetaCheckerOptions, + ) { + const visited = new Set(); - return { - name: prop.getName(), - type: getFullyQualifiedName(subtype), - description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), - tags: getJsDocTags(prop), - get declarations() { - return declarations ??= getDeclarations(prop.declarations ?? []); - }, - get schema() { - return schema ??= resolveSchema(subtype); - }, - rawType: rawType ? subtype : undefined, - getTypeObject() { - return subtype; - }, - }; - } - function resolveExposedProperties(expose: ts.Symbol): ExposeMeta { - const subtype = typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode); - let schema: PropertyMetaSchema | undefined; - let declarations: Declaration[] | undefined; + function shouldIgnore(subtype: ts.Type) { + const name = getFullyQualifiedName(subtype); + if (name === 'any') { + return true; + } - return { - name: expose.getName(), - type: getFullyQualifiedName(subtype), - description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)), - tags: getJsDocTags(expose), - get declarations() { - return declarations ??= getDeclarations(expose.declarations ?? []); - }, - get schema() { - return schema ??= resolveSchema(subtype); - }, - rawType: rawType ? subtype : undefined, - getTypeObject() { - return subtype; - }, - }; - } - function resolveEventSignature(call: ts.Signature): EventMeta { - let schema: PropertyMetaSchema[] | undefined; - let declarations: Declaration[] | undefined; - let subtype: ts.Type | undefined; - let symbol: ts.Symbol | undefined; - let subtypeStr = '[]'; - let getSchema = () => [] as PropertyMetaSchema[]; - - if (call.parameters.length >= 2) { - symbol = call.parameters[1]!; - subtype = typeChecker.getTypeOfSymbolAtLocation(symbol, symbolNode); - if ((call.parameters[1]!.valueDeclaration as any)?.dotDotDotToken) { - subtypeStr = getFullyQualifiedName(subtype); - getSchema = () => typeChecker.getTypeArguments(subtype! as ts.TypeReference).map(resolveSchema); + if (visited.has(subtype)) { + return true; } - else { - subtypeStr = '['; - for (let i = 1; i < call.parameters.length; i++) { - subtypeStr += getFullyQualifiedName(typeChecker.getTypeOfSymbolAtLocation(call.parameters[i]!, symbolNode)) - + ', '; - } - subtypeStr = subtypeStr.slice(0, -2) + ']'; - getSchema = () => { - const result: PropertyMetaSchema[] = []; - for (let i = 1; i < call.parameters.length; i++) { - result.push(resolveSchema(typeChecker.getTypeOfSymbolAtLocation(call.parameters[i]!, symbolNode))); + + if (typeof options === 'object') { + for (const item of options.ignore ?? []) { + if (typeof item === 'function') { + const result = item(name, subtype, typeChecker); + if (typeof result === 'boolean') { + return result; + } } - return result; - }; + else if (name === item) { + return true; + } + } } + + return false; } - return { - name: (typeChecker.getTypeOfSymbolAtLocation(call.parameters[0]!, symbolNode) as ts.StringLiteralType).value, - description: ts.displayPartsToString(call.getDocumentationComment(typeChecker)), - tags: getJsDocTags(call), - type: subtypeStr, - signature: typeChecker.signatureToString(call), - get declarations() { - return declarations ??= call.declaration ? getDeclarations([call.declaration]) : []; - }, - get schema() { - return schema ??= getSchema(); - }, - rawType: rawType ? subtype : undefined, - getTypeObject() { - return subtype; - }, - }; - } - function resolveCallbackSchema(signature: ts.Signature): PropertyMetaSchema { - let schema: PropertyMetaSchema[] | undefined; + function reducer(acc: any, cur: any) { + acc[cur.name] = cur; + return acc; + } - return { - kind: 'event', - type: typeChecker.signatureToString(signature), - get schema() { - return schema ??= signature.parameters.length - ? typeChecker - .getTypeArguments( - typeChecker.getTypeOfSymbolAtLocation(signature.parameters[0]!, symbolNode) as ts.TypeReference, - ) - .map(resolveSchema) - : undefined; - }, - }; - } - function resolveSchema(subtype: ts.Type): PropertyMetaSchema { - const type = getFullyQualifiedName(subtype); + function getJsDocTags(target: ts.Symbol | ts.Signature) { + return target.getJsDocTags(typeChecker).map(tag => ({ + name: tag.name, + text: tag.text !== undefined ? ts.displayPartsToString(tag.text) : undefined, + })); + } - if (shouldIgnore(subtype)) { - return type; + function resolveNestedProperties(prop: ts.Symbol): PropertyMeta { + const subtype = typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode); + let schema: PropertyMetaSchema | undefined; + let declarations: Declaration[] | undefined; + + return { + name: prop.getEscapedName().toString(), + global: false, + description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), + tags: getJsDocTags(prop), + required: !(prop.flags & ts.SymbolFlags.Optional), + type: getFullyQualifiedName(subtype), + get declarations() { + return declarations ??= getDeclarations(prop.declarations ?? []); + }, + get schema() { + return schema ??= resolveSchema(subtype); + }, + rawType: rawType ? subtype : undefined, + getTypeObject() { + return subtype; + }, + }; } + function resolveSlotProperties(prop: ts.Symbol): SlotMeta { + const propType = typeChecker.getNonNullableType(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode)); + const signatures = propType.getCallSignatures(); + const paramType = signatures[0]?.parameters[0]; + const subtype = paramType + ? typeChecker.getTypeOfSymbolAtLocation(paramType, symbolNode) + : typeChecker.getAnyType(); + let schema: PropertyMetaSchema | undefined; + let declarations: Declaration[] | undefined; - visited.add(subtype); + return { + name: prop.getName(), + type: getFullyQualifiedName(subtype), + description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), + tags: getJsDocTags(prop), + get declarations() { + return declarations ??= getDeclarations(prop.declarations ?? []); + }, + get schema() { + return schema ??= resolveSchema(subtype); + }, + rawType: rawType ? subtype : undefined, + getTypeObject() { + return subtype; + }, + }; + } + function resolveExposedProperties(expose: ts.Symbol): ExposeMeta { + const subtype = typeChecker.getTypeOfSymbolAtLocation(expose, symbolNode); + let schema: PropertyMetaSchema | undefined; + let declarations: Declaration[] | undefined; - if (subtype.isUnion()) { - let schema: PropertyMetaSchema[] | undefined; return { - kind: 'enum', - type, + name: expose.getName(), + type: getFullyQualifiedName(subtype), + description: ts.displayPartsToString(expose.getDocumentationComment(typeChecker)), + tags: getJsDocTags(expose), + get declarations() { + return declarations ??= getDeclarations(expose.declarations ?? []); + }, get schema() { - return schema ??= subtype.types.map(resolveSchema); + return schema ??= resolveSchema(subtype); + }, + rawType: rawType ? subtype : undefined, + getTypeObject() { + return subtype; }, }; } - else if (typeChecker.isArrayLikeType(subtype)) { + function resolveEventSignature(call: ts.Signature): EventMeta { let schema: PropertyMetaSchema[] | undefined; + let declarations: Declaration[] | undefined; + let subtype: ts.Type | undefined; + let symbol: ts.Symbol | undefined; + let subtypeStr = '[]'; + let getSchema = () => [] as PropertyMetaSchema[]; + + if (call.parameters.length >= 2) { + symbol = call.parameters[1]!; + subtype = typeChecker.getTypeOfSymbolAtLocation(symbol, symbolNode); + if ((call.parameters[1]!.valueDeclaration as any)?.dotDotDotToken) { + subtypeStr = getFullyQualifiedName(subtype); + getSchema = () => typeChecker.getTypeArguments(subtype! as ts.TypeReference).map(resolveSchema); + } + else { + subtypeStr = '['; + for (let i = 1; i < call.parameters.length; i++) { + subtypeStr += getFullyQualifiedName(typeChecker.getTypeOfSymbolAtLocation(call.parameters[i]!, symbolNode)) + + ', '; + } + subtypeStr = subtypeStr.slice(0, -2) + ']'; + getSchema = () => { + const result: PropertyMetaSchema[] = []; + for (let i = 1; i < call.parameters.length; i++) { + result.push(resolveSchema(typeChecker.getTypeOfSymbolAtLocation(call.parameters[i]!, symbolNode))); + } + return result; + }; + } + } + return { - kind: 'array', - type, + name: (typeChecker.getTypeOfSymbolAtLocation(call.parameters[0]!, symbolNode) as ts.StringLiteralType).value, + description: ts.displayPartsToString(call.getDocumentationComment(typeChecker)), + tags: getJsDocTags(call), + type: subtypeStr, + signature: typeChecker.signatureToString(call), + get declarations() { + return declarations ??= call.declaration ? getDeclarations([call.declaration]) : []; + }, get schema() { - return schema ??= typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema); + return schema ??= getSchema(); + }, + rawType: rawType ? subtype : undefined, + getTypeObject() { + return subtype; }, }; } - else if ( - subtype.getCallSignatures().length === 0 - && (subtype.isClassOrInterface() || subtype.isIntersection() - || (subtype as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) - ) { - let schema: Record | undefined; + function resolveCallbackSchema(signature: ts.Signature): PropertyMetaSchema { + let schema: PropertyMetaSchema[] | undefined; + return { - kind: 'object', - type, + kind: 'event', + type: typeChecker.signatureToString(signature), get schema() { - return schema ??= subtype.getProperties().map(resolveNestedProperties).reduce(reducer, {}); + return schema ??= signature.parameters.length + ? typeChecker + .getTypeArguments( + typeChecker.getTypeOfSymbolAtLocation(signature.parameters[0]!, symbolNode) as ts.TypeReference, + ) + .map(resolveSchema) + : undefined; }, }; } - else if (subtype.getCallSignatures().length === 1) { - return resolveCallbackSchema(subtype.getCallSignatures()[0]!); - } + function resolveSchema(subtype: ts.Type): PropertyMetaSchema { + const type = getFullyQualifiedName(subtype); - return type; - } - function getFullyQualifiedName(type: ts.Type) { - const str = typeChecker.typeToString( - type, - undefined, - ts.TypeFormatFlags.UseFullyQualifiedType | ts.TypeFormatFlags.NoTruncation, - ); - if (str.includes('import(')) { - return str.replace(/import\(.*?\)\./g, ''); + if (shouldIgnore(subtype)) { + return type; + } + + visited.add(subtype); + + if (subtype.isUnion()) { + let schema: PropertyMetaSchema[] | undefined; + return { + kind: 'enum', + type, + get schema() { + return schema ??= subtype.types.map(resolveSchema); + }, + }; + } + else if (typeChecker.isArrayLikeType(subtype)) { + let schema: PropertyMetaSchema[] | undefined; + return { + kind: 'array', + type, + get schema() { + return schema ??= typeChecker.getTypeArguments(subtype as ts.TypeReference).map(resolveSchema); + }, + }; + } + else if ( + subtype.getCallSignatures().length === 0 + && (subtype.isClassOrInterface() || subtype.isIntersection() + || (subtype as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous) + ) { + let schema: Record | undefined; + return { + kind: 'object', + type, + get schema() { + return schema ??= subtype.getProperties().map(resolveNestedProperties).reduce(reducer, {}); + }, + }; + } + else if (subtype.getCallSignatures().length === 1) { + return resolveCallbackSchema(subtype.getCallSignatures()[0]!); + } + + return type; } - return str; - } - function getDeclarations(declaration: ts.Declaration[]) { - if (noDeclarations) { - return []; + function getFullyQualifiedName(type: ts.Type) { + const str = typeChecker.typeToString( + type, + undefined, + ts.TypeFormatFlags.UseFullyQualifiedType | ts.TypeFormatFlags.NoTruncation, + ); + if (str.includes('import(')) { + return str.replace(/import\(.*?\)\./g, ''); + } + return str; } - return declaration.map(getDeclaration).filter(d => !!d); - } - function getDeclaration(declaration: ts.Declaration): Declaration | undefined { - const fileName = declaration.getSourceFile().fileName; - const sourceScript = language.scripts.get(fileName); - if (sourceScript?.generated) { - const script = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); - if (script) { - for (const [sourceScript, map] of language.maps.forEach(script.code)) { - for (const [start] of map.toSourceLocation(declaration.getStart())) { - for (const [end] of map.toSourceLocation(declaration.getEnd())) { - return { - file: sourceScript.id, - range: [start, end], - }; + function getDeclarations(declaration: ts.Declaration[]) { + if (noDeclarations) { + return []; + } + return declaration.map(getDeclaration).filter(d => !!d); + } + function getDeclaration(declaration: ts.Declaration): Declaration | undefined { + const fileName = declaration.getSourceFile().fileName; + const sourceScript = language.scripts.get(fileName); + if (sourceScript?.generated) { + const script = sourceScript.generated.languagePlugin.typescript?.getServiceScript(sourceScript.generated.root); + if (script) { + for (const [sourceScript, map] of language.maps.forEach(script.code)) { + for (const [start] of map.toSourceLocation(declaration.getStart())) { + for (const [end] of map.toSourceLocation(declaration.getEnd())) { + return { + file: sourceScript.id, + range: [start, end], + }; + } } } } + return; } - return undefined; + return { + file: declaration.getSourceFile().fileName, + range: [declaration.getStart(), declaration.getEnd()], + }; } + return { - file: declaration.getSourceFile().fileName, - range: [declaration.getStart(), declaration.getEnd()], + resolveNestedProperties, + resolveSlotProperties, + resolveEventSignature, + resolveExposedProperties, + resolveSchema, }; } - - return { - resolveNestedProperties, - resolveSlotProperties, - resolveEventSignature, - resolveExposedProperties, - resolveSchema, - }; } -function readVueComponentDefaultProps( - root: core.VueVirtualCode, - printer: ts.Printer | undefined, +function readDefaultsFromScriptSetup( ts: typeof import('typescript'), + printer: ts.Printer, + sourceFile: ts.SourceFile, + scriptSetupRanges: core.ScriptSetupRanges, ) { - let result: Record = {}; - const { sfc } = root; - - scriptSetupWorker(); - scriptWorker(); - - return result; - - function scriptSetupWorker() { - if (!sfc.scriptSetup) { - return; - } - const { ast } = sfc.scriptSetup; - - const codegen = core.tsCodegen.get(sfc); - const scriptSetupRanges = codegen?.getScriptSetupRanges(); - - if (scriptSetupRanges?.withDefaults?.argNode) { - const obj = findObjectLiteralExpression(scriptSetupRanges.withDefaults.argNode); - if (obj) { - for (const prop of obj.properties) { - if (ts.isPropertyAssignment(prop)) { - const name = prop.name.getText(ast); - const expNode = resolveDefaultOptionExpression(prop.initializer, ts); - const expText = printer?.printNode(ts.EmitHint.Expression, expNode, ast) ?? expNode.getText(ast); - - result[name] = { - default: expText, - }; - } + const result: Record = {}; + + if (scriptSetupRanges.withDefaults?.arg) { + const obj = findObjectLiteralExpression(ts, scriptSetupRanges.withDefaults.arg.node); + if (obj) { + for (const prop of obj.properties) { + if (ts.isPropertyAssignment(prop)) { + const name = prop.name.getText(sourceFile); + const expNode = resolveDefaultOptionExpression(ts, prop.initializer); + const expText = printer.printNode(ts.EmitHint.Expression, expNode, sourceFile) + ?? expNode.getText(sourceFile); + result[name] = { default: expText }; } } } - else if (scriptSetupRanges?.defineProps?.argNode) { - const obj = findObjectLiteralExpression(scriptSetupRanges.defineProps.argNode); - if (obj) { - result = { - ...result, - ...resolvePropsOption(ast, obj, printer, ts), - }; - } - } - else if (scriptSetupRanges?.defineProps?.destructured) { - for (const [name, initializer] of scriptSetupRanges.defineProps.destructured) { - if (initializer) { - const expText = printer?.printNode(ts.EmitHint.Expression, initializer, ast) ?? initializer.getText(ast); - result[name] = { - default: expText, - }; - } - } + } + else if (scriptSetupRanges.defineProps?.arg) { + const obj = findObjectLiteralExpression(ts, scriptSetupRanges.defineProps.arg.node); + if (obj) { + Object.assign( + result, + resolvePropsOption(ts, printer, sourceFile, obj), + ); } - - if (scriptSetupRanges?.defineModel) { - for (const defineModel of scriptSetupRanges.defineModel) { - const obj = defineModel.argNode ? findObjectLiteralExpression(defineModel.argNode) : undefined; - if (obj) { - const name = defineModel.name - ? sfc.scriptSetup.content.slice(defineModel.name.start, defineModel.name.end).slice(1, -1) - : 'modelValue'; - result[name] = resolveModelOption(ast, obj, printer, ts); - } + } + else if (scriptSetupRanges.defineProps?.destructured) { + for (const [name, initializer] of scriptSetupRanges.defineProps.destructured) { + if (initializer) { + const expText = printer.printNode(ts.EmitHint.Expression, initializer, sourceFile) + ?? initializer.getText(sourceFile); + result[name] = { default: expText }; } } + } - function findObjectLiteralExpression(node: ts.Node) { - if (ts.isObjectLiteralExpression(node)) { - return node; + if (scriptSetupRanges.defineModel) { + for (const defineModel of scriptSetupRanges.defineModel) { + const obj = defineModel.arg ? findObjectLiteralExpression(ts, defineModel.arg.node) : undefined; + if (obj) { + const name = defineModel.name + ? sourceFile.text.slice(defineModel.name.start, defineModel.name.end).slice(1, -1) + : 'modelValue'; + result[name] = { default: resolveModelOption(ts, printer, sourceFile, obj) }; } - let result: ts.ObjectLiteralExpression | undefined; - node.forEachChild(child => { - if (!result) { - result = findObjectLiteralExpression(child); - } - }); - return result; } } - function scriptWorker() { - if (!sfc.script) { - return; - } - const { ast } = sfc.script; - - const scriptResult = readTsComponentDefaultProps(ast, 'default', printer, ts); - for (const [key, value] of Object.entries(scriptResult)) { - result[key] = value; - } - } + return result; } -function readTsComponentDefaultProps( - ast: ts.SourceFile, - exportName: string, - printer: ts.Printer | undefined, +function findObjectLiteralExpression( ts: typeof import('typescript'), + node: ts.Node, ) { - const props = getPropsNode(); - - if (props) { - return resolvePropsOption(ast, props, printer, ts); - } - - return {}; - - function getComponentNode() { - return getComponentNodeFromAst(ast, exportName, ts); + if (ts.isObjectLiteralExpression(node)) { + return node; } + let result: ts.ObjectLiteralExpression | undefined; + node.forEachChild(child => { + if (!result) { + result = findObjectLiteralExpression(ts, child); + } + }); + return result; +} - function getComponentOptionsNode() { - const component = getComponentNode(); - return getComponentOptionsNodeFromComponent(component, ts); +function readDefaultsFromScript( + ts: typeof import('typescript'), + printer: ts.Printer, + sourceFile: ts.SourceFile, + scriptRanges: core.ScriptRanges, + exportName: string, +) { + const component = scriptRanges.exports[exportName]; + if (!component) { + return {}; } - - function getPropsNode() { - const options = getComponentOptionsNode(); - const props = options?.properties.find(prop => prop.name?.getText(ast) === 'props'); - if (props && ts.isPropertyAssignment(props)) { - if (ts.isObjectLiteralExpression(props.initializer)) { - return props.initializer; - } + const props = component?.options?.args.node.properties.find(prop => prop.name?.getText(sourceFile) === 'props'); + if (props && ts.isPropertyAssignment(props)) { + if (ts.isObjectLiteralExpression(props.initializer)) { + return resolvePropsOption(ts, printer, sourceFile, props.initializer); } } + return {}; } function resolvePropsOption( - ast: ts.SourceFile, - props: ts.ObjectLiteralExpression, - printer: ts.Printer | undefined, ts: typeof import('typescript'), + printer: ts.Printer, + sourceFile: ts.SourceFile, + props: ts.ObjectLiteralExpression, ) { const result: Record = {}; for (const prop of props.properties) { if (ts.isPropertyAssignment(prop)) { - const name = prop.name.getText(ast); + const name = prop.name.getText(sourceFile); if (ts.isObjectLiteralExpression(prop.initializer)) { const defaultProp = prop.initializer.properties.find(p => - ts.isPropertyAssignment(p) && p.name.getText(ast) === 'default' + ts.isPropertyAssignment(p) && p.name.getText(sourceFile) === 'default' ) as ts.PropertyAssignment | undefined; const requiredProp = prop.initializer.properties.find(p => - ts.isPropertyAssignment(p) && p.name.getText(ast) === 'required' + ts.isPropertyAssignment(p) && p.name.getText(sourceFile) === 'required' ) as ts.PropertyAssignment | undefined; result[name] = {}; if (requiredProp) { - const exp = requiredProp.initializer.getText(ast); + const exp = requiredProp.initializer.getText(sourceFile); result[name].required = exp === 'true'; } if (defaultProp) { - const expNode = resolveDefaultOptionExpression(defaultProp.initializer, ts); - const expText = printer?.printNode(ts.EmitHint.Expression, expNode, ast) ?? expNode.getText(ast); + const expNode = resolveDefaultOptionExpression(ts, defaultProp.initializer); + const expText = printer.printNode(ts.EmitHint.Expression, expNode, sourceFile) + ?? expNode.getText(sourceFile); result[name].default = expText; } } @@ -1062,30 +973,30 @@ function resolvePropsOption( } function resolveModelOption( - ast: ts.SourceFile, - options: ts.ObjectLiteralExpression, - printer: ts.Printer | undefined, ts: typeof import('typescript'), + printer: ts.Printer, + sourceFile: ts.SourceFile, + options: ts.ObjectLiteralExpression, ) { - const result: { default?: string } = {}; + let _default: string | undefined; for (const prop of options.properties) { if (ts.isPropertyAssignment(prop)) { - const name = prop.name.getText(ast); + const name = prop.name.getText(sourceFile); if (name === 'default') { - const expNode = resolveDefaultOptionExpression(prop.initializer, ts); - const expText = printer?.printNode(ts.EmitHint.Expression, expNode, ast) ?? expNode.getText(ast); - result.default = expText; + const expNode = resolveDefaultOptionExpression(ts, prop.initializer); + const expText = printer.printNode(ts.EmitHint.Expression, expNode, sourceFile) ?? expNode.getText(sourceFile); + _default = expText; } } } - return result; + return _default; } function resolveDefaultOptionExpression( - _default: ts.Expression, ts: typeof import('typescript'), + _default: ts.Expression, ) { if (ts.isArrowFunction(_default)) { if (ts.isBlock(_default.body)) { @@ -1101,38 +1012,17 @@ function resolveDefaultOptionExpression( return _default; } -function readComponentName( - ast: ts.SourceFile, - exportName: string, - ts: typeof import('typescript'), -): string | undefined { - const componentNode = getComponentNodeFromAst(ast, exportName, ts); - const optionsNode = getComponentOptionsNodeFromComponent(componentNode, ts); - - if (optionsNode) { - const nameProp = optionsNode.properties.find( - prop => ts.isPropertyAssignment(prop) && prop.name?.getText(ast) === 'name', - ); - - if (nameProp && ts.isPropertyAssignment(nameProp) && ts.isStringLiteral(nameProp.initializer)) { - return nameProp.initializer.text; - } - } - - return undefined; -} - function readComponentDescription( - ast: ts.SourceFile, - exportName: string, ts: typeof import('typescript'), + scriptRanges: core.ScriptRanges, + exportName: string, typeChecker: ts.TypeChecker, ): string | undefined { - const exportNode = getExportNode(); + const _export = scriptRanges.exports[exportName]; - if (exportNode) { + if (_export) { // Try to get JSDoc comments from the node using TypeScript API - const jsDocComments = ts.getJSDocCommentsAndTags(exportNode); + const jsDocComments = ts.getJSDocCommentsAndTags(_export.node); for (const jsDoc of jsDocComments) { if (ts.isJSDoc(jsDoc) && jsDoc.comment) { // Handle both string and array of comment parts @@ -1146,38 +1036,10 @@ function readComponentDescription( } // Fallback to symbol documentation - const symbol = typeChecker.getSymbolAtLocation(exportNode); + const symbol = typeChecker.getSymbolAtLocation(_export.node); if (symbol) { const description = ts.displayPartsToString(symbol.getDocumentationComment(typeChecker)); return description || undefined; } } - - return undefined; - - function getExportNode() { - let result: ts.Node | undefined; - - if (exportName === 'default') { - ast.forEachChild(child => { - if (ts.isExportAssignment(child)) { - // Return the export assignment itself, not the expression - result = child; - } - }); - } - else { - ast.forEachChild(child => { - if ( - ts.isVariableStatement(child) - && child.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword) - ) { - // Return the variable statement itself - result = child; - } - }); - } - - return result; - } } diff --git a/packages/component-meta/tests/index.spec.ts b/packages/component-meta/tests/index.spec.ts index 766b964a57..43beefcdd9 100644 --- a/packages/component-meta/tests/index.spec.ts +++ b/packages/component-meta/tests/index.spec.ts @@ -661,7 +661,7 @@ const worker = (checker: ComponentMetaChecker, withTsconfig: boolean) => expect(bar).toMatchInlineSnapshot(` { "declarations": [], - "default": ""BAR"", + "default": "'BAR'", "description": "", "getTypeObject": [Function], "global": false, @@ -1459,7 +1459,7 @@ const worker = (checker: ComponentMetaChecker, withTsconfig: boolean) => { "declarations": [], "default": "{ - foo: "bar", + foo: 'bar', }", "description": "Default function Object", "getTypeObject": [Function], diff --git a/packages/language-core/index.ts b/packages/language-core/index.ts index d3509df678..aba7542a74 100644 --- a/packages/language-core/index.ts +++ b/packages/language-core/index.ts @@ -1,6 +1,7 @@ export * from './lib/codegen/template'; export * from './lib/compilerOptions'; export * from './lib/languagePlugin'; +export * from './lib/parsers/scriptRanges'; export * from './lib/parsers/scriptSetupRanges'; export * from './lib/plugins'; export * from './lib/types'; diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index 08e2bd4283..ba42507f19 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -71,7 +71,7 @@ function* generateWorker( //