From 844b470cae9865f06f245549522e2b6a59ddde41 Mon Sep 17 00:00:00 2001 From: johnsoncodehk Date: Sun, 24 Jul 2022 21:06:59 +0800 Subject: [PATCH] feat: add vue-component-meta close #1627 --- README.md | 5 + packages/vue-component-meta/LICENSE | 21 +++ packages/vue-component-meta/README.md | 21 +++ packages/vue-component-meta/package.json | 21 +++ packages/vue-component-meta/src/index.ts | 156 ++++++++++++++++++ .../vue-component-meta/tests/index.spec.ts | 83 ++++++++++ .../vue-component-meta/tsconfig.build.json | 22 +++ .../reference-type-events/component.vue | 5 + .../reference-type-events/my-events.ts | 5 + .../reference-type-props/component.vue | 5 + .../reference-type-props/my-props.ts | 10 ++ .../template-slots/component.vue | 8 + .../vue-component-meta/tsconfig.json | 6 + packages/vue-tsc/README.md | 2 +- pnpm-lock.yaml | 15 +- tsconfig.build.json | 3 + 16 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 packages/vue-component-meta/LICENSE create mode 100644 packages/vue-component-meta/README.md create mode 100644 packages/vue-component-meta/package.json create mode 100644 packages/vue-component-meta/src/index.ts create mode 100644 packages/vue-component-meta/tests/index.spec.ts create mode 100644 packages/vue-component-meta/tsconfig.build.json create mode 100644 packages/vue-test-workspace/vue-component-meta/reference-type-events/component.vue create mode 100644 packages/vue-test-workspace/vue-component-meta/reference-type-events/my-events.ts create mode 100644 packages/vue-test-workspace/vue-component-meta/reference-type-props/component.vue create mode 100644 packages/vue-test-workspace/vue-component-meta/reference-type-props/my-props.ts create mode 100644 packages/vue-test-workspace/vue-component-meta/template-slots/component.vue create mode 100644 packages/vue-test-workspace/vue-component-meta/tsconfig.json 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