diff --git a/packages/component-meta/lib/base.ts b/packages/component-meta/lib/base.ts index 70ad4ff6c0..ca7a50d536 100644 --- a/packages/component-meta/lib/base.ts +++ b/packages/component-meta/lib/base.ts @@ -2,6 +2,13 @@ import { createLanguageServiceHost, resolveFileLanguageId, type TypeScriptProjec import * as core from '@vue/language-core'; import { posix as path } from 'path-browserify'; import type * as ts from 'typescript'; +import { + inferComponentEmit, + inferComponentExposed, + inferComponentProps, + inferComponentSlots, + inferComponentType, +} from './helpers'; import type { ComponentMeta, @@ -17,6 +24,12 @@ import type { export * from './types'; const windowsPathReg = /\\/g; +const publicPropsInterfaces = new Set([ + 'PublicProps', + 'VNodeProps', + 'AllowedComponentProps', + 'ComponentCustomProps', +]); export function createCheckerByJsonConfigBase( ts: typeof import('typescript'), @@ -47,7 +60,6 @@ export function createCheckerByJsonConfigBase( }, checkerOptions, rootDir, - path.join(rootDir, 'jsconfig.json.global.vue'), ); } @@ -79,7 +91,6 @@ export function createCheckerBase( }, checkerOptions, path.dirname(tsconfig), - tsconfig + '.global.vue', ); } @@ -91,7 +102,6 @@ function baseCreate( ], checkerOptions: MetaCheckerOptions, rootPath: string, - globalComponentName: string, ) { let [{ vueOptions, options, projectReferences }, fileNames] = getConfigAndFiles(); /** @@ -108,20 +118,7 @@ function baseCreate( getScriptFileNames: () => [...fileNamesSet], getProjectReferences: () => projectReferences, }; - const globalComponentSnapshot = ts.ScriptSnapshot.fromString(''); const scriptSnapshots = new Map(); - const metaSnapshots = new Map(); - const getScriptFileNames = projectHost.getScriptFileNames; - projectHost.getScriptFileNames = () => { - const names = getScriptFileNames(); - return [ - ...names, - ...names.map(getMetaFileName), - globalComponentName, - getMetaFileName(globalComponentName), - ]; - }; - const vueLanguagePlugin = core.createVueLanguagePlugin( ts, projectHost.getCompilationSettings(), @@ -141,27 +138,16 @@ function baseCreate( fileName => { let snapshot = scriptSnapshots.get(fileName); - if (fileName === globalComponentName) { - snapshot = globalComponentSnapshot; - } - else if (isMetaFileName(fileName)) { - if (!metaSnapshots.has(fileName)) { - metaSnapshots.set(fileName, ts.ScriptSnapshot.fromString(getMetaScriptContent(fileName))); + if (!scriptSnapshots.has(fileName)) { + const fileText = ts.sys.readFile(fileName); + if (fileText !== undefined) { + scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(fileText)); } - snapshot = metaSnapshots.get(fileName); - } - else { - if (!scriptSnapshots.has(fileName)) { - const fileText = ts.sys.readFile(fileName); - if (fileText !== undefined) { - scriptSnapshots.set(fileName, ts.ScriptSnapshot.fromString(fileText)); - } - else { - scriptSnapshots.set(fileName, undefined); - } + else { + scriptSnapshots.set(fileName, undefined); } - snapshot = scriptSnapshots.get(fileName); } + snapshot = scriptSnapshots.get(fileName); if (snapshot) { language.scripts.set(fileName, snapshot); @@ -191,8 +177,6 @@ function baseCreate( }; } - let globalPropNames: string[] | undefined; - return { getExportNames, getComponentMeta, @@ -222,113 +206,82 @@ function baseCreate( }, }; - function isMetaFileName(fileName: string) { - return fileName.endsWith('.meta.ts'); - } - - function getMetaFileName(fileName: string) { - return ( - vueOptions.extensions.some(ext => fileName.endsWith(ext)) - ? fileName - : fileName.slice(0, fileName.lastIndexOf('.')) - ) + '.meta.ts'; - } - - function getMetaScriptContent(fileName: string) { - const helpersPath = require.resolve('vue-component-type-helpers').replace(windowsPathReg, '/'); - let helpersRelativePath = path.relative(path.dirname(fileName), helpersPath); - if (!helpersRelativePath.startsWith('./') && !helpersRelativePath.startsWith('../')) { - helpersRelativePath = './' + helpersRelativePath; - } - let code = ` -import type { ComponentType, ComponentProps, ComponentEmit, ComponentSlots, ComponentExposed } from '${helpersRelativePath}'; -import type * as Components from '${fileName.slice(0, -'.meta.ts'.length)}'; - -export default {} as { [K in keyof typeof Components]: ComponentMeta; }; - -interface ComponentMeta { - type: ComponentType; - props: ComponentProps; - emit: ComponentEmit; - slots: ComponentSlots; - exposed: ComponentExposed; -} -`.trim(); - return code; - } - function getExportNames(componentPath: string) { const program = tsLs.getProgram()!; - const typeChecker = program.getTypeChecker(); - return _getExports(program, typeChecker, componentPath).exports.map(e => e.getName()); + const sourceFile = program.getSourceFile(componentPath); + if (sourceFile) { + const scriptRanges = getScriptRanges(sourceFile); + return Object.keys(scriptRanges.exports); + } } function getComponentMeta(componentPath: string, exportName = 'default'): ComponentMeta { - const program = tsLs.getProgram()!; - const typeChecker = program.getTypeChecker(); - const { symbolNode, exports } = _getExports(program, typeChecker, componentPath); - const _export = exports.find(property => property.getName() === exportName); + let program = tsLs.getProgram()!; + let sourceFile = program.getSourceFile(componentPath); + if (!sourceFile) { + fileNamesSet.add(componentPath); + projectVersion++; + program = tsLs.getProgram()!; + sourceFile = program.getSourceFile(componentPath); + if (!sourceFile) { + throw `Could not find component file: ${componentPath}`; + } + } - if (!_export) { + const scriptRanges = getScriptRanges(sourceFile); + const component = scriptRanges.exports[exportName]; + if (!component) { throw `Could not find export ${exportName}`; } - const componentType = typeChecker.getTypeOfSymbolAtLocation(_export, symbolNode); - const symbolProperties = componentType.getProperties(); + const symbolNode = component.expression.node; + const typeChecker = program.getTypeChecker(); - let _type: ReturnType | undefined; - let _props: ReturnType | undefined; - let _events: ReturnType | undefined; - let _slots: ReturnType | undefined; - let _exposed: ReturnType | undefined; - let _name: string | undefined; - let _description: string | undefined; + let name: string | undefined; + let description: string | undefined; + let type: ReturnType | undefined; + let props: ReturnType | undefined; + let events: ReturnType | undefined; + let slots: ReturnType | undefined; + let exposed: ReturnType | undefined; const meta = { get name() { - return _name ?? (_name = getName()); + return name ?? (name = getName()); }, get description() { - return _description ?? (_description = getDescription()); + return description ?? (description = getDescription()); }, get type() { - return _type ?? (_type = getType()); + return type ?? (type = getType()); }, get props() { - return _props ?? (_props = getProps()); + return props ?? (props = getProps()); }, get events() { - return _events ?? (_events = getEvents()); + return events ?? (events = getEvents()); }, get slots() { - return _slots ?? (_slots = getSlots()); + return slots ?? (slots = getSlots()); }, get exposed() { - return _exposed ?? (_exposed = getExposed()); + return exposed ?? (exposed = getExposed()); }, }; return meta; function getType() { - const $type = symbolProperties.find(prop => prop.escapedName === 'type'); - - if ($type) { - const type = typeChecker.getTypeOfSymbolAtLocation($type, symbolNode); - return Number(typeChecker.typeToString(type)); - } - - return 0; + return inferComponentType(typeChecker, symbolNode) ?? 0; } function getProps() { - const $props = symbolProperties.find(prop => prop.escapedName === 'props'); + const propsType = inferComponentProps(typeChecker, symbolNode); const vnodeEventRegex = /^onVnode[A-Z]/; let result: PropertyMeta[] = []; - if ($props) { - const type = typeChecker.getTypeOfSymbolAtLocation($props, symbolNode); - const properties = type.getProperties(); + if (propsType) { + const properties = propsType.getProperties(); const eventProps = new Set( meta.events.map(event => `on${event.name.charAt(0).toUpperCase()}${event.name.slice(1)}`), @@ -345,14 +298,6 @@ interface ComponentMeta { .filter(prop => !vnodeEventRegex.test(prop.name) && !eventProps.has(prop.name)); } - // fill global - if (componentPath !== globalComponentName) { - globalPropNames ??= getComponentMeta(globalComponentName).props.map(prop => prop.name); - for (const prop of result) { - prop.global = globalPropNames.includes(prop.name); - } - } - // fill defaults const sourceScript = language.scripts.get(componentPath); const sourceFile = program.getSourceFile(componentPath); @@ -403,11 +348,10 @@ interface ComponentMeta { } function getEvents() { - const $emit = symbolProperties.find(prop => prop.escapedName === 'emit'); + const emitType = inferComponentEmit(typeChecker, symbolNode); - if ($emit) { - const type = typeChecker.getTypeOfSymbolAtLocation($emit, symbolNode); - const calls = type.getCallSignatures(); + if (emitType) { + const calls = emitType.getCallSignatures(); return calls.map(call => { const { @@ -422,11 +366,10 @@ interface ComponentMeta { } function getSlots() { - const $slots = symbolProperties.find(prop => prop.escapedName === 'slots'); + const slotsType = inferComponentSlots(typeChecker, symbolNode); - if ($slots) { - const type = typeChecker.getTypeOfSymbolAtLocation($slots, symbolNode); - const properties = type.getProperties(); + if (slotsType) { + const properties = slotsType.getProperties(); return properties.map(prop => { const { @@ -441,13 +384,12 @@ interface ComponentMeta { } function getExposed() { - const $exposed = symbolProperties.find(prop => prop.escapedName === 'exposed'); + const exposedType = inferComponentExposed(typeChecker, symbolNode); - if ($exposed) { - const $props = symbolProperties.find(prop => prop.escapedName === 'props'); - const propsProperties = $props ? typeChecker.getTypeOfSymbolAtLocation($props, symbolNode).getProperties() : []; - const type = typeChecker.getTypeOfSymbolAtLocation($exposed, symbolNode); - const properties = type.getProperties().filter(prop => + if (exposedType) { + const propsType = inferComponentProps(typeChecker, symbolNode); + const propsProperties = propsType?.getProperties() ?? []; + const properties = exposedType.getProperties().filter(prop => // only exposed props will have at least one declaration and no valueDeclaration prop.declarations?.length && !prop.valueDeclaration @@ -489,46 +431,6 @@ interface ComponentMeta { } } - function _getExports( - program: ts.Program, - typeChecker: ts.TypeChecker, - componentPath: string, - ) { - const sourceFile = program.getSourceFile(getMetaFileName(componentPath)); - if (!sourceFile) { - throw 'Could not find main source file'; - } - - const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile); - if (!moduleSymbol) { - throw 'Could not find module symbol'; - } - - const exportedSymbols = typeChecker.getExportsOfModule(moduleSymbol); - - let symbolNode: ts.Expression | undefined; - - for (const symbol of exportedSymbols) { - const [declaration] = symbol.getDeclarations() ?? []; - - if (declaration && ts.isExportAssignment(declaration)) { - symbolNode = declaration.expression; - } - } - - if (!symbolNode) { - throw 'Could not find symbol node'; - } - - const exportDefaultType = typeChecker.getTypeAtLocation(symbolNode); - const exports = exportDefaultType.getProperties(); - - return { - symbolNode, - exports, - }; - } - function getScriptRanges(sourceFile: ts.SourceFile) { let scriptRanges = scriptRangesCache.get(sourceFile); if (!scriptRanges) { @@ -605,10 +507,20 @@ function createSchemaResolvers( const subtype = typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode); let schema: PropertyMetaSchema | undefined; let declarations: Declaration[] | undefined; + let global = false; + + for (const decl of prop.declarations ?? []) { + if ( + decl.getSourceFile() !== symbolNode.getSourceFile() + && isPublicProp(decl) + ) { + global = true; + } + } return { name: prop.getEscapedName().toString(), - global: false, + global, description: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)), tags: getJsDocTags(prop), required: !(prop.flags & ts.SymbolFlags.Optional), @@ -625,6 +537,21 @@ function createSchemaResolvers( }, }; } + + function isPublicProp(declaration: ts.Declaration): boolean { + let parent = declaration.parent; + while (parent) { + if (ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)) { + if (publicPropsInterfaces.has(parent.name.text)) { + return true; + } + return false; + } + parent = parent.parent; + } + return false; + } + function resolveSlotProperties(prop: ts.Symbol): SlotMeta { const propType = typeChecker.getNonNullableType(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode)); const signatures = propType.getCallSignatures(); diff --git a/packages/component-meta/lib/helpers.ts b/packages/component-meta/lib/helpers.ts new file mode 100644 index 0000000000..750691bda1 --- /dev/null +++ b/packages/component-meta/lib/helpers.ts @@ -0,0 +1,153 @@ +import type * as ts from 'typescript'; + +export function inferComponentType( + typeChecker: ts.TypeChecker, + symbolNode: ts.Node, +) { + const componentType = typeChecker.getTypeAtLocation(symbolNode); + const constructSignatures = componentType.getConstructSignatures(); + const callSignatures = componentType.getCallSignatures(); + + for (const _sig of constructSignatures) { + return 1; + } + + for (const _sig of callSignatures) { + return 2; + } +} + +export function inferComponentProps( + typeChecker: ts.TypeChecker, + symbolNode: ts.Node, +): ts.Type | undefined { + const componentType = typeChecker.getTypeAtLocation(symbolNode); + const constructSignatures = componentType.getConstructSignatures(); + const callSignatures = componentType.getCallSignatures(); + + for (const sig of constructSignatures) { + const retType = sig.getReturnType(); + const props = findProperty(typeChecker, symbolNode, retType, '$props'); + if (props) { + return props; + } + } + + for (const sig of callSignatures) { + if (sig.parameters.length > 0) { + const props = sig.parameters[0]; + if (props) { + return typeChecker.getTypeOfSymbolAtLocation(props, symbolNode); + } + } + } +} + +export function inferComponentSlots( + typeChecker: ts.TypeChecker, + symbolNode: ts.Node, +): ts.Type | undefined { + const componentType = typeChecker.getTypeAtLocation(symbolNode); + const constructSignatures = componentType.getConstructSignatures(); + const callSignatures = componentType.getCallSignatures(); + + for (const sig of constructSignatures) { + const retType = sig.getReturnType(); + const slots = findProperty(typeChecker, symbolNode, retType, '$slots'); + if (slots) { + return slots; + } + } + + for (const sig of callSignatures) { + if (sig.parameters.length > 1) { + const ctxParam = sig.parameters[1]; + if (ctxParam) { + const ctxType = typeChecker.getTypeOfSymbolAtLocation(ctxParam, symbolNode); + const slots = findProperty(typeChecker, symbolNode, ctxType, 'slots'); + if (slots) { + return slots; + } + } + } + } +} + +export function inferComponentEmit( + typeChecker: ts.TypeChecker, + symbolNode: ts.Node, +): ts.Type | undefined { + const componentType = typeChecker.getTypeAtLocation(symbolNode); + const constructSignatures = componentType.getConstructSignatures(); + const callSignatures = componentType.getCallSignatures(); + + for (const sig of constructSignatures) { + const retType = sig.getReturnType(); + const emit = findProperty(typeChecker, symbolNode, retType, '$emit'); + if (emit) { + return emit; + } + } + + for (const sig of callSignatures) { + if (sig.parameters.length > 1) { + const ctxParam = sig.parameters[1]; + if (ctxParam) { + const ctxType = typeChecker.getTypeOfSymbolAtLocation(ctxParam, symbolNode); + const emitType = findProperty(typeChecker, symbolNode, ctxType, 'emit'); + if (emitType) { + return emitType; + } + } + } + } +} + +export function inferComponentExposed( + typeChecker: ts.TypeChecker, + symbolNode: ts.Node, +): ts.Type | undefined { + const componentType = typeChecker.getTypeAtLocation(symbolNode); + const constructSignatures = componentType.getConstructSignatures(); + const callSignatures = componentType.getCallSignatures(); + + for (const sig of constructSignatures) { + return sig.getReturnType(); + } + + for (const sig of callSignatures) { + if (sig.parameters.length > 2) { + const exposeParam = sig.parameters[2]; + if (exposeParam) { + const exposeType = typeChecker.getTypeOfSymbolAtLocation(exposeParam, symbolNode); + const callSignatures = exposeType.getCallSignatures(); + for (const callSig of callSignatures) { + const params = callSig.getParameters(); + if (params.length > 0) { + return typeChecker.getTypeOfSymbolAtLocation(params[0]!, symbolNode); + } + } + } + } + } +} + +function findProperty( + typeChecker: ts.TypeChecker, + location: ts.Node, + type: ts.Type, + property: string, +): ts.Type | undefined { + const symbol = type.getProperty(property); + if (symbol) { + return typeChecker.getTypeOfSymbolAtLocation(symbol, location); + } + if (type.isUnionOrIntersection()) { + for (const sub of type.types) { + const found = findProperty(typeChecker, location, sub, property); + if (found) { + return found; + } + } + } +} diff --git a/packages/component-meta/package.json b/packages/component-meta/package.json index 48a4946fa1..cf1a9ce2a9 100644 --- a/packages/component-meta/package.json +++ b/packages/component-meta/package.json @@ -15,8 +15,7 @@ "dependencies": { "@volar/typescript": "2.4.27", "@vue/language-core": "workspace:*", - "path-browserify": "^1.0.1", - "vue-component-type-helpers": "workspace:*" + "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" diff --git a/packages/language-core/lib/parsers/scriptRanges.ts b/packages/language-core/lib/parsers/scriptRanges.ts index 2d04882b62..6bbd79211f 100644 --- a/packages/language-core/lib/parsers/scriptRanges.ts +++ b/packages/language-core/lib/parsers/scriptRanges.ts @@ -14,7 +14,7 @@ export function parseScriptRanges( const _exports: Record< 'default' | string, TextRange & { - expression: TextRange; + expression: TextRange; isObjectLiteral: boolean; options?: { isObjectLiteral: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9453ea4e74..89768bcba6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,9 +89,6 @@ importers: typescript: specifier: '*' version: 5.9.3 - vue-component-type-helpers: - specifier: workspace:* - version: link:../component-type-helpers devDependencies: '@types/node': specifier: ^22.10.4