From 58d6f1cbbbf86923db7f9950f55d73cb3198315b Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 30 Jul 2024 22:55:59 +0100 Subject: [PATCH 01/12] Add initial vue-support & svelte-support packages --- packages/svelte-support/LICENSE.md | 21 +++++ packages/svelte-support/package.json | 57 ++++++++++++ packages/svelte-support/src/index.ts | 98 ++++++++++++++++++++ packages/svelte-support/src/types.ts | 80 ++++++++++++++++ packages/vue-support/LICENSE.md | 21 +++++ packages/vue-support/package.json | 53 +++++++++++ packages/vue-support/src/index.ts | 131 +++++++++++++++++++++++++++ packages/vue-support/src/types.ts | 80 ++++++++++++++++ 8 files changed, 541 insertions(+) create mode 100644 packages/svelte-support/LICENSE.md create mode 100644 packages/svelte-support/package.json create mode 100644 packages/svelte-support/src/index.ts create mode 100644 packages/svelte-support/src/types.ts create mode 100644 packages/vue-support/LICENSE.md create mode 100644 packages/vue-support/package.json create mode 100644 packages/vue-support/src/index.ts create mode 100644 packages/vue-support/src/types.ts diff --git a/packages/svelte-support/LICENSE.md b/packages/svelte-support/LICENSE.md new file mode 100644 index 00000000..adef8fd1 --- /dev/null +++ b/packages/svelte-support/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 0no.co + +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. \ No newline at end of file diff --git a/packages/svelte-support/package.json b/packages/svelte-support/package.json new file mode 100644 index 00000000..ae0c35c5 --- /dev/null +++ b/packages/svelte-support/package.json @@ -0,0 +1,57 @@ +{ + "name": "@gql.tada/svelte-support", + "version": "1.0.0", + "public": true, + "description": "Svelte Support package for gql.tada’s CLI tool.", + "author": "0no.co ", + "sideEffects": false, + "source": "./src/index.ts", + "main": "./dist/gql-tada-svelte-support", + "module": "./dist/gql-tada-svelte-support.mjs", + "types": "./dist/gql-tada-svelte-support.d.ts", + "exports": { + ".": { + "types": "./dist/gql-tada-svelte-support.d.ts", + "import": "./dist/gql-tada-svelte-support.mjs", + "require": "./dist/gql-tada-svelte-support.js", + "source": "./src/index.ts" + }, + "./package.json": "./package.json" + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist/" + ], + "repository": { + "type": "git", + "url": "https://github.com/0no-co/gql.tada.git", + "directory": "packages/svelte-support" + }, + "bugs": { + "url": "https://github.com/0no-co/gql.tada/issues" + }, + "homepage": "https://gql-tada.0no.co/", + "license": "MIT", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.mjs", + "clean": "rimraf dist node_modules/.cache", + "prepublishOnly": "run-s clean build" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "svelte2tsx": "^0.7.6", + "vscode-languageserver-textdocument": "^1.0.11" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "devDependencies": { + "@vue/language-core": "~2.0.0" + } +} diff --git a/packages/svelte-support/src/index.ts b/packages/svelte-support/src/index.ts new file mode 100644 index 00000000..bb7ccbec --- /dev/null +++ b/packages/svelte-support/src/index.ts @@ -0,0 +1,98 @@ +import ts from 'typescript'; +import { decode } from '@jridgewell/sourcemap-codec'; +import { svelte2tsx } from 'svelte2tsx'; +import { TextDocument } from 'vscode-languageserver-textdocument'; + +import type { CodeMapping, VirtualCode } from './types'; + +// See: https://github.com/johnsoncodehk/language-tools/blob/volar2/packages/language-server/src/languagePlugin.ts +export const transform = (sourceFile: ts.SourceFile): VirtualCode | undefined => { + const text = sourceFile.getFullText(); + + let tsx: ReturnType; + + try { + tsx = svelte2tsx(text, { + filename: sourceFile.fileName, + isTsFile: true, + emitOnTemplateError: true, + mode: 'ts', + }); + } catch (error) { + return; + } + + const v3Mappings = decode(tsx.map.mappings); + const document = TextDocument.create('', 'svelte', 0, text); + const generateDocument = TextDocument.create('', 'typescript', 0, tsx.code); + const mappings: CodeMapping[] = []; + + let current: + | { + genOffset: number; + sourceOffset: number; + } + | undefined; + + for (let genLine = 0; genLine < v3Mappings.length; genLine++) { + for (const segment of v3Mappings[genLine]) { + const genCharacter = segment[0]; + const genOffset = generateDocument.offsetAt({ line: genLine, character: genCharacter }); + if (current) { + let length = genOffset - current.genOffset; + const sourceText = text.substring(current.sourceOffset, current.sourceOffset + length); + const genText = tsx.code.substring(current.genOffset, current.genOffset + length); + if (sourceText !== genText) { + length = 0; + for (let i = 0; i < genOffset - current.genOffset; i++) { + if (sourceText[i] === genText[i]) { + length = i + 1; + } else { + break; + } + } + } + if (length > 0) { + const lastMapping = mappings.length ? mappings[mappings.length - 1] : undefined; + if ( + lastMapping && + lastMapping.generatedOffsets[0] + lastMapping.lengths[0] === current.genOffset && + lastMapping.sourceOffsets[0] + lastMapping.lengths[0] === current.sourceOffset + ) { + lastMapping.lengths[0] += length; + } else { + mappings.push({ + sourceOffsets: [current.sourceOffset], + generatedOffsets: [current.genOffset], + lengths: [length], + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: false, + format: false, + }, + }); + } + } + current = undefined; + } + if (segment[2] !== undefined && segment[3] !== undefined) { + const sourceOffset = document.offsetAt({ line: segment[2], character: segment[3] }); + current = { + genOffset, + sourceOffset, + }; + } + } + } + + return { + id: 'ts', + languageId: 'typescript', + snapshot: ts.ScriptSnapshot.fromString(tsx.code), + mappings: mappings, + embeddedCodes: [], + } satisfies VirtualCode; +}; diff --git a/packages/svelte-support/src/types.ts b/packages/svelte-support/src/types.ts new file mode 100644 index 00000000..91b7f41a --- /dev/null +++ b/packages/svelte-support/src/types.ts @@ -0,0 +1,80 @@ +export interface TextSpan { + start: number; + length: number; +} + +export interface TextChangeRange { + span: TextSpan; + newLength: number; +} + +export interface IScriptSnapshot { + /** Gets a portion of the script snapshot specified by [start, end). */ + getText(start: number, end: number): string; + /** Gets the length of this script snapshot. */ + getLength(): number; + /** + * Gets the TextChangeRange that describe how the text changed between this text and + * an older version. This information is used by the incremental parser to determine + * what sections of the script need to be re-parsed. 'undefined' can be returned if the + * change range cannot be determined. However, in that case, incremental parsing will + * not happen and the entire document will be re - parsed. + */ + getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined; + /** Releases all resources held by this script snapshot */ + dispose?(): void; +} + +export interface CodeInformation { + /** virtual code is expected to support verification */ + verification?: + | boolean + | { + shouldReport?(): boolean; + }; + /** virtual code is expected to support assisted completion */ + completion?: + | boolean + | { + isAdditional?: boolean; + onlyImport?: boolean; + }; + /** virtual code is expected correctly reflect semantic of the source code */ + semantic?: + | boolean + | { + shouldHighlight?(): boolean; + }; + /** virtual code is expected correctly reflect reference relationships of the source code */ + navigation?: + | boolean + | { + shouldRename?(): boolean; + resolveRenameNewName?(newName: string): string; + resolveRenameEditText?(newText: string): string; + }; + /** virtual code is expected correctly reflect the structural information of the source code */ + structure?: boolean; + /** virtual code is expected correctly reflect the format information of the source code */ + format?: boolean; +} + +export interface Mapping { + sourceOffsets: number[]; + generatedOffsets: number[]; + lengths: number[]; + generatedLengths?: number[]; + data: Data; +} + +export type CodeMapping = Mapping; + +export interface VirtualCode { + id: string; + languageId: string; + snapshot: IScriptSnapshot; + mappings: CodeMapping[]; + associatedScriptMappings?: Map; + embeddedCodes?: VirtualCode[]; + linkedCodeMappings?: Mapping[]; +} diff --git a/packages/vue-support/LICENSE.md b/packages/vue-support/LICENSE.md new file mode 100644 index 00000000..adef8fd1 --- /dev/null +++ b/packages/vue-support/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 0no.co + +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. \ No newline at end of file diff --git a/packages/vue-support/package.json b/packages/vue-support/package.json new file mode 100644 index 00000000..9104e7b6 --- /dev/null +++ b/packages/vue-support/package.json @@ -0,0 +1,53 @@ +{ + "name": "@gql.tada/vue-support", + "version": "1.0.0", + "public": true, + "description": "Vue Support package for gql.tada’s CLI tool.", + "author": "0no.co ", + "sideEffects": false, + "source": "./src/index.ts", + "main": "./dist/gql-tada-vue-support", + "module": "./dist/gql-tada-vue-support.mjs", + "types": "./dist/gql-tada-vue-support.d.ts", + "exports": { + ".": { + "types": "./dist/gql-tada-vue-support.d.ts", + "import": "./dist/gql-tada-vue-support.mjs", + "require": "./dist/gql-tada-vue-support.js", + "source": "./src/index.ts" + }, + "./package.json": "./package.json" + }, + "files": [ + "CHANGELOG.md", + "LICENSE.md", + "README.md", + "dist/" + ], + "repository": { + "type": "git", + "url": "https://github.com/0no-co/gql.tada.git", + "directory": "packages/vue-support" + }, + "bugs": { + "url": "https://github.com/0no-co/gql.tada/issues" + }, + "homepage": "https://gql-tada.0no.co/", + "license": "MIT", + "scripts": { + "build": "rollup -c ../../scripts/rollup.config.mjs", + "clean": "rimraf dist node_modules/.cache", + "prepublishOnly": "run-s clean build" + }, + "dependencies": { + "@vue/compiler-dom": "^3.4.23", + "@vue/language-core": "~2.0.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + } +} diff --git a/packages/vue-support/src/index.ts b/packages/vue-support/src/index.ts new file mode 100644 index 00000000..cf22afeb --- /dev/null +++ b/packages/vue-support/src/index.ts @@ -0,0 +1,131 @@ +import ts from 'typescript'; +import * as vueCompilerDOM from '@vue/compiler-dom'; +import * as vue from '@vue/language-core'; +import { parse } from '@vue/language-core'; + +import type { VirtualCode } from './types'; + +const useVueFilePlugin = (): vue.VueLanguagePluginReturn => { + return { + version: 2, + + parseSFC(_fileName, content) { + return parse(content); + }, + + updateSFC(sfc, change) { + const blocks = [ + sfc.descriptor.template, + sfc.descriptor.script, + sfc.descriptor.scriptSetup, + ...sfc.descriptor.styles, + ...sfc.descriptor.customBlocks, + ].filter((block): block is NonNullable => !!block); + + const hitBlock = blocks.find( + (block) => change.start >= block.loc.start.offset && change.end <= block.loc.end.offset + ); + if (!hitBlock) { + return; + } + + const oldContent = hitBlock.content; + const newContent = (hitBlock.content = + hitBlock.content.substring(0, change.start - hitBlock.loc.start.offset) + + change.newText + + hitBlock.content.substring(change.end - hitBlock.loc.start.offset)); + + // #3449 + const endTagRegex = new RegExp(``); + const insertedEndTag = !!oldContent.match(endTagRegex) !== !!newContent.match(endTagRegex); + if (insertedEndTag) { + return; + } + + const lengthDiff = change.newText.length - (change.end - change.start); + + for (const block of blocks) { + if (block.loc.start.offset > change.end) { + block.loc.start.offset += lengthDiff; + } + if (block.loc.end.offset >= change.end) { + block.loc.end.offset += lengthDiff; + } + } + + return sfc; + }, + }; +}; + +function* forEachEmbeddedCode(virtualCode: VirtualCode) { + yield virtualCode; + if (virtualCode.embeddedCodes) { + for (const embeddedCode of virtualCode.embeddedCodes) { + yield* forEachEmbeddedCode(embeddedCode); + } + } +} + +let VueVirtualCode: typeof vue.VueVirtualCode | undefined; +if ('VueVirtualCode' in vue) { + VueVirtualCode = vue.VueVirtualCode; +} else if ('VueGeneratedCode' in vue) { + VueVirtualCode = (vue as any).VueGeneratedCode; +} + +let createPlugins: typeof vue.createPlugins | undefined; +if ('createPlugins' in vue) { + createPlugins = vue.createPlugins; +} else if ('getBasePlugins' in vue) { + createPlugins = (vue as any).getBasePlugins; +} else if ('getDefaultVueLanguagePlugins' in vue) { + createPlugins = (vue as any).getDefaultVueLanguagePlugins; +} + +const vueCompilerOptions = vue.resolveVueCompilerOptions({}); + +let plugins: ReturnType | undefined; + +export const check = () => { + const undefinedValues = [ + !VueVirtualCode && 'VueVirtualCode', + !createPlugins && 'createPlugins', + ].filter(Boolean); + if (undefinedValues.length) { + throw new Error( + 'This version of `@vue/language-core` seems to be unsupported. ' + + `(undefined: ${undefinedValues.join(', ')})\n` + + 'Please report this issue along with your installed version of `@vue/language-core.' + ); + } +}; + +export const transform = (sourceFile: ts.SourceFile): VirtualCode | undefined => { + if (!VueVirtualCode || !createPlugins) { + return undefined; + } else if (!plugins) { + const pluginContext = { + modules: { + typescript: ts, + '@vue/compiler-dom': vueCompilerDOM, + }, + compilerOptions: {}, + globalTypesHolder: undefined, + vueCompilerOptions, + }; + plugins = createPlugins(pluginContext); + plugins.push(useVueFilePlugin()); + } + + const snapshot = ts.ScriptSnapshot.fromString(sourceFile.getFullText()); + const root = new VueVirtualCode( + sourceFile.fileName, + 'vue', + snapshot, + vueCompilerOptions, + plugins, + ...([ts, false] as any as [typeof ts]) + ); + for (const code of forEachEmbeddedCode(root)) if (code.id.startsWith('script_')) return code; +}; diff --git a/packages/vue-support/src/types.ts b/packages/vue-support/src/types.ts new file mode 100644 index 00000000..91b7f41a --- /dev/null +++ b/packages/vue-support/src/types.ts @@ -0,0 +1,80 @@ +export interface TextSpan { + start: number; + length: number; +} + +export interface TextChangeRange { + span: TextSpan; + newLength: number; +} + +export interface IScriptSnapshot { + /** Gets a portion of the script snapshot specified by [start, end). */ + getText(start: number, end: number): string; + /** Gets the length of this script snapshot. */ + getLength(): number; + /** + * Gets the TextChangeRange that describe how the text changed between this text and + * an older version. This information is used by the incremental parser to determine + * what sections of the script need to be re-parsed. 'undefined' can be returned if the + * change range cannot be determined. However, in that case, incremental parsing will + * not happen and the entire document will be re - parsed. + */ + getChangeRange(oldSnapshot: IScriptSnapshot): TextChangeRange | undefined; + /** Releases all resources held by this script snapshot */ + dispose?(): void; +} + +export interface CodeInformation { + /** virtual code is expected to support verification */ + verification?: + | boolean + | { + shouldReport?(): boolean; + }; + /** virtual code is expected to support assisted completion */ + completion?: + | boolean + | { + isAdditional?: boolean; + onlyImport?: boolean; + }; + /** virtual code is expected correctly reflect semantic of the source code */ + semantic?: + | boolean + | { + shouldHighlight?(): boolean; + }; + /** virtual code is expected correctly reflect reference relationships of the source code */ + navigation?: + | boolean + | { + shouldRename?(): boolean; + resolveRenameNewName?(newName: string): string; + resolveRenameEditText?(newText: string): string; + }; + /** virtual code is expected correctly reflect the structural information of the source code */ + structure?: boolean; + /** virtual code is expected correctly reflect the format information of the source code */ + format?: boolean; +} + +export interface Mapping { + sourceOffsets: number[]; + generatedOffsets: number[]; + lengths: number[]; + generatedLengths?: number[]; + data: Data; +} + +export type CodeMapping = Mapping; + +export interface VirtualCode { + id: string; + languageId: string; + snapshot: IScriptSnapshot; + mappings: CodeMapping[]; + associatedScriptMappings?: Map; + embeddedCodes?: VirtualCode[]; + linkedCodeMappings?: Mapping[]; +} From 409404e8ab884dcc11f19b462b1ccee673ef076f Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 30 Jul 2024 22:56:30 +0100 Subject: [PATCH 02/12] Update dependencies --- packages/cli-utils/package.json | 10 ++++++++ pnpm-lock.yaml | 45 ++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index a49a7078..cb8c8c09 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -63,8 +63,18 @@ "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", "svelte2tsx": "^0.7.6" }, + "peerDependenciesMeta": { + "@gql.tada/svelte-support": { + "optional": true + }, + "@gql.tada/vue-support": { + "optional": true + } + }, "peerDependencies": { "@0no-co/graphqlsp": "^1.12.9", + "@gql.tada/svelte-support": "workspace:*", + "@gql.tada/vue-support": "workspace:*", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", "typescript": "^5.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f615749..7a5b2461 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,12 @@ importers: '@gql.tada/internal': specifier: workspace:* version: link:../internal + '@gql.tada/svelte-support': + specifier: workspace:* + version: link:../svelte-support + '@gql.tada/vue-support': + specifier: workspace:* + version: link:../vue-support '@vue/compiler-dom': specifier: ^3.4.23 version: 3.4.25 @@ -316,6 +322,37 @@ importers: specifier: ^5.5.2 version: 5.5.2 + packages/svelte-support: + dependencies: + '@jridgewell/sourcemap-codec': + specifier: ^1.4.15 + version: 1.5.0 + svelte2tsx: + specifier: ^0.7.6 + version: 0.7.6(svelte@4.2.17)(typescript@5.5.2) + typescript: + specifier: ^5.5.2 + version: 5.5.2 + vscode-languageserver-textdocument: + specifier: ^1.0.11 + version: 1.0.11 + devDependencies: + '@vue/language-core': + specifier: ~2.0.0 + version: 2.0.29(typescript@5.5.2) + + packages/vue-support: + dependencies: + '@vue/compiler-dom': + specifier: ^3.4.23 + version: 3.4.25 + '@vue/language-core': + specifier: ~2.0.0 + version: 2.0.29(typescript@5.5.2) + typescript: + specifier: ^5.5.2 + version: 5.5.2 + website: dependencies: '@apollo/client': @@ -4755,7 +4792,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.2': {} @@ -4780,7 +4817,7 @@ snapshots: '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@manypkg/find-root@1.1.0': dependencies: @@ -5705,7 +5742,7 @@ snapshots: code-red@1.0.4: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@types/estree': 1.0.5 acorn: 8.11.3 estree-walker: 3.0.3 @@ -6610,7 +6647,7 @@ snapshots: magic-string@0.30.10: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 magic-string@0.30.11: dependencies: From e92e8e1127b4e2785e3a6fd4af47464be34b155a Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Tue, 30 Jul 2024 23:02:10 +0100 Subject: [PATCH 03/12] Use external support packages in CLI --- packages/cli-utils/src/ts/transformers.ts | 72 ++++++++++ .../cli-utils/src/ts/transformers/index.ts | 40 ------ .../cli-utils/src/ts/transformers/svelte.ts | 97 ------------- packages/cli-utils/src/ts/transformers/vue.ts | 130 ------------------ packages/cli-utils/src/utils/error.ts | 6 + 5 files changed, 78 insertions(+), 267 deletions(-) create mode 100644 packages/cli-utils/src/ts/transformers.ts delete mode 100644 packages/cli-utils/src/ts/transformers/index.ts delete mode 100644 packages/cli-utils/src/ts/transformers/svelte.ts delete mode 100644 packages/cli-utils/src/ts/transformers/vue.ts create mode 100644 packages/cli-utils/src/utils/error.ts diff --git a/packages/cli-utils/src/ts/transformers.ts b/packages/cli-utils/src/ts/transformers.ts new file mode 100644 index 00000000..a27e4714 --- /dev/null +++ b/packages/cli-utils/src/ts/transformers.ts @@ -0,0 +1,72 @@ +import type ts from 'typescript'; +import * as path from 'node:path'; +import type { VirtualCode } from '@vue/language-core'; + +import { TadaError } from '../utils/error'; + +let _svelte: typeof import('@gql.tada/svelte-support'); +let _vue: typeof import('@gql.tada/vue-support'); + +const transformSvelte = async ( + ...args: Parameters +): Promise> => { + if (!_svelte) { + try { + _svelte = await import('@gql.tada/svelte-support'); + } catch (_error) { + throw new TadaError( + 'For Svelte support the `gql.tada/svelte-support` package must be installed.\n' + + 'Install the package and try again.' + ); + } + } + return _svelte.transform(...args); +}; + +const transformVue = async ( + ...args: Parameters +): Promise> => { + if (!_vue) { + try { + _vue = await import('@gql.tada/vue-support'); + } catch (_error) { + throw new TadaError( + 'For Vue support the `gql.tada/vue-support` package must be installed.\n' + + 'Install the package and try again.' + ); + } + } + return _vue.transform(...args); +}; + +const checkVue = async (): Promise => { + if (!_vue) { + try { + _vue = await import('@gql.tada/vue-support'); + } catch (_error) { + throw new TadaError( + 'For Vue support the `gql.tada/vue-support` package must be installed.\n' + + 'Install the package and try again.' + ); + } + } + return _vue.check(); +}; + +export const transformExtensions = ['.svelte', '.vue'] as const; + +export const transform = async (sourceFile: ts.SourceFile): Promise => { + const extname = path.extname(sourceFile.fileName); + if (extname === '.svelte') { + return transformSvelte(sourceFile); + } else if (extname === '.vue') { + await checkVue(); + return transformVue(sourceFile); + } else { + throw new TadaError( + `Tried transforming unknown file type "${extname}". Supported: ${transformExtensions.join( + ', ' + )}` + ); + } +}; diff --git a/packages/cli-utils/src/ts/transformers/index.ts b/packages/cli-utils/src/ts/transformers/index.ts deleted file mode 100644 index 926164e0..00000000 --- a/packages/cli-utils/src/ts/transformers/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type ts from 'typescript'; -import * as path from 'node:path'; -import type { VirtualCode } from '@vue/language-core'; - -let _svelte: typeof import('./svelte'); -let _vue: typeof import('./vue'); - -const transformSvelte = async ( - ...args: Parameters -): Promise> => { - return (_svelte || (_svelte = await import('./svelte'))).transform(...args); -}; - -const transformVue = async ( - ...args: Parameters -): Promise> => { - return (_vue || (_vue = await import('./vue'))).transform(...args); -}; - -const checkVue = async (): Promise => { - return (_vue || (_vue = await import('./vue'))).check(); -}; - -export const transformExtensions = ['.svelte', '.vue'] as const; - -export const transform = async (sourceFile: ts.SourceFile): Promise => { - const extname = path.extname(sourceFile.fileName); - if (extname === '.svelte') { - return transformSvelte(sourceFile); - } else if (extname === '.vue') { - await checkVue(); - return transformVue(sourceFile); - } else { - throw new Error( - `Tried transforming unknown file type "${extname}". Supported: ${transformExtensions.join( - ', ' - )}` - ); - } -}; diff --git a/packages/cli-utils/src/ts/transformers/svelte.ts b/packages/cli-utils/src/ts/transformers/svelte.ts deleted file mode 100644 index 1dbba66e..00000000 --- a/packages/cli-utils/src/ts/transformers/svelte.ts +++ /dev/null @@ -1,97 +0,0 @@ -import ts from 'typescript'; -import type { CodeMapping, VirtualCode } from '@vue/language-core'; -import { decode } from '@jridgewell/sourcemap-codec'; -import { svelte2tsx } from 'svelte2tsx'; -import { TextDocument } from 'vscode-languageserver-textdocument'; - -// See: https://github.com/johnsoncodehk/language-tools/blob/volar2/packages/language-server/src/languagePlugin.ts -export const transform = (sourceFile: ts.SourceFile): VirtualCode | undefined => { - const text = sourceFile.getFullText(); - - let tsx: ReturnType; - - try { - tsx = svelte2tsx(text, { - filename: sourceFile.fileName, - isTsFile: true, - emitOnTemplateError: true, - mode: 'ts', - }); - } catch (error) { - return; - } - - const v3Mappings = decode(tsx.map.mappings); - const document = TextDocument.create('', 'svelte', 0, text); - const generateDocument = TextDocument.create('', 'typescript', 0, tsx.code); - const mappings: CodeMapping[] = []; - - let current: - | { - genOffset: number; - sourceOffset: number; - } - | undefined; - - for (let genLine = 0; genLine < v3Mappings.length; genLine++) { - for (const segment of v3Mappings[genLine]) { - const genCharacter = segment[0]; - const genOffset = generateDocument.offsetAt({ line: genLine, character: genCharacter }); - if (current) { - let length = genOffset - current.genOffset; - const sourceText = text.substring(current.sourceOffset, current.sourceOffset + length); - const genText = tsx.code.substring(current.genOffset, current.genOffset + length); - if (sourceText !== genText) { - length = 0; - for (let i = 0; i < genOffset - current.genOffset; i++) { - if (sourceText[i] === genText[i]) { - length = i + 1; - } else { - break; - } - } - } - if (length > 0) { - const lastMapping = mappings.length ? mappings[mappings.length - 1] : undefined; - if ( - lastMapping && - lastMapping.generatedOffsets[0] + lastMapping.lengths[0] === current.genOffset && - lastMapping.sourceOffsets[0] + lastMapping.lengths[0] === current.sourceOffset - ) { - lastMapping.lengths[0] += length; - } else { - mappings.push({ - sourceOffsets: [current.sourceOffset], - generatedOffsets: [current.genOffset], - lengths: [length], - data: { - verification: true, - completion: true, - semantic: true, - navigation: true, - structure: false, - format: false, - }, - }); - } - } - current = undefined; - } - if (segment[2] !== undefined && segment[3] !== undefined) { - const sourceOffset = document.offsetAt({ line: segment[2], character: segment[3] }); - current = { - genOffset, - sourceOffset, - }; - } - } - } - - return { - id: 'ts', - languageId: 'typescript', - snapshot: ts.ScriptSnapshot.fromString(tsx.code), - mappings: mappings, - embeddedCodes: [], - } satisfies VirtualCode; -}; diff --git a/packages/cli-utils/src/ts/transformers/vue.ts b/packages/cli-utils/src/ts/transformers/vue.ts deleted file mode 100644 index a9918045..00000000 --- a/packages/cli-utils/src/ts/transformers/vue.ts +++ /dev/null @@ -1,130 +0,0 @@ -import ts from 'typescript'; -import type { VirtualCode } from '@vue/language-core'; -import * as vueCompilerDOM from '@vue/compiler-dom'; -import * as vue from '@vue/language-core'; -import { parse } from '@vue/language-core'; - -const useVueFilePlugin = (): vue.VueLanguagePluginReturn => { - return { - version: 2, - - parseSFC(_fileName, content) { - return parse(content); - }, - - updateSFC(sfc, change) { - const blocks = [ - sfc.descriptor.template, - sfc.descriptor.script, - sfc.descriptor.scriptSetup, - ...sfc.descriptor.styles, - ...sfc.descriptor.customBlocks, - ].filter((block): block is NonNullable => !!block); - - const hitBlock = blocks.find( - (block) => change.start >= block.loc.start.offset && change.end <= block.loc.end.offset - ); - if (!hitBlock) { - return; - } - - const oldContent = hitBlock.content; - const newContent = (hitBlock.content = - hitBlock.content.substring(0, change.start - hitBlock.loc.start.offset) + - change.newText + - hitBlock.content.substring(change.end - hitBlock.loc.start.offset)); - - // #3449 - const endTagRegex = new RegExp(``); - const insertedEndTag = !!oldContent.match(endTagRegex) !== !!newContent.match(endTagRegex); - if (insertedEndTag) { - return; - } - - const lengthDiff = change.newText.length - (change.end - change.start); - - for (const block of blocks) { - if (block.loc.start.offset > change.end) { - block.loc.start.offset += lengthDiff; - } - if (block.loc.end.offset >= change.end) { - block.loc.end.offset += lengthDiff; - } - } - - return sfc; - }, - }; -}; - -function* forEachEmbeddedCode(virtualCode: VirtualCode) { - yield virtualCode; - if (virtualCode.embeddedCodes) { - for (const embeddedCode of virtualCode.embeddedCodes) { - yield* forEachEmbeddedCode(embeddedCode); - } - } -} - -let VueVirtualCode: typeof vue.VueVirtualCode | undefined; -if ('VueVirtualCode' in vue) { - VueVirtualCode = vue.VueVirtualCode; -} else if ('VueGeneratedCode' in vue) { - VueVirtualCode = (vue as any).VueGeneratedCode; -} - -let createPlugins: typeof vue.createPlugins | undefined; -if ('createPlugins' in vue) { - createPlugins = vue.createPlugins; -} else if ('getBasePlugins' in vue) { - createPlugins = (vue as any).getBasePlugins; -} else if ('getDefaultVueLanguagePlugins' in vue) { - createPlugins = (vue as any).getDefaultVueLanguagePlugins; -} - -const vueCompilerOptions = vue.resolveVueCompilerOptions({}); - -let plugins: ReturnType | undefined; - -export const check = () => { - const undefinedValues = [ - !VueVirtualCode && 'VueVirtualCode', - !createPlugins && 'createPlugins', - ].filter(Boolean); - if (undefinedValues.length) { - throw new Error( - 'This version of `@vue/language-core` seems to be unsupported. ' + - `(undefined: ${undefinedValues.join(', ')})\n` + - 'Please report this issue along with your installed version of `@vue/language-core.' - ); - } -}; - -export const transform = (sourceFile: ts.SourceFile): VirtualCode | undefined => { - if (!VueVirtualCode || !createPlugins) { - return undefined; - } else if (!plugins) { - const pluginContext = { - modules: { - typescript: ts, - '@vue/compiler-dom': vueCompilerDOM, - }, - compilerOptions: {}, - globalTypesHolder: undefined, - vueCompilerOptions, - }; - plugins = createPlugins(pluginContext); - plugins.push(useVueFilePlugin()); - } - - const snapshot = ts.ScriptSnapshot.fromString(sourceFile.getFullText()); - const root = new VueVirtualCode( - sourceFile.fileName, - 'vue', - snapshot, - vueCompilerOptions, - plugins, - ...([ts, false] as any as [typeof ts]) - ); - for (const code of forEachEmbeddedCode(root)) if (code.id.startsWith('script_')) return code; -}; diff --git a/packages/cli-utils/src/utils/error.ts b/packages/cli-utils/src/utils/error.ts new file mode 100644 index 00000000..9cf6d34d --- /dev/null +++ b/packages/cli-utils/src/utils/error.ts @@ -0,0 +1,6 @@ +export class TadaError extends Error { + constructor(message: string) { + super(message); + this.name = 'TadaError'; + } +} From 3cd4d406afc484ce3790493acf415ae038c1ba43 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 11:30:42 +0100 Subject: [PATCH 04/12] Fix messages --- packages/cli-utils/src/ts/transformers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli-utils/src/ts/transformers.ts b/packages/cli-utils/src/ts/transformers.ts index a27e4714..886f3ec3 100644 --- a/packages/cli-utils/src/ts/transformers.ts +++ b/packages/cli-utils/src/ts/transformers.ts @@ -15,7 +15,7 @@ const transformSvelte = async ( _svelte = await import('@gql.tada/svelte-support'); } catch (_error) { throw new TadaError( - 'For Svelte support the `gql.tada/svelte-support` package must be installed.\n' + + 'For Svelte support the `@gql.tada/svelte-support` package must be installed.\n' + 'Install the package and try again.' ); } @@ -31,7 +31,7 @@ const transformVue = async ( _vue = await import('@gql.tada/vue-support'); } catch (_error) { throw new TadaError( - 'For Vue support the `gql.tada/vue-support` package must be installed.\n' + + 'For Vue support the `@gql.tada/vue-support` package must be installed.\n' + 'Install the package and try again.' ); } @@ -45,7 +45,7 @@ const checkVue = async (): Promise => { _vue = await import('@gql.tada/vue-support'); } catch (_error) { throw new TadaError( - 'For Vue support the `gql.tada/vue-support` package must be installed.\n' + + 'For Vue support the `@gql.tada/vue-support` package must be installed.\n' + 'Install the package and try again.' ); } From 081e80d6d4d88d8ff318ea9cdb089b7418872d59 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 11:52:35 +0100 Subject: [PATCH 05/12] Add special color for TadaErrors --- packages/cli-utils/src/commands/shared/logger.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/cli-utils/src/commands/shared/logger.ts b/packages/cli-utils/src/commands/shared/logger.ts index e77af687..f80a7afd 100644 --- a/packages/cli-utils/src/commands/shared/logger.ts +++ b/packages/cli-utils/src/commands/shared/logger.ts @@ -53,7 +53,10 @@ export function externalError(message: string, error: unknown) { (error.name === 'TSError' || error.name === 'TadaError' || 'code' in error) ) { title = 'code' in error ? 'System Error' : 'Error'; - text = (error as Error).message.trim(); + text = + error.name === 'TadaError' + ? t.text([t.cmd(t.CSI.Style, t.Style.Blue), (error as Error).message]) + : (error as Error).message.trim(); } else if ('stack' in error && typeof error.stack === 'string') { title = 'Unexpected Error'; text = `${error.stack}`; From ec23c89bf5e387176532ae314285e3149c6fb935 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 11:55:48 +0100 Subject: [PATCH 06/12] Remove redundant dependencies --- packages/cli-utils/package.json | 6 +----- pnpm-lock.yaml | 12 ------------ 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index cb8c8c09..bbfe5701 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -41,7 +41,6 @@ }, "devDependencies": { "@clack/prompts": "^0.7.0", - "@jridgewell/sourcemap-codec": "^1.4.15", "@types/node": "^20.11.0", "@volar/source-map": "^2.1.6", "clipanion": "4.0.0-rc.3", @@ -52,16 +51,13 @@ "typanion": "^3.14.0", "type-fest": "^4.10.2", "typescript": "^5.5.2", - "vscode-languageserver-textdocument": "^1.0.11", "wonka": "^6.3.4" }, "dependencies": { "@0no-co/graphqlsp": "^1.12.9", "@gql.tada/internal": "workspace:*", - "@vue/compiler-dom": "^3.4.23", "@vue/language-core": "~2.0.0", - "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", - "svelte2tsx": "^0.7.6" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" }, "peerDependenciesMeta": { "@gql.tada/svelte-support": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a5b2461..1c2845f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,25 +235,16 @@ importers: '@gql.tada/vue-support': specifier: workspace:* version: link:../vue-support - '@vue/compiler-dom': - specifier: ^3.4.23 - version: 3.4.25 '@vue/language-core': specifier: ~2.0.0 version: 2.0.29(typescript@5.5.2) graphql: specifier: ^15.5.0 || ^16.0.0 || ^17.0.0 version: 16.8.1 - svelte2tsx: - specifier: ^0.7.6 - version: 0.7.6(svelte@4.2.17)(typescript@5.5.2) devDependencies: '@clack/prompts': specifier: ^0.7.0 version: 0.7.0 - '@jridgewell/sourcemap-codec': - specifier: ^1.4.15 - version: 1.4.15 '@types/node': specifier: ^20.11.0 version: 20.11.0 @@ -284,9 +275,6 @@ importers: typescript: specifier: ^5.5.2 version: 5.5.2 - vscode-languageserver-textdocument: - specifier: ^1.0.11 - version: 1.0.11 wonka: specifier: ^6.3.4 version: 6.3.4 From d3452c00609ee18d4ecd6055a1bb3ef30e7e1f91 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 11:56:53 +0100 Subject: [PATCH 07/12] Remove non-inferred type for VirtualCode --- packages/cli-utils/package.json | 1 - packages/cli-utils/src/ts/transformers.ts | 3 +-- pnpm-lock.yaml | 3 --- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index bbfe5701..25acfb43 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -56,7 +56,6 @@ "dependencies": { "@0no-co/graphqlsp": "^1.12.9", "@gql.tada/internal": "workspace:*", - "@vue/language-core": "~2.0.0", "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" }, "peerDependenciesMeta": { diff --git a/packages/cli-utils/src/ts/transformers.ts b/packages/cli-utils/src/ts/transformers.ts index 886f3ec3..e4522253 100644 --- a/packages/cli-utils/src/ts/transformers.ts +++ b/packages/cli-utils/src/ts/transformers.ts @@ -1,6 +1,5 @@ import type ts from 'typescript'; import * as path from 'node:path'; -import type { VirtualCode } from '@vue/language-core'; import { TadaError } from '../utils/error'; @@ -55,7 +54,7 @@ const checkVue = async (): Promise => { export const transformExtensions = ['.svelte', '.vue'] as const; -export const transform = async (sourceFile: ts.SourceFile): Promise => { +export const transform = async (sourceFile: ts.SourceFile) => { const extname = path.extname(sourceFile.fileName); if (extname === '.svelte') { return transformSvelte(sourceFile); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c2845f8..6496cac9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -235,9 +235,6 @@ importers: '@gql.tada/vue-support': specifier: workspace:* version: link:../vue-support - '@vue/language-core': - specifier: ~2.0.0 - version: 2.0.29(typescript@5.5.2) graphql: specifier: ^15.5.0 || ^16.0.0 || ^17.0.0 version: 16.8.1 From 0e797909bc23414a57aa8144d5d2b91a61c7c6f6 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 11:58:19 +0100 Subject: [PATCH 08/12] Add changeset --- .changeset/beige-fishes-lay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/beige-fishes-lay.md diff --git a/.changeset/beige-fishes-lay.md b/.changeset/beige-fishes-lay.md new file mode 100644 index 00000000..f64751c6 --- /dev/null +++ b/.changeset/beige-fishes-lay.md @@ -0,0 +1,5 @@ +--- +"@gql.tada/cli-utils": minor +--- + +Split `.vue` and `.svelte` SFC file support out into support packages. If you need Vue support, you must now install `@gql.tada/vue-support` alongside `gql.tada`, and if you need Svelte support, you must now install `@gql.tada/svelte-support` alongside `gql.tada`. From 475530726346cd2442de04617c9ff4b52aa8c63c Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 11:58:47 +0100 Subject: [PATCH 09/12] Update LICENSE file --- packages/cli-utils/LICENSE.md | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/packages/cli-utils/LICENSE.md b/packages/cli-utils/LICENSE.md index 268a2bfc..9f28d59d 100644 --- a/packages/cli-utils/LICENSE.md +++ b/packages/cli-utils/LICENSE.md @@ -74,30 +74,6 @@ The above copyright notice and this permission notice shall be included in all c 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. -## @jridgewell/sourcemap-codec - -The MIT License - -Copyright (c) 2015 Rich Harris - -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. - ## @volar/source-map MIT License From 149cbae1d1c7e61831ea8344718cc43958e87685 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 12:41:05 +0100 Subject: [PATCH 10/12] Add error codes --- packages/cli-utils/src/ts/transformers.ts | 6 +++++- packages/cli-utils/src/utils/error.ts | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/cli-utils/src/ts/transformers.ts b/packages/cli-utils/src/ts/transformers.ts index e4522253..464382a9 100644 --- a/packages/cli-utils/src/ts/transformers.ts +++ b/packages/cli-utils/src/ts/transformers.ts @@ -1,7 +1,7 @@ import type ts from 'typescript'; import * as path from 'node:path'; -import { TadaError } from '../utils/error'; +import { TadaError, TadaErrorCode } from '../utils/error'; let _svelte: typeof import('@gql.tada/svelte-support'); let _vue: typeof import('@gql.tada/vue-support'); @@ -14,6 +14,7 @@ const transformSvelte = async ( _svelte = await import('@gql.tada/svelte-support'); } catch (_error) { throw new TadaError( + TadaErrorCode.SVELTE_SUPPORT, 'For Svelte support the `@gql.tada/svelte-support` package must be installed.\n' + 'Install the package and try again.' ); @@ -30,6 +31,7 @@ const transformVue = async ( _vue = await import('@gql.tada/vue-support'); } catch (_error) { throw new TadaError( + TadaErrorCode.VUE_SUPPORT, 'For Vue support the `@gql.tada/vue-support` package must be installed.\n' + 'Install the package and try again.' ); @@ -44,6 +46,7 @@ const checkVue = async (): Promise => { _vue = await import('@gql.tada/vue-support'); } catch (_error) { throw new TadaError( + TadaErrorCode.VUE_SUPPORT, 'For Vue support the `@gql.tada/vue-support` package must be installed.\n' + 'Install the package and try again.' ); @@ -63,6 +66,7 @@ export const transform = async (sourceFile: ts.SourceFile) => { return transformVue(sourceFile); } else { throw new TadaError( + TadaErrorCode.UNKNOWN_EXTERNAL_FILE, `Tried transforming unknown file type "${extname}". Supported: ${transformExtensions.join( ', ' )}` diff --git a/packages/cli-utils/src/utils/error.ts b/packages/cli-utils/src/utils/error.ts index 9cf6d34d..edc38c7e 100644 --- a/packages/cli-utils/src/utils/error.ts +++ b/packages/cli-utils/src/utils/error.ts @@ -1,6 +1,18 @@ +export const enum TadaErrorCode { + VUE_SUPPORT, + SVELTE_SUPPORT, + UNKNOWN_EXTERNAL_FILE, +} + export class TadaError extends Error { - constructor(message: string) { + static isTadaError(error: unknown): error is TadaError { + return !!(typeof error === 'object' && error && 'name' in error && error.name === 'TadaError'); + } + + readonly code: TadaErrorCode; + constructor(code: TadaErrorCode, message: string) { super(message); + this.code = code; this.name = 'TadaError'; } } From 9323569d1b9ab2e7df54c09806764974793fd96e Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 12:41:17 +0100 Subject: [PATCH 11/12] Add doctor step for external files --- .../src/commands/doctor/helpers/versions.ts | 28 ++++++++++ .../cli-utils/src/commands/doctor/runner.ts | 53 ++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/cli-utils/src/commands/doctor/helpers/versions.ts b/packages/cli-utils/src/commands/doctor/helpers/versions.ts index d398b017..69bf8947 100644 --- a/packages/cli-utils/src/commands/doctor/helpers/versions.ts +++ b/packages/cli-utils/src/commands/doctor/helpers/versions.ts @@ -57,3 +57,31 @@ export const getGqlTadaVersion = async (meta: PackageJson): Promise => { + const pkg = '@gql.tada/svelte-support'; + const isInstalled = !!meta.devDependencies?.[pkg] || !!meta.devDependencies?.[pkg]; + if (isInstalled) { + return true; + } + try { + // NOTE: Resolved from current folder, since it's a child dependency + return !!createRequire(__dirname)(`${pkg}/package.json`)?.version; + } catch (_error) { + return false; + } +}; + +export const hasVueSupport = async (meta: PackageJson): Promise => { + const pkg = '@gql.tada/vue-support'; + const isInstalled = !!meta.devDependencies?.[pkg] || !!meta.devDependencies?.[pkg]; + if (isInstalled) { + return true; + } + try { + // NOTE: Resolved from current folder, since it's a child dependency + return !!createRequire(__dirname)(`${pkg}/package.json`)?.version; + } catch (_error) { + return false; + } +}; diff --git a/packages/cli-utils/src/commands/doctor/runner.ts b/packages/cli-utils/src/commands/doctor/runner.ts index 74946533..60663cf1 100644 --- a/packages/cli-utils/src/commands/doctor/runner.ts +++ b/packages/cli-utils/src/commands/doctor/runner.ts @@ -1,3 +1,4 @@ +import type ts from 'typescript'; import path from 'node:path'; import type { GraphQLSPConfig, LoadConfigResult } from '@gql.tada/internal'; @@ -5,6 +6,7 @@ import { loadRef, loadConfig, parseConfig } from '@gql.tada/internal'; import type { ComposeInput } from '../../term'; import { MINIMUM_VERSIONS, semverComply } from '../../utils/semver'; +import { programFactory } from '../../ts'; import { findGraphQLConfig } from './helpers/graphqlConfig'; import * as versions from './helpers/versions'; import * as vscode from './helpers/vscode'; @@ -28,6 +30,7 @@ const enum Messages { CHECK_TS_VERSION = 'Checking TypeScript version', CHECK_DEPENDENCIES = 'Checking installed dependencies', CHECK_TSCONFIG = 'Checking tsconfig.json', + CHECK_EXTERNAL_FILES = 'Checking external files support', CHECK_VSCODE = 'Checking VSCode setup', CHECK_SCHEMA = 'Checking schema', } @@ -40,8 +43,7 @@ export async function* run(): AsyncIterable { // Check TypeScript version let packageJson: versions.PackageJson; try { - // packageJson = await versions.readPackageJson(); - packageJson = {}; + packageJson = await versions.readPackageJson(); } catch (_error) { yield logger.failedTask(Messages.CHECK_TS_VERSION); throw logger.errorMessage( @@ -146,6 +148,8 @@ export async function* run(): AsyncIterable { yield logger.completedTask(Messages.CHECK_TSCONFIG); + yield* runExternalFilesChecks(configResult, packageJson); + yield* runVSCodeChecks(); yield logger.runningTask(Messages.CHECK_SCHEMA); @@ -217,3 +221,48 @@ async function* runVSCodeChecks(): AsyncIterable { } } } + +async function* runExternalFilesChecks( + configResult: LoadConfigResult, + packageJson: versions.PackageJson +): AsyncIterable { + let externalFiles: readonly ts.SourceFile[] = []; + try { + const factory = programFactory(configResult); + externalFiles = factory.createExternalFiles(); + } catch (_error) { + // NOTE: If the project fails to load, we currently just ignore this check and move on + return; + } + + if (externalFiles.length) { + yield logger.runningTask(Messages.CHECK_EXTERNAL_FILES); + await delay(); + + const extensions = new Set( + externalFiles.map((sourceFile) => path.extname(sourceFile.fileName)) + ); + + if (extensions.has('.svelte') && !(await versions.hasSvelteSupport(packageJson))) { + yield logger.failedTask(Messages.CHECK_EXTERNAL_FILES); + throw logger.errorMessage( + `A version of ${logger.code( + '@gql.tada/svelte-support' + )} must be installed for Svelte file support.\n` + + logger.hint(`Have you installed ${logger.code('@gql.tada/svelte-support')}?`) + ); + } + + if (extensions.has('.vue') && !(await versions.hasVueSupport(packageJson))) { + yield logger.failedTask(Messages.CHECK_EXTERNAL_FILES); + throw logger.errorMessage( + `A version of ${logger.code( + '@gql.tada/vue-support' + )} must be installed for Vue file support.\n` + + logger.hint(`Have you installed ${logger.code('@gql.tada/vue-support')}?`) + ); + } + + yield logger.completedTask(Messages.CHECK_EXTERNAL_FILES); + } +} From 975d12ba2cc9a82e1c81ce4c4ed407c4a1d5ef1d Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Wed, 31 Jul 2024 18:15:18 +0100 Subject: [PATCH 12/12] Update Installation page with Vue/Svelte notes --- website/get-started/installation.md | 57 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/website/get-started/installation.md b/website/get-started/installation.md index 9a507de8..b52c677f 100644 --- a/website/get-started/installation.md +++ b/website/get-started/installation.md @@ -12,7 +12,7 @@ repository.](https://github.com/0no-co/gql.tada/blob/main/examples/example-pokem With `gql.tada`, you'll mainly interact with three different parts of the library: - the library code you import from the `gql.tada` package - the TypeScript plugin, `gql.tada/ts-plugin` -- and the `gql.tada` CLI +- and [the `gql.tada` CLI](/get-started/workflows) ## Step 1 — Installing packages @@ -340,6 +340,8 @@ to use it. ``` ::: +--- + ### VSCode Setup As shown above, `gql.tada` has a TypeScript plugin to provide @@ -365,3 +367,56 @@ To resolve this, you should create a `.vscode/settings.json` file to prompt you To enable syntax highlighting for GraphQL, you can install the official [“GraphQL: Syntax Highlighting” VSCode extension.](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax) + +--- + +### Vue and Svelte Support + +If you're using Vue's `.vue` files and Svelte's `.svelte` files, the +TypeScript plugin won't be able to run in your editor under normal +circumstances. +While some implementations exist for TypeScript to run and type +check Vue and Svelte files, `gql.tada` won't have any output in +these cases. + +However, while the TypeScript plugin may not support `.vue` and +`.svelte` files, [the `gql.tada check` command does.](/get-started/workflows#running-diagnostics) +To enable support for either files, you'll need to install the +corresponding support packages. + +::: code-group +```sh [npm] +# for Vue +npm install -D @gql.tada/vue-support +# for Svelte +npm install -D @gql.tada/svelte-support +``` + +```sh [pnpm] +# for Vue +pnpm add -D @gql.tada/vue-support +# for Svelte +pnpm add -D @gql.tada/svelte-support +``` + +```sh [yarn] +# for Vue +yarn add -D @gql.tada/vue-support +# for Svelte +yarn add -D @gql.tada/svelte-support +``` + +```sh [bun] +# for Vue +bun add -d @gql.tada/vue-support +# for Svelte +bun add -d @gql.tada/svelte-support +``` +::: + +Once these are installed, the CLI's `check` and other commands will +be able to parse and check external files for `gql.tada` errors. + + + Learn more about using the CLI +