From 600535ac4fda1309d4c183d06176f2e3d6bcb703 Mon Sep 17 00:00:00 2001 From: y-kurami Date: Thu, 6 Apr 2017 22:26:06 +0900 Subject: [PATCH] first commit --- .gitignore | 6 ++ LICENSE.txt | 21 +++++++ README.md | 12 ++++ package.json | 24 ++++++++ src/graphql-language-service-adapter.ts | 59 +++++++++++++++++++ src/index.ts | 39 ++++++++++++ src/language-service-proxy-builder.ts | 25 ++++++++ src/schema-json-manager.ts | 56 ++++++++++++++++++ .../index.d.ts | 37 ++++++++++++ tsconfig.json | 11 ++++ 10 files changed, 290 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 package.json create mode 100644 src/graphql-language-service-adapter.ts create mode 100644 src/index.ts create mode 100644 src/language-service-proxy-builder.ts create mode 100644 src/schema-json-manager.ts create mode 100644 src/typedef/graphql-language-service-interface/index.d.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..37a6dd4cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +built/ +*.log +*.swp +*.swo +.DS_Store diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..93d1f7a9f --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) [2017] [Quramy] + +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/README.md b/README.md new file mode 100644 index 000000000..f1d6f508b --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# ts-graphql-plugin + +TypeScript Language Service Pugin for GraphQL. + +## Features +*T.B.D.* + +## How to install +*T.B.D.* + +## License +This software is released under the MIT License, see LICENSE.txt. diff --git a/package.json b/package.json new file mode 100644 index 000000000..4438c1885 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "ts-graphql-plugin", + "version": "0.1.0", + "description": "TypeScript Language Service Plugin for GraphQL", + "main": "built/index.js", + "scripts": { + "compile": "tsc -p .", + "watch": "tsc -w -p .", + "prepublish": "npm run compile" + }, + "author": "Quramy", + "license": "MIT", + "dependencies": { + "graphql": "^0.9.1", + "graphql-language-service-interface": "0.0.4" + }, + "devDependencies": { + "typescript": "^2.2.2", + "@types/node": "^7.0.8" + }, + "peerDependencies": { + "typescript": "^2.2.2" + } +} diff --git a/src/graphql-language-service-adapter.ts b/src/graphql-language-service-adapter.ts new file mode 100644 index 000000000..608f8bc8a --- /dev/null +++ b/src/graphql-language-service-adapter.ts @@ -0,0 +1,59 @@ +import * as ts from 'typescript/lib/tsserverlibrary'; + +import { buildClientSchema } from 'graphql'; +import { CompletionItem, getAutocompleteSuggestions } from 'graphql-language-service-interface'; + +export interface GraphQLLanguageServiceAdapterCreateOptions { + schema?: any; + logger?: (msg: string) => void; +} + +export class GraphQLLanguageServiceAdapter { + + private _schema: any; + private _logger: (msg: string) => void = () => { }; + + constructor( + private _getNode: (fileName: string, position) => ts.Node = () => null, + opt: GraphQLLanguageServiceAdapterCreateOptions = { }, + ) { + if (opt.logger) this._logger = opt.logger; + if (opt.schema) this.updateSchema(opt.schema); + } + + updateSchema(schema: { data: any }) { + this._schema = buildClientSchema(schema.data); + } + + getCompletionInfo(delegate: (fileName: string, position: number) => ts.CompletionInfo, fileName: string, position: number, ) { + if (!this._schema) return delegate(fileName, position); + const node = this._getNode(fileName, position); + if (!node || node.kind !== ts.SyntaxKind.NoSubstitutionTemplateLiteral) { + return delegate(fileName, position); + } + const cursor = position - node.getStart(); + const text = node.getText(); + const gqlCompletionItems = getAutocompleteSuggestions(this._schema, text, cursor); + this._logger(JSON.stringify(gqlCompletionItems)); + return translateCompletionItems(gqlCompletionItems); + } + +} + +function translateCompletionItems(items: CompletionItem[]): ts.CompletionInfo { + const result: ts.CompletionInfo = { + isGlobalCompletion: false, + isMemberCompletion: false, + isNewIdentifierLocation: false, + entries: items.map(r => { + const kind = r.kind ? r.kind + '' : 'unknown'; + return { + name: r.label, + kindModifiers: 'declare', + kind, + sortText: '0', + }; + }), + }; + return result; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..4976307c1 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,39 @@ +import * as ts from 'typescript/lib/tsserverlibrary'; +import { GraphQLLanguageServiceAdapter } from "./graphql-language-service-adapter"; +import { LanguageServiceProxyBuilder } from "./language-service-proxy-builder"; +import { SchamaJsonManager } from "./schema-json-manager"; + +function findNode(sourceFile: ts.SourceFile, position: number): ts.Node | undefined { + function find(node: ts.Node): ts.Node|undefined { + if (position >= node.getStart() && position < node.getEnd()) { + return ts.forEachChild(node, find) || node; + } + } + return find(sourceFile); +} + +function create(info: ts.server.PluginCreateInfo): ts.LanguageService { + const getNode = (fileName: string, position: number) => findNode(info.languageService.getProgram().getSourceFile(fileName), position); + const logger = (msg: string) => info.project.projectService.logger.info(msg); + const program = info.languageService.getProgram(); + + const schemaManager = new SchamaJsonManager(info); + const schema = schemaManager.getSchema(); + const adapter = new GraphQLLanguageServiceAdapter(getNode, { schema, logger }); + + const proxy = new LanguageServiceProxyBuilder(info) + .wrap("getCompletionsAtPosition", delegate => adapter.getCompletionInfo.bind(adapter, delegate)) + .build() + ; + + schemaManager.registerOnChange(adapter.updateSchema.bind(adapter)); + schemaManager.startWatch(); + + return proxy; +} + +const moduleFactory: ts.server.PluginModuleFactory = function(mod: { typescript: typeof ts }) { + return { create }; +}; + +export = moduleFactory; diff --git a/src/language-service-proxy-builder.ts b/src/language-service-proxy-builder.ts new file mode 100644 index 000000000..42b54ec22 --- /dev/null +++ b/src/language-service-proxy-builder.ts @@ -0,0 +1,25 @@ +import * as ts from 'typescript/lib/tsserverlibrary'; + +export interface LanguageServiceMethodWrapper { + (delegate: ts.LanguageService[K], info?: ts.server.PluginCreateInfo): ts.LanguageService[K]; +} + +export class LanguageServiceProxyBuilder { + + private _wrappers = []; + + constructor(private _info: ts.server.PluginCreateInfo) { } + + wrap(name: K, wrapper: LanguageServiceMethodWrapper) { + this._wrappers.push({ name, wrapper }); + return this; + } + + build() { + const ret = this._info.languageService; + this._wrappers.forEach(({ name, wrapper }) => { + ret[name] = wrapper(this._info.languageService[name], this._info); + }); + return ret; + } +} diff --git a/src/schema-json-manager.ts b/src/schema-json-manager.ts new file mode 100644 index 000000000..d9e4fe5c5 --- /dev/null +++ b/src/schema-json-manager.ts @@ -0,0 +1,56 @@ +import * as ts from 'typescript/lib/tsserverlibrary'; + +export class SchamaJsonManager { + private _schemaPath: string; + private _watcher: ts.FileWatcher; + private _onChanges: ((schema: any) => void)[] = []; + + constructor(private _info: ts.server.PluginCreateInfo) { + this._schemaPath = this._info.config.schema; + } + + private _log(msg: string) { + this._info.project.projectService.logger.info(msg); + } + + getSchema() { + if (!this._schemaPath || typeof this._schemaPath !== 'string') return; + try { + const isExists = this._info.languageServiceHost.fileExists(this._schemaPath); + if (!isExists) return; + return JSON.parse(this._info.languageServiceHost.readFile(this._schemaPath, 'utf-8')); + } catch (e) { + this._log('Fail to read schema file...'); + this._log(e.message); + return; + } + } + + registerOnChange(cb: (schema: any) => void) { + this._onChanges.push(cb); + return () => { + this._onChanges.filter(x => x !== cb); + }; + } + + startWatch(interval: number = 100) { + try { + this._watcher = this._info.serverHost.watchFile(this._schemaPath, () => { + this._log("Change schema file."); + if (this._onChanges.length) { + const schema = this.getSchema(); + if (schema) this._onChanges.forEach(cb => cb(schema)); + } + }, interval); + } catch (e) { + this._log('Fail to read schema file...'); + this._log(e.message); + return; + } + } + + closeWatch() { + if (this._watcher) this._watcher.close(); + } + +} diff --git a/src/typedef/graphql-language-service-interface/index.d.ts b/src/typedef/graphql-language-service-interface/index.d.ts new file mode 100644 index 000000000..eba55a677 --- /dev/null +++ b/src/typedef/graphql-language-service-interface/index.d.ts @@ -0,0 +1,37 @@ +declare module 'graphql-language-service-interface' { + export interface State { + level: number; + levels?: number[]; + prevState: State; + rule: any; // tmp + kind: string; + name: string; + type: string; + step: number; + needsSeperator: boolean; + needsAdvance?: boolean; + indentLevel?: number; + } + + export interface ContextToken { + start: number; + end: number; + string: string; + state: State; + style: string; + } + + export interface CompletionItem { + label: string; + kind?: number; + detail?: string; + documentation?: string; + // GraphQL Deprecation information + isDeprecated?: string; + deprecationReason?: string; + } + + export function getTokenAtPosition(queryText: string, cursor: number): ContextToken; + + export function getAutocompleteSuggestions(schema: any, queryText: string, cursor: number, contextToken?: ContextToken): CompletionItem[]; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..9317c8d0e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "sourceMap": false, + "outDir": "built", + "rootDir": "src", + "lib": ["es2015"] + }, + "exclude": ["built", "node_modules"] +}