diff --git a/README.md b/README.md
index af7ef8b21..081647a94 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@
*VSCode extension to support Vue in TS server*
- [vue-tsc](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc) \
*Type-check and dts build command line tool*
+- [vue-component-meta](https://github.com/johnsoncodehk/volar/tree/master/packages/vue-component-meta) (Experimental) \
+*Component props, events, slots types information extract tool*
- [vite-plugin-vue-component-preview](https://github.com/johnsoncodehk/vite-plugin-vue-component-preview) \
*Vite plugin for support Vue component preview view with `Vue Language Features`*
@@ -132,6 +134,7 @@ flowchart LR
%% VOLAR_TS_FASTER["@volar/typescript-faster"]
%% VOLAR_PREVIEW["@volar/preview"]
VUE_TSC[vue-tsc]
+ VUE_COMPONENT_META[vue-component-meta]
TS_VUE_PLUGIN[typescript-vue-plugin]
click VOLAR_VUE_SERVER "https://github.com/johnsoncodehk/volar/tree/master/packages/vue-language-server"
@@ -144,6 +147,7 @@ flowchart LR
click VOLAR_TS_FASTER "https://github.com/johnsoncodehk/volar/tree/master/packages/typescript-faster"
click VOLAR_PREVIEW "https://github.com/johnsoncodehk/volar/tree/master/packages/preview"
click VUE_TSC "https://github.com/johnsoncodehk/volar/tree/master/packages/vue-tsc"
+ click VUE_COMPONENT_META "https://github.com/johnsoncodehk/volar/tree/master/packages/vue-component-meta"
click TS_VUE_PLUGIN "https://github.com/johnsoncodehk/volar/tree/master/packages/typescript-vue-plugin"
%% Extrnal Packages
@@ -226,6 +230,7 @@ flowchart LR
%% VOLAR_VUE_SERVICE --> VOLAR_TS_FASTER
%% VOLAR_VUE_TS --> TS
+ VUE_COMPONENT_META --> VOLAR_VUE_CORE
VOLAR_VUE_TS --> VOLAR_VUE_CORE
VOLAR_VUE_SERVICE --> VOLAR_VUE_CORE
diff --git a/packages/vue-component-meta/LICENSE b/packages/vue-component-meta/LICENSE
new file mode 100644
index 000000000..b55e47a7e
--- /dev/null
+++ b/packages/vue-component-meta/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021-present Johnson Chu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/vue-component-meta/README.md b/packages/vue-component-meta/README.md
new file mode 100644
index 000000000..ef54d3481
--- /dev/null
+++ b/packages/vue-component-meta/README.md
@@ -0,0 +1,21 @@
+# vue-component-meta
+
+## Usage
+
+See https://github.com/johnsoncodehk/volar/blob/master/packages/vue-component-meta/tests/index.spec.ts.
+
+## Sponsors
+
+
+
+
+
+
+
+---
+
+
+
+
+
+
diff --git a/packages/vue-component-meta/package.json b/packages/vue-component-meta/package.json
new file mode 100644
index 000000000..7f1af0639
--- /dev/null
+++ b/packages/vue-component-meta/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "vue-component-meta",
+ "version": "0.39.0",
+ "main": "out/index.js",
+ "license": "MIT",
+ "files": [
+ "out/**/*.js",
+ "out/**/*.d.ts"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/johnsoncodehk/volar.git",
+ "directory": "packages/vue-component-meta"
+ },
+ "dependencies": {
+ "@volar/vue-language-core": "0.39.0"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ }
+}
diff --git a/packages/vue-component-meta/src/index.ts b/packages/vue-component-meta/src/index.ts
new file mode 100644
index 000000000..fa06cbbd4
--- /dev/null
+++ b/packages/vue-component-meta/src/index.ts
@@ -0,0 +1,156 @@
+import * as vue from '@volar/vue-language-core';
+import * as ts from 'typescript/lib/tsserverlibrary';
+
+export function createComponentMetaChecker(tsconfigPath: string) {
+
+ const parsedCommandLine = vue.tsShared.createParsedCommandLine(ts, {
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
+ readDirectory: (path, extensions, exclude, include, depth) => {
+ return ts.sys.readDirectory(path, [...extensions, '.vue'], exclude, include, depth);
+ },
+ fileExists: ts.sys.fileExists,
+ readFile: ts.sys.readFile,
+ }, tsconfigPath);
+ const scriptSnapshot: Record = {};
+ const core = vue.createLanguageContext({
+ ...ts.sys,
+ getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options), // should use ts.getDefaultLibFilePath not ts.getDefaultLibFileName
+ useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
+ getCompilationSettings: () => parsedCommandLine.options,
+ getScriptFileNames: () => {
+ const result = [...parsedCommandLine.fileNames];
+ for (const fileName of parsedCommandLine.fileNames) {
+ if (fileName.endsWith('.vue')) {
+ result.push(fileName + '.meta.ts');
+ }
+ }
+ return result;
+ },
+ getProjectReferences: () => parsedCommandLine.projectReferences,
+ getScriptVersion: (fileName) => '0',
+ getScriptSnapshot: (fileName) => {
+ if (!scriptSnapshot[fileName]) {
+ const fileText = fileName.endsWith('.meta.ts') ? getMetaScriptContent(fileName) : ts.sys.readFile(fileName);
+ if (fileText !== undefined) {
+ scriptSnapshot[fileName] = ts.ScriptSnapshot.fromString(fileText);
+ }
+ }
+ return scriptSnapshot[fileName];
+ },
+ getTypeScriptModule: () => ts,
+ getVueCompilationSettings: () => parsedCommandLine.vueOptions,
+ });
+ const tsLs = ts.createLanguageService(core.typescriptLanguageServiceHost);
+ const program = tsLs.getProgram()!;
+ const typeChecker = program.getTypeChecker();
+
+ return {
+ getComponentMeta,
+ };
+
+ function getMetaScriptContent(fileName: string) {
+ return `
+ import Component from '${fileName.substring(0, fileName.length - '.meta.ts'.length)}';
+ export default new Component();
+ `;
+ }
+
+ function getComponentMeta(componentPath: string) {
+
+ const sourceFile = program?.getSourceFile(componentPath + '.meta.ts');
+ 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 (ts.isExportAssignment(declaration)) {
+ symbolNode = declaration.expression;
+ }
+ }
+
+ if (!symbolNode) {
+ throw 'Could not find symbol node';
+ }
+
+ const symbolType = typeChecker.getTypeAtLocation(symbolNode);
+ const symbolProperties = symbolType.getProperties();
+
+ return {
+ props: getProps(),
+ events: getEvents(),
+ slots: getSlots(),
+ };
+
+ function getProps() {
+
+ const $props = symbolProperties.find(prop => prop.escapedName === '$props');
+
+ if ($props) {
+ const type = typeChecker.getTypeOfSymbolAtLocation($props, symbolNode!);
+ const properties = type.getProperties();
+ return properties.map(prop => ({
+ name: prop.escapedName as string,
+ // @ts-ignore
+ isOptional: !!prop.declarations?.[0]?.questionToken,
+ type: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!)),
+ documentationComment: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)),
+ }));
+ }
+
+ return [];
+ }
+ function getEvents() {
+
+ const $emit = symbolProperties.find(prop => prop.escapedName === '$emit');
+
+ if ($emit) {
+ const type = typeChecker.getTypeOfSymbolAtLocation($emit, symbolNode!);
+ const calls = type.getCallSignatures();
+ return calls.map(call => ({
+ // @ts-ignore
+ name: typeChecker.getTypeOfSymbolAtLocation(call.parameters[0], symbolNode!).value,
+ parametersType: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!)),
+ // @ts-ignore
+ parameters: typeChecker.getTypeArguments(typeChecker.getTypeOfSymbolAtLocation(call.parameters[1], symbolNode!)).map(arg => ({
+ name: 'TODO',
+ type: typeChecker.typeToString(arg),
+ isOptional: 'TODO',
+ })),
+ documentationComment: ts.displayPartsToString(call.getDocumentationComment(typeChecker)),
+ }));
+ }
+
+ return [];
+ }
+ function getSlots() {
+
+ const propertyName = (parsedCommandLine.vueOptions.target ?? 3) < 3 ? '$scopedSlots' : '$slots';
+ const $slots = symbolProperties.find(prop => prop.escapedName === propertyName);
+
+ if ($slots) {
+ const type = typeChecker.getTypeOfSymbolAtLocation($slots, symbolNode!);
+ const properties = type.getProperties();
+ return properties.map(prop => ({
+ name: prop.escapedName as string,
+ propsType: typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(typeChecker.getTypeOfSymbolAtLocation(prop, symbolNode!).getCallSignatures()[0].parameters[0], symbolNode!)),
+ // props: {}, // TODO
+ documentationComment: ts.displayPartsToString(prop.getDocumentationComment(typeChecker)),
+ }));
+ }
+
+ return [];
+ }
+ }
+}
diff --git a/packages/vue-component-meta/tests/index.spec.ts b/packages/vue-component-meta/tests/index.spec.ts
new file mode 100644
index 000000000..35315b591
--- /dev/null
+++ b/packages/vue-component-meta/tests/index.spec.ts
@@ -0,0 +1,83 @@
+import * as path from 'path';
+import { describe, expect, it } from 'vitest';
+import * as metaChecker from '..';
+
+describe(`vue-component-meta`, () => {
+
+ const tsconfigPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/tsconfig.json');
+ const checker = metaChecker.createComponentMetaChecker(tsconfigPath);
+
+ it('reference-type-props', () => {
+
+ const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-props/component.vue');
+ const meta = checker.getComponentMeta(componentPath);
+
+ const a = meta.props.find(prop =>
+ prop.name === 'foo'
+ && !prop.isOptional
+ && prop.type === 'string'
+ && prop.documentationComment === 'string foo'
+ );
+ const b = meta.props.find(prop =>
+ prop.name === 'bar'
+ && prop.isOptional
+ && prop.type === 'number | undefined'
+ && prop.documentationComment === 'optional number bar'
+ );
+
+ expect(a).toBeDefined();
+ expect(b).toBeDefined();
+ });
+
+ it('reference-type-events', () => {
+
+ const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/reference-type-events/component.vue');
+ const meta = checker.getComponentMeta(componentPath);
+
+ const a = meta.events.find(event =>
+ event.name === 'foo'
+ && event.parametersType === '[data: { foo: string; }]'
+ && event.parameters.length === 1
+ && event.parameters[0].type === '{ foo: string; }'
+ );
+ const b = meta.events.find(event =>
+ event.name === 'bar'
+ && event.parametersType === '[arg1: number, arg2?: any]'
+ && event.parameters.length === 2
+ && event.parameters[0].type === 'number'
+ && event.parameters[1].type === 'any'
+ );
+ const c = meta.events.find(event =>
+ event.name === 'baz'
+ && event.parametersType === '[]'
+ && event.parameters.length === 0
+ );
+
+ expect(a).toBeDefined();
+ expect(b).toBeDefined();
+ expect(c).toBeDefined();
+ });
+
+ it('template-slots', () => {
+
+ const componentPath = path.resolve(__dirname, '../../vue-test-workspace/vue-component-meta/template-slots/component.vue');
+ const meta = checker.getComponentMeta(componentPath);
+
+ const a = meta.slots.find(event =>
+ event.name === 'default'
+ && event.propsType === '{ num: number; }'
+ );
+ const b = meta.slots.find(event =>
+ event.name === 'named-slot'
+ && event.propsType === '{ str: string; }'
+ );
+ const c = meta.slots.find(event =>
+ event.name === 'vbind'
+ && event.propsType === '{ num: number; str: string; }'
+ );
+
+ expect(a).toBeDefined();
+ expect(b).toBeDefined();
+ expect(c).toBeDefined();
+ });
+});
diff --git a/packages/vue-component-meta/tsconfig.build.json b/packages/vue-component-meta/tsconfig.build.json
new file mode 100644
index 000000000..078c2a42c
--- /dev/null
+++ b/packages/vue-component-meta/tsconfig.build.json
@@ -0,0 +1,22 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "out",
+ "rootDir": "src",
+ },
+ "include": [
+ "src"
+ ],
+ "exclude": [
+ "node_modules",
+ ".vscode-test"
+ ],
+ "references": [
+ {
+ "path": "../vue-language-core/tsconfig.build.json"
+ },
+ {
+ "path": "../vue-typescript/tsconfig.build.json"
+ },
+ ],
+}
\ No newline at end of file
diff --git a/packages/vue-test-workspace/vue-component-meta/reference-type-events/component.vue b/packages/vue-test-workspace/vue-component-meta/reference-type-events/component.vue
new file mode 100644
index 000000000..706b31acc
--- /dev/null
+++ b/packages/vue-test-workspace/vue-component-meta/reference-type-events/component.vue
@@ -0,0 +1,5 @@
+
diff --git a/packages/vue-test-workspace/vue-component-meta/reference-type-events/my-events.ts b/packages/vue-test-workspace/vue-component-meta/reference-type-events/my-events.ts
new file mode 100644
index 000000000..e5af6f334
--- /dev/null
+++ b/packages/vue-test-workspace/vue-component-meta/reference-type-events/my-events.ts
@@ -0,0 +1,5 @@
+export interface MyEvents {
+ (event: 'foo', data: { foo: string; }): void;
+ (event: 'bar', arg1: number, arg2?: any): void;
+ (event: 'baz'): void;
+}
diff --git a/packages/vue-test-workspace/vue-component-meta/reference-type-props/component.vue b/packages/vue-test-workspace/vue-component-meta/reference-type-props/component.vue
new file mode 100644
index 000000000..46af87265
--- /dev/null
+++ b/packages/vue-test-workspace/vue-component-meta/reference-type-props/component.vue
@@ -0,0 +1,5 @@
+
diff --git a/packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts b/packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts
new file mode 100644
index 000000000..c0bd649b2
--- /dev/null
+++ b/packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts
@@ -0,0 +1,10 @@
+export interface MyProps {
+ /**
+ * string foo
+ */
+ foo: string,
+ /**
+ * optional number bar
+ */
+ bar?: number,
+}
diff --git a/packages/vue-test-workspace/vue-component-meta/template-slots/component.vue b/packages/vue-test-workspace/vue-component-meta/template-slots/component.vue
new file mode 100644
index 000000000..0688341c9
--- /dev/null
+++ b/packages/vue-test-workspace/vue-component-meta/template-slots/component.vue
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/packages/vue-test-workspace/vue-component-meta/tsconfig.json b/packages/vue-test-workspace/vue-component-meta/tsconfig.json
new file mode 100644
index 000000000..e1b85270d
--- /dev/null
+++ b/packages/vue-test-workspace/vue-component-meta/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "../tsconfig.json",
+ "include": [
+ "**/*",
+ ]
+}
\ No newline at end of file
diff --git a/packages/vue-tsc/README.md b/packages/vue-tsc/README.md
index ee7bc70f0..5635297d8 100644
--- a/packages/vue-tsc/README.md
+++ b/packages/vue-tsc/README.md
@@ -15,7 +15,7 @@ Roadmap:
- [x] dts emit support
- [x] Watch mode support
-## Using
+## Usage
Type check:
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 69964b0b2..17e3336e6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -19,7 +19,7 @@ importers:
typescript: 4.7.4
vite: 3.0.2
vitepress: 1.0.0-alpha.4
- vitest: 0.18.1
+ vitest: 0.19.0
vue: 3.2.37
extensions/vscode-alpine-language-features:
@@ -253,6 +253,12 @@ importers:
devDependencies:
typescript: 4.7.4
+ packages/vue-component-meta:
+ specifiers:
+ '@volar/vue-language-core': 0.39.0
+ dependencies:
+ '@volar/vue-language-core': link:../vue-language-core
+
packages/vue-language-core:
specifiers:
'@volar/code-gen': 0.39.0
@@ -5311,12 +5317,13 @@ packages:
- stylus
dev: true
- /vitest/0.18.1:
- resolution: {integrity: sha512-4F/1K/Vn4AvJwe7i2YblR02PT5vMKcw9KN4unDq2KD0YcSxX0B/6D6Qu9PJaXwVuxXMFTQ5ovd4+CQaW3bwofA==}
+ /vitest/0.19.0:
+ resolution: {integrity: sha512-nU80Gm95tMchigHpAMukxv1LoWbBGgknX/1MqrXCOoJoJL7/wfq4h2aow61o2jwf5szQrahoNqBqaGb+fYdYrQ==}
engines: {node: '>=v14.16.0'}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
+ '@vitest/browser': '*'
'@vitest/ui': '*'
c8: '*'
happy-dom: '*'
@@ -5324,6 +5331,8 @@ packages:
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
+ '@vitest/browser':
+ optional: true
'@vitest/ui':
optional: true
c8:
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 5312d4733..6574211f1 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -14,5 +14,8 @@
{
"path": "./packages/vue-tsc/tsconfig.build.json"
},
+ {
+ "path": "./packages/vue-component-meta/tsconfig.build.json"
+ },
]
}
\ No newline at end of file