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