├── .gitattributes ├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── jsLibraryMappings.xml ├── misc.xml ├── modules.xml └── runConfigurations │ └── bin_server_js.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── server.js ├── js-graphql-language-service.iml ├── package.json ├── schemas └── builtin-schema.json └── src ├── languageservice.js ├── project.js ├── relay-templates.js └── tests ├── data ├── getAST.json ├── getAnnotations.graphql ├── getAnnotations.json ├── getHintsForField.json ├── getHintsForType.json ├── getSchema.txt ├── getTokenDocumentation.json ├── getTokens.graphql ├── getTokens.json ├── getTypeDocumentation.json ├── projects │ ├── todoapp-modern │ │ ├── graphql.config.json │ │ ├── todoAppModernExpectedSchema.txt │ │ └── todoapp-modern.graphql │ └── todoapp │ │ ├── getAnnotations.graphql │ │ ├── getApolloAnnotations.graphql │ │ ├── getLokkaAnnotations.graphql │ │ ├── getSchemaTokens.json │ │ ├── graphql.config.json │ │ ├── schema.json │ │ └── todoAppExpectedSchema.txt └── relay │ ├── commentBeforeFragment.json │ ├── multiplePlaceholdersPerLine.json │ ├── templateFragment1.graphql │ ├── templateFragment1.json │ ├── templateFragment2.graphql │ ├── templateFragment2.json │ ├── templateFragment3.json │ └── templateFragment4.json └── spec.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Bundles 2 | dist 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 31 | node_modules 32 | 33 | # ========================= 34 | # Operating System Files 35 | # ========================= 36 | 37 | # OSX 38 | # ========================= 39 | 40 | .DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | 44 | # Thumbnails 45 | ._* 46 | 47 | # Files that might appear in the root of a volume 48 | .DocumentRevisions-V100 49 | .fseventsd 50 | .Spotlight-V100 51 | .TemporaryItems 52 | .Trashes 53 | .VolumeIcon.icns 54 | 55 | # Directories potentially created on remote AFP share 56 | .AppleDB 57 | .AppleDesktop 58 | Network Trash Folder 59 | Temporary Items 60 | .apdisk 61 | 62 | # Windows 63 | # ========================= 64 | 65 | # Windows image file caches 66 | Thumbs.db 67 | ehthumbs.db 68 | 69 | # Folder config file 70 | Desktop.ini 71 | 72 | # Recycle Bin used on file shares 73 | $RECYCLE.BIN/ 74 | 75 | # Windows Installer files 76 | *.cab 77 | *.msi 78 | *.msm 79 | *.msp 80 | 81 | # Windows shortcuts 82 | *.lnk 83 | 84 | .idea/dictionaries/* 85 | .idea/workspace.xml 86 | .idea/uiDesigner.xml 87 | .idea/vcs.xml -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | js-graphql-language-service -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/bin_server_js.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.5.1 (2018-03-04) 2 | 3 | Features: 4 | 5 | - Support for strongly typed variable placeholders in GraphQL tagged templates (#19) 6 | 7 | ## 1.5.0 (2017-09-20) 8 | 9 | Features: 10 | 11 | - Support for loading the schema from .graphql file (Relay Modern projects) (#16) 12 | 13 | 14 | ## 1.4.0 (2017-01-29) 15 | 16 | Features: 17 | 18 | - Upgraded to `graphql 0.9.1` and `codemirror-graphql 0.6.2` (#10, #12, #14) 19 | - Add support for top level Apollo fragment template placeholders (#13) 20 | 21 | Fixes: 22 | 23 | - Only use comment for placeholder if no significant tokens follow on the same line (#15) 24 | 25 | ## 1.3.2 (2016-10-30) 26 | 27 | Fixes: 28 | 29 | - Object literal for variables in getFragment closes Relay.QL template expression (#9) 30 | 31 | ## 1.3.1 (2016-09-25) 32 | 33 | Features: 34 | 35 | - Support __schema root in schema.json (#7) 36 | 37 | ## 1.3.0 (2016-09-11) 38 | 39 | Features: 40 | 41 | - Support for gql apollo and lokka environments (#6) 42 | 43 | ## 1.2.1 (2016-09-09) 44 | 45 | Fixes: 46 | 47 | - Invalid "Relay mutation must have a selection of subfields" error (#5) 48 | 49 | ## 1.2.0 (2016-08-28) 50 | 51 | Changes: 52 | 53 | - Upgraded to `graphql 0.7.0` to support breaking change in directive locations introspection (#4) 54 | - Upgraded to `codemirror-graphql 0.5.4` to support schema shorthand syntax highlighting (#4) 55 | 56 | ## 1.1.2 (2016-06-09) 57 | 58 | Fixes: 59 | 60 | - Increased maximum size of JSON schema from 100kb to 32mb (#3) 61 | 62 | ## 1.1.1 (2016-02-03) 63 | 64 | Changes: 65 | 66 | - Upgraded to `graphql 0.4.16`. 67 | - Upgraded to `codemirror-graphql 0.2.2` to enable fragment suggestions after '...'. See [js-graphql-intellij-plugin/issues/4](https://github.com/jimkyndemeyer/js-graphql-intellij-plugin/issues/4) 68 | 69 | ## 1.1.0 (2016-01-31) 70 | 71 | Features: 72 | 73 | - Support for GraphQL Schema Language (#1) 74 | 75 | 76 | ## 1.0.0 (2015-12-13) 77 | 78 | Features: 79 | 80 | - Initial release. Tokens, Annotations, Hints, Schema, Documentation. 81 | 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Jim Kynde Meyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/jimkyndemeyer/js-graphql-intellij-plugin/raw/master/docs/js-graphql-logo.png) 2 | 3 | # JS GraphQL Language Service 4 | 5 | **This repo is no longer maintained. It was made obsolete by the release of js-graphql-intellij-plugin 2.0** 6 | 7 | A Node.js powered language service that provides a GraphQL language API on top of [codemirror-graphql](https://github.com/graphql/codemirror-graphql) and [graphql-js](https://github.com/graphql/graphql-js). 8 | 9 | It provided various GraphQL language features in [js-graphql-intellij-plugin](https://github.com/jimkyndemeyer/js-graphql-intellij-plugin) 1.x for IntelliJ IDEA and WebStorm, but was replaced with a Java-based native implementaton in 2.0. 10 | 11 | Features: 12 | 13 | - Schema-aware completion and error highlighting 14 | - Syntax highlighting 15 | - Configurable GraphQL schema retrieval and reloading based on a local file or a url using 'then-request' 16 | - Schema documentation for types and tokens 17 | 18 | Inspired by TypeScript's language service, this project makes it possible to leverage Facebook's JavaScript implementation of GraphQL inside IDEs such as IntelliJ IDEA and WebStorm -- without re-implementing GraphQL in Java. 19 | 20 | ## Running 21 | `npm run-script start` starts the language service at http://127.0.0.1:3000/js-graphql-language-service 22 | 23 | ## Using the Language Service API 24 | 25 | The API is based on POSTing JSON commands. The following commands are supported: 26 | 27 | - `setProjectDir`: Set the project directory from which a `graphql.config.json` can be loaded to determine how the Schema can be retrieved from a file or url 28 | - `getSchema`: Gets the GraphQL schema that has been loaded using the `setProjectDir` command, falling back to a bare-bones default schema 29 | - `getTokens`: Gets the tokens contained in a buffer. Relay.QL and gql templating is supported unless env is passed as `{"env":"graphql"}` 30 | - `getAnnotations`: Gets the errors contained in a buffer, optionally with support for Relay.QL templates using `{"env":"relay"}` 31 | - `getHints`: Gets the schema-aware completions for a buffer at a specified `line` anc `ch`, optionally with support for Relay.QL templates using `{"env":"relay"}` 32 | - `getAST`: Gets the GraphQL AST of a buffer, optionally with support for Relay.QL templates using `{env:"relay"}` 33 | - `getTokenDocumentation`: Gets the schema documentation for a token in a buffer at a specified `line` and `ch`, optionally with support for Relay.QL templates using `{"env":"relay"}` 34 | - `getTypeDocumentation`: Gets schema documentation for a specific GraphQL Type based on the description, fields, implementations, and interfaces specified in the current schema 35 | 36 | Supported environments using the `env` parameter: 37 | 38 | - `graphql`: For GraphQL file buffers. No templating is processed, and all error annotations are returned. 39 | - `graphql-template`: For use with `graphql` tagged templates that contain placeholders, e.g. for variables. Annotations are filtered to allow placeholder use cases. 40 | - `relay`: For use with `Relay.QL` tagged Relay templates, e.g. as injections in a JavaScript and TypeScript buffer. Annotations are filtered to allow Relay use cases. 41 | - `apollo`: For use with `gql` tagged Apollo Client templates, e.g. as injections in a JavaScript and TypeScript buffer. Annotations are filtered to allow Apollo use cases. 42 | - `lokka`: For use with `gql` tagged Lokka Client templates, e.g. as injections in a JavaScript and TypeScript buffer. Annotations are filtered to allow Lokka use cases. 43 | 44 | Please see [src/tests/spec.js](src/tests/spec.js) for examples of how to use the language service, and [src/tests/data](src/tests/data) for the response data. 45 | 46 | ## Building 47 | 48 | Make sure browserify is installed globally with: 49 | 50 | ``` 51 | npm install -g browserify 52 | ``` 53 | 54 | To bundle the bin\server.js file into a single dist/js-graphql-language-service.dist.js file for distribution: 55 | 56 | ``` 57 | npm run-script bundle-to-dist 58 | ``` 59 | 60 | Or, the dist file can be outputted directly into a `js-graphql-intellij-plugin` checkout using: 61 | 62 | ``` 63 | npm run-script bundle-to-intellij-plugin 64 | ``` 65 | 66 | ## License 67 | MIT 68 | -------------------------------------------------------------------------------- /bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | let app = require('../src/languageservice'); 9 | let http = require('http'); 10 | 11 | 12 | /** 13 | * Get port from environment or command line and store in Express. 14 | */ 15 | 16 | let portAsString = process.env.PORT || '3000'; 17 | 18 | process.argv.forEach(function(arg) { 19 | if(arg.indexOf('--port=') != -1) { 20 | portAsString = arg.substring(7) 21 | } 22 | }); 23 | 24 | let port = normalizePort(portAsString); 25 | app.set('port', port); 26 | 27 | /** 28 | * Create HTTP server. 29 | */ 30 | 31 | let server = http.createServer(app); 32 | 33 | /** 34 | * Listen on provided port, on all network interfaces. 35 | */ 36 | 37 | server.listen(port, 'localhost'); 38 | server.on('error', onError); 39 | server.on('listening', onListening); 40 | 41 | /** 42 | * Normalize a port into a number, string, or false. 43 | */ 44 | 45 | function normalizePort(val) { 46 | let port = parseInt(val, 10); 47 | 48 | if (isNaN(port)) { 49 | // named pipe 50 | return val; 51 | } 52 | 53 | if (port >= 0) { 54 | // port number 55 | return port; 56 | } 57 | 58 | return false; 59 | } 60 | 61 | /** 62 | * Event listener for HTTP server "error" event. 63 | */ 64 | 65 | function onError(error) { 66 | if (error.syscall !== 'listen') { 67 | throw error; 68 | } 69 | 70 | let bind = typeof port === 'string' 71 | ? 'Pipe ' + port 72 | : 'Port ' + port; 73 | 74 | // handle specific listen errors with friendly messages 75 | switch (error.code) { 76 | case 'EACCES': 77 | console.error(bind + ' requires elevated privileges'); 78 | process.exit(1); 79 | break; 80 | case 'EADDRINUSE': 81 | console.error(bind + ' is already in use'); 82 | process.exit(1); 83 | break; 84 | default: 85 | throw error; 86 | } 87 | } 88 | 89 | /** 90 | * Event listener for HTTP server "listening" event. 91 | */ 92 | 93 | function onListening() { 94 | let addr = server.address(); 95 | let bind = typeof addr === 'string' 96 | ? 'pipe ' + addr 97 | : ':' + addr.port; 98 | console.log('JS GraphQL listening on http://' + addr.address + bind + '/js-graphql-language-service'); 99 | } 100 | -------------------------------------------------------------------------------- /js-graphql-language-service.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-graphql-language-service", 3 | "version": "1.5.1", 4 | "private": true, 5 | "contributors": [ 6 | "Jim Kynde Meyer " 7 | ], 8 | "homepage": "https://github.com/jimkyndemeyer/js-graphql-language-service", 9 | "bugs": { 10 | "url": "https://github.com/jimkyndemeyer/js-graphql-language-service/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/jimkyndemeyer/js-graphql-language-service.git" 15 | }, 16 | "license": "MIT", 17 | "scripts": { 18 | "start": "node ./bin/server.js", 19 | "test": "mocha -R spec src/tests/spec.js", 20 | "bundle-to-dist": "browserify --node --ignore-missing --entry bin/server.js --outfile dist/js-graphql-language-service.dist.js", 21 | "bundle-to-intellij-plugin": "browserify --node --ignore-missing --entry bin/server.js --outfile ./../js-graphql-intellij-plugin/resources/META-INF/dist/js-graphql-language-service.dist.js" 22 | }, 23 | "dependencies": { 24 | "body-parser": "^1.16.0", 25 | "codemirror": "^5.23.0", 26 | "codemirror-graphql": "^0.6.2", 27 | "express": "~4.14.0", 28 | "filewatcher": "^3.0.1", 29 | "graphql": "^0.9.1", 30 | "hashmap": "^2.0.6", 31 | "mock-browser": "^0.92.12", 32 | "stackjs": "^0.1.0", 33 | "then-request": "^2.2.0" 34 | }, 35 | "devDependencies": { 36 | "mocha": "^3.2.0", 37 | "supertest": "^2.0.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /schemas/builtin-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "__schema": { 4 | "queryType": { 5 | "name": "Query" 6 | }, 7 | "mutationType": { 8 | "name": "Mutation" 9 | }, 10 | "types": [ 11 | { 12 | "kind": "OBJECT", 13 | "name": "Query", 14 | "description": null, 15 | "fields": [ 16 | { 17 | "name": "node", 18 | "description": "Fetches an object given its ID", 19 | "args": [ 20 | { 21 | "name": "id", 22 | "description": "The ID of an object", 23 | "type": { 24 | "kind": "NON_NULL", 25 | "name": null, 26 | "ofType": { 27 | "kind": "SCALAR", 28 | "name": "ID", 29 | "ofType": null 30 | } 31 | }, 32 | "defaultValue": null 33 | } 34 | ], 35 | "type": { 36 | "kind": "INTERFACE", 37 | "name": "Node", 38 | "ofType": null 39 | }, 40 | "isDeprecated": false, 41 | "deprecationReason": null 42 | } 43 | ], 44 | "inputFields": null, 45 | "interfaces": [], 46 | "enumValues": null, 47 | "possibleTypes": null 48 | }, 49 | { 50 | "kind": "SCALAR", 51 | "name": "ID", 52 | "description": null, 53 | "fields": null, 54 | "inputFields": null, 55 | "interfaces": null, 56 | "enumValues": null, 57 | "possibleTypes": null 58 | }, 59 | { 60 | "kind": "INTERFACE", 61 | "name": "Node", 62 | "description": "An object with an ID", 63 | "fields": [ 64 | { 65 | "name": "id", 66 | "description": "The id of the object.", 67 | "args": [], 68 | "type": { 69 | "kind": "NON_NULL", 70 | "name": null, 71 | "ofType": { 72 | "kind": "SCALAR", 73 | "name": "ID", 74 | "ofType": null 75 | } 76 | }, 77 | "isDeprecated": false, 78 | "deprecationReason": null 79 | } 80 | ], 81 | "inputFields": null, 82 | "interfaces": null, 83 | "enumValues": null, 84 | "possibleTypes": [ 85 | ] 86 | }, 87 | { 88 | "kind": "SCALAR", 89 | "name": "String", 90 | "description": null, 91 | "fields": null, 92 | "inputFields": null, 93 | "interfaces": null, 94 | "enumValues": null, 95 | "possibleTypes": null 96 | }, 97 | { 98 | "kind": "SCALAR", 99 | "name": "Int", 100 | "description": null, 101 | "fields": null, 102 | "inputFields": null, 103 | "interfaces": null, 104 | "enumValues": null, 105 | "possibleTypes": null 106 | }, 107 | { 108 | "kind": "OBJECT", 109 | "name": "PageInfo", 110 | "description": "Information about pagination in a connection.", 111 | "fields": [ 112 | { 113 | "name": "hasNextPage", 114 | "description": "When paginating forwards, are there more items?", 115 | "args": [], 116 | "type": { 117 | "kind": "NON_NULL", 118 | "name": null, 119 | "ofType": { 120 | "kind": "SCALAR", 121 | "name": "Boolean", 122 | "ofType": null 123 | } 124 | }, 125 | "isDeprecated": false, 126 | "deprecationReason": null 127 | }, 128 | { 129 | "name": "hasPreviousPage", 130 | "description": "When paginating backwards, are there more items?", 131 | "args": [], 132 | "type": { 133 | "kind": "NON_NULL", 134 | "name": null, 135 | "ofType": { 136 | "kind": "SCALAR", 137 | "name": "Boolean", 138 | "ofType": null 139 | } 140 | }, 141 | "isDeprecated": false, 142 | "deprecationReason": null 143 | }, 144 | { 145 | "name": "startCursor", 146 | "description": "When paginating backwards, the cursor to continue.", 147 | "args": [], 148 | "type": { 149 | "kind": "SCALAR", 150 | "name": "String", 151 | "ofType": null 152 | }, 153 | "isDeprecated": false, 154 | "deprecationReason": null 155 | }, 156 | { 157 | "name": "endCursor", 158 | "description": "When paginating forwards, the cursor to continue.", 159 | "args": [], 160 | "type": { 161 | "kind": "SCALAR", 162 | "name": "String", 163 | "ofType": null 164 | }, 165 | "isDeprecated": false, 166 | "deprecationReason": null 167 | } 168 | ], 169 | "inputFields": null, 170 | "interfaces": [], 171 | "enumValues": null, 172 | "possibleTypes": null 173 | }, 174 | { 175 | "kind": "SCALAR", 176 | "name": "Boolean", 177 | "description": null, 178 | "fields": null, 179 | "inputFields": null, 180 | "interfaces": null, 181 | "enumValues": null, 182 | "possibleTypes": null 183 | }, 184 | { 185 | "kind": "OBJECT", 186 | "name": "Mutation", 187 | "description": null, 188 | "fields": [ 189 | { 190 | "name": "id", 191 | "description": "The id of the object.", 192 | "args": [], 193 | "type": { 194 | "kind": "NON_NULL", 195 | "name": null, 196 | "ofType": { 197 | "kind": "SCALAR", 198 | "name": "ID", 199 | "ofType": null 200 | } 201 | }, 202 | "isDeprecated": false, 203 | "deprecationReason": null 204 | } 205 | ], 206 | "inputFields": null, 207 | "interfaces": [], 208 | "enumValues": null, 209 | "possibleTypes": null 210 | }, 211 | { 212 | "kind": "OBJECT", 213 | "name": "__Schema", 214 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query and mutation operations.", 215 | "fields": [ 216 | { 217 | "name": "types", 218 | "description": "A list of all types supported by this server.", 219 | "args": [], 220 | "type": { 221 | "kind": "NON_NULL", 222 | "name": null, 223 | "ofType": { 224 | "kind": "LIST", 225 | "name": null, 226 | "ofType": { 227 | "kind": "NON_NULL", 228 | "name": null, 229 | "ofType": { 230 | "kind": "OBJECT", 231 | "name": "__Type" 232 | } 233 | } 234 | } 235 | }, 236 | "isDeprecated": false, 237 | "deprecationReason": null 238 | }, 239 | { 240 | "name": "queryType", 241 | "description": "The type that query operations will be rooted at.", 242 | "args": [], 243 | "type": { 244 | "kind": "NON_NULL", 245 | "name": null, 246 | "ofType": { 247 | "kind": "OBJECT", 248 | "name": "__Type", 249 | "ofType": null 250 | } 251 | }, 252 | "isDeprecated": false, 253 | "deprecationReason": null 254 | }, 255 | { 256 | "name": "mutationType", 257 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 258 | "args": [], 259 | "type": { 260 | "kind": "OBJECT", 261 | "name": "__Type", 262 | "ofType": null 263 | }, 264 | "isDeprecated": false, 265 | "deprecationReason": null 266 | }, 267 | { 268 | "name": "directives", 269 | "description": "A list of all directives supported by this server.", 270 | "args": [], 271 | "type": { 272 | "kind": "NON_NULL", 273 | "name": null, 274 | "ofType": { 275 | "kind": "LIST", 276 | "name": null, 277 | "ofType": { 278 | "kind": "NON_NULL", 279 | "name": null, 280 | "ofType": { 281 | "kind": "OBJECT", 282 | "name": "__Directive" 283 | } 284 | } 285 | } 286 | }, 287 | "isDeprecated": false, 288 | "deprecationReason": null 289 | } 290 | ], 291 | "inputFields": null, 292 | "interfaces": [], 293 | "enumValues": null, 294 | "possibleTypes": null 295 | }, 296 | { 297 | "kind": "OBJECT", 298 | "name": "__Type", 299 | "description": null, 300 | "fields": [ 301 | { 302 | "name": "kind", 303 | "description": null, 304 | "args": [], 305 | "type": { 306 | "kind": "NON_NULL", 307 | "name": null, 308 | "ofType": { 309 | "kind": "ENUM", 310 | "name": "__TypeKind", 311 | "ofType": null 312 | } 313 | }, 314 | "isDeprecated": false, 315 | "deprecationReason": null 316 | }, 317 | { 318 | "name": "name", 319 | "description": null, 320 | "args": [], 321 | "type": { 322 | "kind": "SCALAR", 323 | "name": "String", 324 | "ofType": null 325 | }, 326 | "isDeprecated": false, 327 | "deprecationReason": null 328 | }, 329 | { 330 | "name": "description", 331 | "description": null, 332 | "args": [], 333 | "type": { 334 | "kind": "SCALAR", 335 | "name": "String", 336 | "ofType": null 337 | }, 338 | "isDeprecated": false, 339 | "deprecationReason": null 340 | }, 341 | { 342 | "name": "fields", 343 | "description": null, 344 | "args": [ 345 | { 346 | "name": "includeDeprecated", 347 | "description": null, 348 | "type": { 349 | "kind": "SCALAR", 350 | "name": "Boolean", 351 | "ofType": null 352 | }, 353 | "defaultValue": "false" 354 | } 355 | ], 356 | "type": { 357 | "kind": "LIST", 358 | "name": null, 359 | "ofType": { 360 | "kind": "NON_NULL", 361 | "name": null, 362 | "ofType": { 363 | "kind": "OBJECT", 364 | "name": "__Field", 365 | "ofType": null 366 | } 367 | } 368 | }, 369 | "isDeprecated": false, 370 | "deprecationReason": null 371 | }, 372 | { 373 | "name": "interfaces", 374 | "description": null, 375 | "args": [], 376 | "type": { 377 | "kind": "LIST", 378 | "name": null, 379 | "ofType": { 380 | "kind": "NON_NULL", 381 | "name": null, 382 | "ofType": { 383 | "kind": "OBJECT", 384 | "name": "__Type", 385 | "ofType": null 386 | } 387 | } 388 | }, 389 | "isDeprecated": false, 390 | "deprecationReason": null 391 | }, 392 | { 393 | "name": "possibleTypes", 394 | "description": null, 395 | "args": [], 396 | "type": { 397 | "kind": "LIST", 398 | "name": null, 399 | "ofType": { 400 | "kind": "NON_NULL", 401 | "name": null, 402 | "ofType": { 403 | "kind": "OBJECT", 404 | "name": "__Type", 405 | "ofType": null 406 | } 407 | } 408 | }, 409 | "isDeprecated": false, 410 | "deprecationReason": null 411 | }, 412 | { 413 | "name": "enumValues", 414 | "description": null, 415 | "args": [ 416 | { 417 | "name": "includeDeprecated", 418 | "description": null, 419 | "type": { 420 | "kind": "SCALAR", 421 | "name": "Boolean", 422 | "ofType": null 423 | }, 424 | "defaultValue": "false" 425 | } 426 | ], 427 | "type": { 428 | "kind": "LIST", 429 | "name": null, 430 | "ofType": { 431 | "kind": "NON_NULL", 432 | "name": null, 433 | "ofType": { 434 | "kind": "OBJECT", 435 | "name": "__EnumValue", 436 | "ofType": null 437 | } 438 | } 439 | }, 440 | "isDeprecated": false, 441 | "deprecationReason": null 442 | }, 443 | { 444 | "name": "inputFields", 445 | "description": null, 446 | "args": [], 447 | "type": { 448 | "kind": "LIST", 449 | "name": null, 450 | "ofType": { 451 | "kind": "NON_NULL", 452 | "name": null, 453 | "ofType": { 454 | "kind": "OBJECT", 455 | "name": "__InputValue", 456 | "ofType": null 457 | } 458 | } 459 | }, 460 | "isDeprecated": false, 461 | "deprecationReason": null 462 | }, 463 | { 464 | "name": "ofType", 465 | "description": null, 466 | "args": [], 467 | "type": { 468 | "kind": "OBJECT", 469 | "name": "__Type", 470 | "ofType": null 471 | }, 472 | "isDeprecated": false, 473 | "deprecationReason": null 474 | } 475 | ], 476 | "inputFields": null, 477 | "interfaces": [], 478 | "enumValues": null, 479 | "possibleTypes": null 480 | }, 481 | { 482 | "kind": "ENUM", 483 | "name": "__TypeKind", 484 | "description": "An enum describing what kind of type a given __Type is", 485 | "fields": null, 486 | "inputFields": null, 487 | "interfaces": null, 488 | "enumValues": [ 489 | { 490 | "name": "SCALAR", 491 | "description": "Indicates this type is a scalar.", 492 | "isDeprecated": false, 493 | "deprecationReason": null 494 | }, 495 | { 496 | "name": "OBJECT", 497 | "description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.", 498 | "isDeprecated": false, 499 | "deprecationReason": null 500 | }, 501 | { 502 | "name": "INTERFACE", 503 | "description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.", 504 | "isDeprecated": false, 505 | "deprecationReason": null 506 | }, 507 | { 508 | "name": "UNION", 509 | "description": "Indicates this type is a union. `possibleTypes` is a valid field.", 510 | "isDeprecated": false, 511 | "deprecationReason": null 512 | }, 513 | { 514 | "name": "ENUM", 515 | "description": "Indicates this type is an enum. `enumValues` is a valid field.", 516 | "isDeprecated": false, 517 | "deprecationReason": null 518 | }, 519 | { 520 | "name": "INPUT_OBJECT", 521 | "description": "Indicates this type is an input object. `inputFields` is a valid field.", 522 | "isDeprecated": false, 523 | "deprecationReason": null 524 | }, 525 | { 526 | "name": "LIST", 527 | "description": "Indicates this type is a list. `ofType` is a valid field.", 528 | "isDeprecated": false, 529 | "deprecationReason": null 530 | }, 531 | { 532 | "name": "NON_NULL", 533 | "description": "Indicates this type is a non-null. `ofType` is a valid field.", 534 | "isDeprecated": false, 535 | "deprecationReason": null 536 | } 537 | ], 538 | "possibleTypes": null 539 | }, 540 | { 541 | "kind": "OBJECT", 542 | "name": "__Field", 543 | "description": null, 544 | "fields": [ 545 | { 546 | "name": "name", 547 | "description": null, 548 | "args": [], 549 | "type": { 550 | "kind": "NON_NULL", 551 | "name": null, 552 | "ofType": { 553 | "kind": "SCALAR", 554 | "name": "String", 555 | "ofType": null 556 | } 557 | }, 558 | "isDeprecated": false, 559 | "deprecationReason": null 560 | }, 561 | { 562 | "name": "description", 563 | "description": null, 564 | "args": [], 565 | "type": { 566 | "kind": "SCALAR", 567 | "name": "String", 568 | "ofType": null 569 | }, 570 | "isDeprecated": false, 571 | "deprecationReason": null 572 | }, 573 | { 574 | "name": "args", 575 | "description": null, 576 | "args": [], 577 | "type": { 578 | "kind": "NON_NULL", 579 | "name": null, 580 | "ofType": { 581 | "kind": "LIST", 582 | "name": null, 583 | "ofType": { 584 | "kind": "NON_NULL", 585 | "name": null, 586 | "ofType": { 587 | "kind": "OBJECT", 588 | "name": "__InputValue" 589 | } 590 | } 591 | } 592 | }, 593 | "isDeprecated": false, 594 | "deprecationReason": null 595 | }, 596 | { 597 | "name": "type", 598 | "description": null, 599 | "args": [], 600 | "type": { 601 | "kind": "NON_NULL", 602 | "name": null, 603 | "ofType": { 604 | "kind": "OBJECT", 605 | "name": "__Type", 606 | "ofType": null 607 | } 608 | }, 609 | "isDeprecated": false, 610 | "deprecationReason": null 611 | }, 612 | { 613 | "name": "isDeprecated", 614 | "description": null, 615 | "args": [], 616 | "type": { 617 | "kind": "NON_NULL", 618 | "name": null, 619 | "ofType": { 620 | "kind": "SCALAR", 621 | "name": "Boolean", 622 | "ofType": null 623 | } 624 | }, 625 | "isDeprecated": false, 626 | "deprecationReason": null 627 | }, 628 | { 629 | "name": "deprecationReason", 630 | "description": null, 631 | "args": [], 632 | "type": { 633 | "kind": "SCALAR", 634 | "name": "String", 635 | "ofType": null 636 | }, 637 | "isDeprecated": false, 638 | "deprecationReason": null 639 | } 640 | ], 641 | "inputFields": null, 642 | "interfaces": [], 643 | "enumValues": null, 644 | "possibleTypes": null 645 | }, 646 | { 647 | "kind": "OBJECT", 648 | "name": "__InputValue", 649 | "description": null, 650 | "fields": [ 651 | { 652 | "name": "name", 653 | "description": null, 654 | "args": [], 655 | "type": { 656 | "kind": "NON_NULL", 657 | "name": null, 658 | "ofType": { 659 | "kind": "SCALAR", 660 | "name": "String", 661 | "ofType": null 662 | } 663 | }, 664 | "isDeprecated": false, 665 | "deprecationReason": null 666 | }, 667 | { 668 | "name": "description", 669 | "description": null, 670 | "args": [], 671 | "type": { 672 | "kind": "SCALAR", 673 | "name": "String", 674 | "ofType": null 675 | }, 676 | "isDeprecated": false, 677 | "deprecationReason": null 678 | }, 679 | { 680 | "name": "type", 681 | "description": null, 682 | "args": [], 683 | "type": { 684 | "kind": "NON_NULL", 685 | "name": null, 686 | "ofType": { 687 | "kind": "OBJECT", 688 | "name": "__Type", 689 | "ofType": null 690 | } 691 | }, 692 | "isDeprecated": false, 693 | "deprecationReason": null 694 | }, 695 | { 696 | "name": "defaultValue", 697 | "description": null, 698 | "args": [], 699 | "type": { 700 | "kind": "SCALAR", 701 | "name": "String", 702 | "ofType": null 703 | }, 704 | "isDeprecated": false, 705 | "deprecationReason": null 706 | } 707 | ], 708 | "inputFields": null, 709 | "interfaces": [], 710 | "enumValues": null, 711 | "possibleTypes": null 712 | }, 713 | { 714 | "kind": "OBJECT", 715 | "name": "__EnumValue", 716 | "description": null, 717 | "fields": [ 718 | { 719 | "name": "name", 720 | "description": null, 721 | "args": [], 722 | "type": { 723 | "kind": "NON_NULL", 724 | "name": null, 725 | "ofType": { 726 | "kind": "SCALAR", 727 | "name": "String", 728 | "ofType": null 729 | } 730 | }, 731 | "isDeprecated": false, 732 | "deprecationReason": null 733 | }, 734 | { 735 | "name": "description", 736 | "description": null, 737 | "args": [], 738 | "type": { 739 | "kind": "SCALAR", 740 | "name": "String", 741 | "ofType": null 742 | }, 743 | "isDeprecated": false, 744 | "deprecationReason": null 745 | }, 746 | { 747 | "name": "isDeprecated", 748 | "description": null, 749 | "args": [], 750 | "type": { 751 | "kind": "NON_NULL", 752 | "name": null, 753 | "ofType": { 754 | "kind": "SCALAR", 755 | "name": "Boolean", 756 | "ofType": null 757 | } 758 | }, 759 | "isDeprecated": false, 760 | "deprecationReason": null 761 | }, 762 | { 763 | "name": "deprecationReason", 764 | "description": null, 765 | "args": [], 766 | "type": { 767 | "kind": "SCALAR", 768 | "name": "String", 769 | "ofType": null 770 | }, 771 | "isDeprecated": false, 772 | "deprecationReason": null 773 | } 774 | ], 775 | "inputFields": null, 776 | "interfaces": [], 777 | "enumValues": null, 778 | "possibleTypes": null 779 | }, 780 | { 781 | "kind": "OBJECT", 782 | "name": "__Directive", 783 | "description": null, 784 | "fields": [ 785 | { 786 | "name": "name", 787 | "description": null, 788 | "args": [], 789 | "type": { 790 | "kind": "NON_NULL", 791 | "name": null, 792 | "ofType": { 793 | "kind": "SCALAR", 794 | "name": "String", 795 | "ofType": null 796 | } 797 | }, 798 | "isDeprecated": false, 799 | "deprecationReason": null 800 | }, 801 | { 802 | "name": "description", 803 | "description": null, 804 | "args": [], 805 | "type": { 806 | "kind": "SCALAR", 807 | "name": "String", 808 | "ofType": null 809 | }, 810 | "isDeprecated": false, 811 | "deprecationReason": null 812 | }, 813 | { 814 | "name": "args", 815 | "description": null, 816 | "args": [], 817 | "type": { 818 | "kind": "NON_NULL", 819 | "name": null, 820 | "ofType": { 821 | "kind": "LIST", 822 | "name": null, 823 | "ofType": { 824 | "kind": "NON_NULL", 825 | "name": null, 826 | "ofType": { 827 | "kind": "OBJECT", 828 | "name": "__InputValue" 829 | } 830 | } 831 | } 832 | }, 833 | "isDeprecated": false, 834 | "deprecationReason": null 835 | }, 836 | { 837 | "name": "onOperation", 838 | "description": null, 839 | "args": [], 840 | "type": { 841 | "kind": "NON_NULL", 842 | "name": null, 843 | "ofType": { 844 | "kind": "SCALAR", 845 | "name": "Boolean", 846 | "ofType": null 847 | } 848 | }, 849 | "isDeprecated": false, 850 | "deprecationReason": null 851 | }, 852 | { 853 | "name": "onFragment", 854 | "description": null, 855 | "args": [], 856 | "type": { 857 | "kind": "NON_NULL", 858 | "name": null, 859 | "ofType": { 860 | "kind": "SCALAR", 861 | "name": "Boolean", 862 | "ofType": null 863 | } 864 | }, 865 | "isDeprecated": false, 866 | "deprecationReason": null 867 | }, 868 | { 869 | "name": "onField", 870 | "description": null, 871 | "args": [], 872 | "type": { 873 | "kind": "NON_NULL", 874 | "name": null, 875 | "ofType": { 876 | "kind": "SCALAR", 877 | "name": "Boolean", 878 | "ofType": null 879 | } 880 | }, 881 | "isDeprecated": false, 882 | "deprecationReason": null 883 | } 884 | ], 885 | "inputFields": null, 886 | "interfaces": [], 887 | "enumValues": null, 888 | "possibleTypes": null 889 | } 890 | ], 891 | "directives": [ 892 | { 893 | "name": "include", 894 | "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", 895 | "args": [ 896 | { 897 | "name": "if", 898 | "description": "Included when true.", 899 | "type": { 900 | "kind": "NON_NULL", 901 | "name": null, 902 | "ofType": { 903 | "kind": "SCALAR", 904 | "name": "Boolean", 905 | "ofType": null 906 | } 907 | }, 908 | "defaultValue": null 909 | } 910 | ], 911 | "onOperation": false, 912 | "onFragment": true, 913 | "onField": true 914 | }, 915 | { 916 | "name": "skip", 917 | "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", 918 | "args": [ 919 | { 920 | "name": "if", 921 | "description": "Skipped when true.", 922 | "type": { 923 | "kind": "NON_NULL", 924 | "name": null, 925 | "ofType": { 926 | "kind": "SCALAR", 927 | "name": "Boolean", 928 | "ofType": null 929 | } 930 | }, 931 | "defaultValue": null 932 | } 933 | ], 934 | "onOperation": false, 935 | "onFragment": true, 936 | "onField": true 937 | } 938 | ] 939 | } 940 | } 941 | } -------------------------------------------------------------------------------- /src/languageservice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Jim Kynde Meyer 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | const util = require('util'); 10 | const express = require('express'); 11 | const bodyParser = require("body-parser"); 12 | const relayTemplates = require('./relay-templates'); 13 | 14 | 15 | // ---- Start CodeMirror wrapper ---- 16 | 17 | const mock = new (require('mock-browser').mocks).MockBrowser; 18 | const document = mock.getDocument(); 19 | const navigator = mock.getNavigator(); 20 | 21 | global.window = global; 22 | global.document = document; 23 | global.navigator = navigator; 24 | 25 | document.createRange = function () { 26 | return { 27 | startNode: null, start: null, end: null, endNode: null, 28 | setStart: function (node, start) { this.startNode = node; this.start = start; }, 29 | setEnd: function (node, end) { this.endNode = node, this.end = end; }, 30 | getBoundingClientRect: function () { return null; }, 31 | getClientRects: function() { return []; } 32 | } 33 | }; 34 | 35 | // cm needs a parent node to replace the text area 36 | const cmContainer = document.createElement('div'); 37 | const cmTextArea = document.createElement('textarea'); 38 | cmContainer.appendChild(cmTextArea); 39 | cmTextArea.value = ''; 40 | 41 | // ---- End CodeMirror wrapper ---- 42 | 43 | 44 | // CodeMirror requires and initialization 45 | const CodeMirror = require('codemirror'); 46 | require('codemirror/addon/hint/show-hint'); 47 | require('codemirror/addon/lint/lint'); 48 | require('codemirror-graphql/hint'); 49 | require('codemirror-graphql/lint'); 50 | require('codemirror-graphql/mode'); 51 | 52 | const cm = CodeMirror.fromTextArea(cmTextArea, { mode: 'graphql'}); 53 | let cmCurrentDocValue = null; 54 | 55 | 56 | // GraphQL requires 57 | const graphqlLanguage = require('graphql/language'); 58 | const buildClientSchema = require('graphql/utilities/buildClientSchema').buildClientSchema; 59 | const GraphQLInterfaceType = require('graphql/type').GraphQLInterfaceType; 60 | const GraphQLUnionType = require('graphql/type').GraphQLUnionType; 61 | 62 | // prepare schema 63 | const printSchema = require('graphql/utilities/schemaPrinter').printSchema; 64 | const exampleSchemaJson = require('../schemas/builtin-schema.json'); 65 | const exampleSchema = buildClientSchema(exampleSchemaJson.data); 66 | 67 | let schema = exampleSchema; 68 | let schemaVersion = 0; 69 | let schemaUrl = ''; 70 | 71 | // project 72 | const project = require('./project'); 73 | project.onSchemaChanged({ 74 | onSchemaChanged: function(newSchema, url) { 75 | const schemaRoot = getSchemaRoot(newSchema); 76 | if(schemaRoot) { 77 | try { 78 | schema = buildClientSchema(schemaRoot); 79 | schemaVersion++; 80 | schemaUrl = url; 81 | let schemaJson = JSON.stringify(newSchema); 82 | if(schemaJson.length > 500) { 83 | schemaJson = schemaJson.substr(0, 500) + " ..."; 84 | } 85 | console.log("Loaded schema from '"+(url||'unknown')+"': " + schemaJson); 86 | } catch (e) { 87 | console.error("Error creating client schema", e); 88 | } 89 | } else { 90 | if(url) { 91 | console.error('Unable to load schema from "'+url+'". Expected {data:{__schema:...}} or {__schema:...} from a schema introspection query. Got root keys: ' + getRootKeys(newSchema)); 92 | } 93 | schema = exampleSchema; 94 | schemaVersion++; 95 | } 96 | } 97 | }); 98 | 99 | /** 100 | * Gets the JSON object that parents the __schema introspection result 101 | */ 102 | function getSchemaRoot(newSchema) { 103 | let root = null; 104 | if(newSchema) { 105 | if(newSchema.data && newSchema.data.__schema) { 106 | // compatible with babel-relay-plugin 107 | return newSchema.data; 108 | } 109 | if(newSchema.__schema) { 110 | // compatible with graphene 111 | return newSchema; 112 | } 113 | } 114 | return root; 115 | } 116 | 117 | function getRootKeys(newSchema) { 118 | if(newSchema) { 119 | return Object.keys(newSchema); 120 | } 121 | return '[]'; 122 | } 123 | 124 | // setup express endpoint for the language service and register the 'application/graphql' mime-type 125 | const app = express(); 126 | app.use(bodyParser.json({limit: '32mb'})); 127 | app.use(bodyParser.text({type: 'application/graphql' })); 128 | 129 | app.all('/js-graphql-language-service', function (req, res) { 130 | 131 | let raw = req.get('Content-Type') == 'application/graphql'; 132 | 133 | // prepare request data 134 | const command = req.body.command || req.query.command || 'getTokens'; 135 | let env = req.body.env || req.query.env; 136 | if(!env) { 137 | if(command == 'getTokens') { 138 | // the intellij plugin lexer has no environment info, 139 | // so ensure that getTokens supports '${var}', '...${fragment}', anonymous 'fragment on' etc. 140 | env = 'relay'; 141 | } else { 142 | // fallback 143 | env = 'graphql'; 144 | } 145 | } 146 | let requestData = { 147 | command: command, 148 | env: env, 149 | useRelayTemplates: env == 'relay' || env == 'apollo' || env == 'lokka' || env == 'graphql-template', 150 | projectDir: req.body.projectDir || req.query.projectDir, 151 | buffer: (raw ? req.body : req.body.buffer || req.query.buffer) || '', 152 | line: parseInt(req.body.line || req.query.line || '0', 10), 153 | ch: parseInt(req.body.ch || req.query.ch || '0', 10), 154 | }; 155 | 156 | 157 | // ---- Documentation and Project Commands ---- 158 | 159 | if(requestData.command == 'getTypeDocumentation') { 160 | let typeName = req.body.type || req.query.type; 161 | let typeDoc = getTypeDocumentation(typeName) 162 | res.header('Content-Type', 'application/json'); 163 | res.send(JSON.stringify(typeDoc)); 164 | return; 165 | } else if(requestData.command == 'getFieldDocumentation') { 166 | let typeName = req.body.type || req.query.type; 167 | let fieldName = req.body.field || req.query.field; 168 | let fieldDoc = getFieldDocumentation(typeName, fieldName); 169 | res.header('Content-Type', 'application/json'); 170 | res.send(JSON.stringify(fieldDoc)); 171 | return; 172 | } else if(requestData.command == 'setProjectDir') { 173 | project.setProjectDir(requestData.projectDir); 174 | res.header('Content-Type', 'application/json'); 175 | res.send({projectDir:requestData.projectDir}); 176 | return; 177 | } else if(requestData.command == 'getSchema') { 178 | res.header('Content-Type', 'text/plain'); 179 | res.send(printSchema(schema)); 180 | return; 181 | } else if(requestData.command == 'getSchemaWithVersion') { 182 | res.header('Content-Type', 'application/json'); 183 | res.send(JSON.stringify({ 184 | schema: printSchema(schema), 185 | queryType: (schema.getQueryType() || '').toString(), 186 | mutationType: (schema.getMutationType() || '').toString(), 187 | subscriptionType: (schema.getSubscriptionType() || '').toString(), 188 | url: schemaUrl, 189 | version: schemaVersion 190 | })); 191 | return; 192 | } 193 | 194 | 195 | // ---- CodeMirror Commands ---- 196 | 197 | res.header('Content-Type', 'application/json'); 198 | 199 | // update CodeMirror's text buffer 200 | let textToParse = requestData.buffer || ''; 201 | 202 | 203 | // -- Relay templates -- 204 | 205 | let relayContext = null; 206 | if(requestData.useRelayTemplates) { 207 | relayContext = relayTemplates.createRelayContext(textToParse, env); 208 | textToParse = relayTemplates.transformBufferAndRequestData(requestData, relayContext); 209 | } 210 | 211 | 212 | // -- Perform the requested command -- 213 | 214 | if(cmCurrentDocValue != textToParse) { 215 | // only tell CM to re-parse if the doc has changed 216 | // TODO: only update changed areas of the text for performance 217 | cm.doc.setValue(textToParse); 218 | cmCurrentDocValue = textToParse; 219 | } 220 | 221 | let responseData = {}; 222 | if(requestData.command == 'getTokens') { 223 | responseData = getTokens(cm, textToParse); 224 | } else if(requestData.command == 'getHints') { 225 | responseData = getHints(cm, requestData.line, requestData.ch); 226 | } else if(requestData.command == 'getTokenDocumentation') { 227 | responseData = getTokenDocumentation(cm, requestData.line, requestData.ch); 228 | } else if(requestData.command == 'getAnnotations') { 229 | responseData = getAnnotations(cm, textToParse); 230 | } else if(requestData.command == 'getAST') { 231 | responseData = getAST(textToParse); 232 | } else { 233 | responseData.error = 'Unknown command "'+requestData.command+'"'; 234 | } 235 | 236 | if(requestData.useRelayTemplates && relayContext) { 237 | relayTemplates.transformResponseData(responseData, requestData.command, relayContext); 238 | } 239 | 240 | 241 | // -- send the response -- 242 | 243 | res.send(JSON.stringify(responseData)); 244 | }); 245 | 246 | 247 | // ---- 'getTokens' command ---- 248 | 249 | const lineSeparator = cm.doc.lineSeparator(); 250 | const lineSeparatorLength = lineSeparator.length; 251 | 252 | function getTokens(cm, textToParse) { 253 | let tokens = []; 254 | let lineNum = 0; 255 | let lineCount = cm.lineCount(); 256 | let lineTokens = cm.getLineTokens(lineNum, true); 257 | let lineStartPos = 0; 258 | 259 | while (lineNum < lineCount) { 260 | for (let i = 0; i < lineTokens.length; i++) { 261 | let token = lineTokens[i]; 262 | let state = token.state; 263 | let tokenRet = { 264 | text: token.string, 265 | type: token.type || 'ws', 266 | start: lineStartPos + token.start, 267 | end: lineStartPos + token.end, 268 | scope: (state.levels||[]).length, 269 | kind: state.kind 270 | }; 271 | 272 | if(tokenRet.type == 'ws' && tokenRet.text.trim() == ',') { 273 | // preserve the commas 274 | tokenRet.type = 'punctuation'; 275 | } else if(tokenRet.type == 'atom') { 276 | // for schema language definitions we want the type name tokens to be a 'def' 277 | let isSchemaTypeDef = false; 278 | for(let t = tokens.length - 1; t >= 0; t--) { 279 | let prevToken = tokens[t]; 280 | if(prevToken.type == 'keyword') { 281 | switch (prevToken.text) { 282 | case 'type': 283 | case 'interface': 284 | case 'enum': 285 | case 'input': 286 | case 'union': 287 | case 'scalar': 288 | isSchemaTypeDef = true; 289 | break; 290 | } 291 | break; 292 | } if(prevToken.type != 'ws' && prevToken.type != 'punctuation') { 293 | break; 294 | } 295 | } 296 | if(isSchemaTypeDef) { 297 | tokenRet.type = 'def'; 298 | } 299 | } 300 | 301 | // codemirror-graphql 0.5.7 swapped qualifier and property in https://github.com/graphql/codemirror-graphql/commit/8d6ce868146df28fc832346fbbc72b78246de52f 302 | // but we want the actual properties to point to their declaration to enable "Go to declaration", "find usages" etc. 303 | // so swap then back into "qualifier" ":" "property" 304 | if(tokenRet.type == 'property' && tokenRet.kind =='AliasedField') { 305 | tokenRet.type = 'qualifier'; 306 | } else if(tokenRet.type == 'qualifier' && tokenRet.kind =='AliasedField') { 307 | tokenRet.type = 'property'; 308 | tokenRet.kind = 'Field'; 309 | } 310 | 311 | if(tokenRet.type == 'string') { 312 | // we need separate tokens for the string contents and the surrounding quotes to auto-close quotes in intellij 313 | let text = tokenRet.text; 314 | let openQuote = Object.assign({}, tokenRet, {type: 'open_quote', text:'"', end: tokenRet.start + 1}); 315 | let closeQuote = Object.assign({}, tokenRet, {type:'close_quote', text:'"', start: tokenRet.end - 1}); 316 | tokens.push(openQuote); 317 | if(text.charAt(0)=='"') { 318 | tokenRet.start++; 319 | } 320 | let hasCloseQuote = (text.length > 1 && text.charAt(text.length-1)=='"'); 321 | if(hasCloseQuote) { 322 | tokenRet.end--; 323 | } 324 | if(tokenRet.start != tokenRet.end) { 325 | tokenRet.text = text.substring(1, text.length - (hasCloseQuote ? 1 : 0)); 326 | tokens.push(tokenRet); 327 | } 328 | if(hasCloseQuote) { 329 | tokens.push(closeQuote); 330 | } 331 | } else { 332 | tokens.push(tokenRet); 333 | } 334 | 335 | } 336 | lineStartPos += cm.getLine(lineNum).length + lineSeparatorLength; 337 | lineNum++; 338 | if(lineNum < lineCount) { 339 | // insert a line break token 340 | tokens.push({ 341 | text: lineSeparator, 342 | type: 'ws', 343 | start: lineStartPos - lineSeparatorLength, 344 | end: lineStartPos 345 | }); 346 | } 347 | lineTokens = cm.getLineTokens(lineNum, true); 348 | } 349 | if(tokens.length == 0 && textToParse) { 350 | // didn't get any tokens from the graphql codemirror mode 351 | return null; 352 | } 353 | return {tokens: tokens}; 354 | } 355 | 356 | 357 | // ---- 'getHints' command ---- 358 | 359 | const isRelayType = function(type) { 360 | if(type) { 361 | let typeName = type.toString(); 362 | if(typeName == 'Node' || typeName == 'PageInfo!' || typeName.indexOf('Edge]')!=-1 || typeName.indexOf('Connection')!=-1) { 363 | return true; 364 | } 365 | let interfaces = type.getInterfaces ? type.getInterfaces() : null; 366 | if(interfaces) { 367 | for(let i = 0; i < interfaces.length; i++) { 368 | let interfaceName = interfaces[i].toString(); 369 | if(interfaceName == 'Node') { 370 | return true; 371 | } 372 | } 373 | } 374 | } 375 | return false; 376 | }; 377 | 378 | const hintHelper = cm.getHelper({line: 0, ch: 0}, 'hint'); 379 | function getHints(cm, line, ch, fullType, tokenName) { 380 | if(hintHelper) { 381 | 382 | // move cursor to location of the hint 383 | cm.setCursor(line, ch, {scroll:false}); 384 | 385 | let hints = hintHelper(cm, { schema: schema }); 386 | if(hints && hints.list) { 387 | if(tokenName) { 388 | let list = hints.list; 389 | hints.list = []; 390 | for(let i = 0; i < list.length; i++) { 391 | if(list[i].text == tokenName) { 392 | hints.list.push(list[i]); 393 | break; 394 | } 395 | } 396 | } 397 | // remove the type property since it can contain circular GraphQL type references 398 | hints.list.forEach(function(hint) { 399 | if(hint.type) { 400 | if(fullType) { 401 | hint.fullType = hint.type; 402 | } 403 | hint.description = hint.description || hint.type.description; 404 | hint.relay = isRelayType(hint.type); 405 | hint.type = hint.type.toString(); 406 | } 407 | }); 408 | // add the from/to of the text span to remove before inserting the completion 409 | return { 410 | hints: hints.list, 411 | from: hints.from, 412 | to: hints.to 413 | } 414 | } 415 | // empty result 416 | return { hints: [], from: null, to: null }; 417 | } 418 | return {} 419 | } 420 | 421 | 422 | // ---- 'getTypeDocumentation' command ---- 423 | 424 | function _getSchemaType(typeName) { 425 | let type = schema.getTypeMap()[typeName]; 426 | if(!type) { 427 | if(typeName == 'Query') { 428 | type = schema.getQueryType(); 429 | } else if(typeName == 'Mutation') { 430 | type = schema.getMutationType(); 431 | } else if(typeName == 'Subscription') { 432 | type = schema.getSubscriptionType(); 433 | } 434 | } 435 | return type; 436 | } 437 | 438 | function getTypeDocumentation(typeName) { 439 | let ret = {}; 440 | let type = _getSchemaType(typeName); 441 | if(type) { 442 | let interfaces = type.getInterfaces ? type.getInterfaces() : []; 443 | let fields = type.getFields ? type.getFields() : {}; 444 | let fieldsList = []; 445 | for (let f in fields) { 446 | fieldsList.push(fields[f]); 447 | } 448 | let implementations = null; 449 | if(type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) { 450 | implementations = schema.getPossibleTypes(type) || []; 451 | } else { 452 | implementations = []; 453 | } 454 | 455 | ret = { 456 | type: type.toString(), 457 | description: type.description, 458 | interfaces : interfaces.map(function(intf) { 459 | return intf.toString(); 460 | }), 461 | implementations: implementations.map(function(impl) { 462 | return impl.toString(); 463 | }), 464 | fields: fieldsList.map(function(field) { 465 | return { 466 | name: field.name, 467 | args: (field.args || []).map(function(arg) { 468 | return { 469 | name: arg.name, 470 | type: arg.type.toString(), 471 | description: arg.description 472 | } 473 | }), 474 | type: field.type.toString(), 475 | description: field.description 476 | }; 477 | }) 478 | } 479 | } 480 | return ret; 481 | } 482 | 483 | 484 | // ---- 'getFieldDocumentation' command ---- 485 | 486 | function getFieldDocumentation(typeName, fieldName) { 487 | let doc = {}; 488 | let type = _getSchemaType(typeName); 489 | if(type) { 490 | let fields = type.getFields ? type.getFields() : {}; 491 | let field = fields[fieldName]; 492 | if(field) { 493 | doc.type = field.type.toString(); 494 | doc.description = field.description; 495 | } 496 | } 497 | return doc; 498 | } 499 | 500 | 501 | // ---- 'getTokenDocumentation' command ---- 502 | 503 | function getTokenDocumentation(cm, line, ch) { 504 | let doc = {}; 505 | let token = cm.getTokenAt({line:line, ch: ch + 1/*if we don't +1 cm will return the token that ends at ch */}, true); 506 | if(token && token.state) { 507 | let tokenText = token.state.name; 508 | let hintsRes = getHints(cm, line, ch, true, tokenText); 509 | if(hintsRes && hintsRes.hints) { 510 | let matchingHint = null; 511 | hintsRes.hints.forEach(function(hint) { 512 | if(hint.text == tokenText) { 513 | matchingHint = hint; 514 | } 515 | }); 516 | if(matchingHint) { 517 | let type = matchingHint.fullType; 518 | if(type) { 519 | doc.type = type.toString(); 520 | doc.description = matchingHint.description; 521 | } 522 | 523 | } 524 | } 525 | } 526 | 527 | return doc; 528 | } 529 | 530 | 531 | // ---- 'getAnnotations' command ---- 532 | 533 | const lintHelper = cm.getHelper({line: 0, ch: 0}, 'lint'); 534 | function getAnnotations(cm, text) { 535 | if(lintHelper) { 536 | let annotations = lintHelper(text, { schema: schema }, cm); 537 | if(annotations) { 538 | let ranges = {}; 539 | let uniqueAnnotations = []; 540 | annotations.forEach(ann => { 541 | try { 542 | let range = ann.from.line + ":" + ann.from.ch + "-" + ann.to.line + ":" + ann.to.ch 543 | if(!ranges[range]) { 544 | uniqueAnnotations.push(ann); 545 | ranges[range] = ann; 546 | } 547 | } catch (e) { 548 | console.error('Unable to determine range for annotation', ann, e); 549 | } 550 | }); 551 | return { 552 | annotations: uniqueAnnotations 553 | } 554 | } 555 | } 556 | return {} 557 | } 558 | 559 | 560 | // ---- 'getAST' command ---- 561 | 562 | function getAST(text) { 563 | try { 564 | let ast = graphqlLanguage.parse(text, {noSource: true}); 565 | return ast; 566 | } catch (e) { 567 | return {error: e, locations: e.locations, nodes: e.nodes}; 568 | } 569 | } 570 | 571 | 572 | module.exports = app; -------------------------------------------------------------------------------- /src/project.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Jim Kynde Meyer 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | const path = require('path'); 10 | const fs = require('fs'); 11 | const filewatcher = require('filewatcher'); 12 | const request = require('then-request'); 13 | const configFileName = 'graphql.config.json'; 14 | const introspectionQuery = require('graphql/utilities/introspectionQuery').introspectionQuery; 15 | const graphql = require('graphql'); 16 | const project = { 17 | 18 | projectDir: null, 19 | projectFile : null, 20 | schemaFile: null, 21 | schemaUrl: null, 22 | watcher : null, 23 | schemaChangedCallbacks : [], 24 | 25 | setProjectDir : function(projectDir) { 26 | 27 | if(this.projectDir === projectDir) { 28 | return; 29 | } 30 | 31 | console.log("Setting Project Dir '"+projectDir+"'"); 32 | 33 | this.projectDir = projectDir; 34 | this.projectFile = path.join(projectDir, configFileName); 35 | this.schemaFile = null; 36 | this.schemaUrl = null; 37 | 38 | if(this.watcher) { 39 | this.watcher.removeAll(); 40 | } 41 | 42 | // create watcher and register change handler 43 | this.watcher = filewatcher({ 44 | forcePolling: false, 45 | debounce: 500, 46 | interval: 1000, 47 | persistent: false 48 | }); 49 | 50 | this._watch(this.projectDir); 51 | if(this._fileExists(this.projectFile)) { 52 | this._watch(this.projectFile, true); 53 | } 54 | 55 | this.watcher.on('change', function(file, stat) { 56 | if(file == this.projectDir) { 57 | if(this.schemaFile == null && this.schemaUrl == null) { 58 | // something changed in the project dir, and we don't have a schema, so see if we can load one 59 | this._loadSchema(); 60 | if(this._fileExists(this.projectFile)) { 61 | this._watch(this.projectFile, true); 62 | } 63 | } 64 | } else if(file == this.projectFile || file == this.schemaFile) { 65 | if (stat && !stat.deleted) { 66 | // file created or modified 67 | this._loadSchema(); 68 | } else { 69 | // file deleted 70 | this._sendSchemaChanged(null); 71 | } 72 | } 73 | }.bind(this)); 74 | 75 | this._loadSchema(); 76 | 77 | }, 78 | 79 | _watch : function(file, log) { 80 | log = !this.watcher.watchers[file] && log; 81 | this.watcher.add(file); 82 | if(log) { 83 | console.log("Watching '" + file + "' for changes."); 84 | } 85 | }, 86 | 87 | onSchemaChanged : function(callback) { 88 | this.schemaChangedCallbacks.push(callback); 89 | }, 90 | 91 | _loadGraphQL: function(schemaFile, onSchemaLoaded) { 92 | try { 93 | const schemaString = fs.readFileSync(schemaFile, 'utf8').trim(); 94 | if(schemaString) { 95 | const schema = graphql.buildSchema(schemaString); 96 | graphql.graphql(schema, introspectionQuery).then(result => { 97 | onSchemaLoaded(result); 98 | }).catch(function(e) { 99 | console.error('Unable to get schema.json from introspection query', e); 100 | }); 101 | } 102 | } catch(e) { 103 | console.error('Unable to load GraphQL schema from "'+schemaFile+'":', e.message); 104 | } 105 | }, 106 | 107 | _loadJSON : function(fileName) { 108 | try { 109 | var jsonString = fs.readFileSync(fileName, 'utf8').trim(); 110 | if(jsonString) { 111 | return JSON.parse(jsonString); 112 | } 113 | } catch(e) { 114 | console.error('Unable to load JSON from "'+fileName+"'", e); 115 | } 116 | return {}; 117 | }, 118 | 119 | _fileExists : function (file) { 120 | try { 121 | fs.accessSync(this.projectFile, fs.R_OK); 122 | return true; 123 | } catch (ignored) { 124 | // no file to read 125 | } 126 | return false; 127 | }, 128 | 129 | _loadSchema : function() { 130 | try { 131 | if(!this._fileExists(this.projectFile)) { 132 | return; 133 | } 134 | let config = this._loadJSON(this.projectFile); 135 | if(config && config.schema) { 136 | let schemaConfig = config.schema; 137 | if(schemaConfig.file) { 138 | try { 139 | const schemaFile = path.isAbsolute(schemaConfig.file) ? schemaConfig.file : path.join(this.projectDir, schemaConfig.file); 140 | const schemaExt = path.extname(schemaFile); 141 | const isGraphQL = (schemaExt === '.graphql' || schemaExt === '.graphqls'); 142 | const onSchemaLoaded = function(schema) { 143 | if(schema) { 144 | this.schemaFile = schemaFile; 145 | this._watch(schemaFile, true); 146 | this._sendSchemaChanged(schema, schemaFile); 147 | } 148 | }.bind(this); 149 | if(isGraphQL) { 150 | this._loadGraphQL(schemaFile, onSchemaLoaded); 151 | } else { 152 | onSchemaLoaded(this._loadJSON(schemaFile)); 153 | } 154 | return; 155 | } catch(e) { 156 | console.error("Couldn't load schema", e); 157 | } 158 | 159 | } else if(schemaConfig.request && schemaConfig.request.url) { 160 | 161 | try { 162 | // need to do a network request to fetch the schema 163 | let schemaRequestConfig = schemaConfig.request; 164 | let doIntrospectionQuery = schemaRequestConfig.postIntrospectionQuery; 165 | let method = doIntrospectionQuery ? 'POST' : schemaRequestConfig.method || 'GET'; 166 | if(doIntrospectionQuery) { 167 | schemaRequestConfig.options = schemaRequestConfig.options || {}; 168 | schemaRequestConfig.options.headers = schemaRequestConfig.options.headers || {}; 169 | schemaRequestConfig.options.headers['Content-Type'] = 'application/json'; 170 | schemaRequestConfig.options.body = JSON.stringify({query: introspectionQuery}); 171 | } 172 | request(method, schemaRequestConfig.url, schemaRequestConfig.options).then((schemaResponse) => { 173 | if (schemaResponse.statusCode == 200) { 174 | let schemaBody = schemaResponse.getBody('utf-8'); 175 | let schema = JSON.parse(schemaBody); 176 | if(schema) { 177 | this.schemaUrl = schemaRequestConfig.url; 178 | this._sendSchemaChanged(schema, schemaRequestConfig.url); 179 | } 180 | } else { 181 | console.error("Error loading schema from '"+schemaRequestConfig.url+"'", schemaResponse, schemaConfig.request); 182 | this._sendSchemaChanged(null); 183 | } 184 | }).catch((error) => { 185 | console.error("Error loading schema from '"+schemaRequestConfig.url+"'", error, schemaConfig.request); 186 | }); 187 | } catch (e) { 188 | console.error("Couldn't load schema using request config", e, schemaConfig.request); 189 | } 190 | return; 191 | } 192 | } 193 | } catch (e) { 194 | console.error("Error loading schema from '" + this.projectFile + "'", e); 195 | } 196 | // fallback is no schema 197 | this._sendSchemaChanged(null); 198 | }, 199 | 200 | _sendSchemaChanged : function(newSchema, url) { 201 | try { 202 | this.schemaChangedCallbacks.forEach(cb => cb.onSchemaChanged(newSchema, url)); 203 | } catch (e) { 204 | console.error('Error signalling schema change', e); 205 | } 206 | } 207 | 208 | 209 | }; 210 | 211 | module.exports = project; 212 | -------------------------------------------------------------------------------- /src/relay-templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Jim Kynde Meyer 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use strict'; 9 | const HashMap = require('hashmap'); 10 | 11 | const comment = '#'.charCodeAt(0); 12 | const ws = ' '.charCodeAt(0); 13 | const dot = '.'.charCodeAt(0); 14 | const comma = ','.charCodeAt(0); 15 | const newLine = '\n'.charCodeAt(0); 16 | const returnLine = '\r'.charCodeAt(0); 17 | const tab = '\t'.charCodeAt(0); 18 | const templateMark = '$'.charCodeAt(0); 19 | const templateLBrace = '{'.charCodeAt(0); 20 | const templateRBrace = '}'.charCodeAt(0); 21 | const leftParen = '('.charCodeAt(0); 22 | const rightParen = ')'.charCodeAt(0); 23 | const underscore = '_'.charCodeAt(0); 24 | 25 | const typeName = '__typename'; 26 | const typeNameLength = typeName.length; 27 | const lokkaFragmentPlaceholderField = '__'; 28 | const lokkaFragmentPlaceholderFieldLength = lokkaFragmentPlaceholderField.length; 29 | 30 | const relayTemplatePlaceHolder = '____'; 31 | const fragmentNamePlaceHolder = '____'; 32 | const fragmentNamePlaceHolderReplaceLength = fragmentNamePlaceHolder.length + 1; // +1 for ws after during replacement 33 | 34 | const anonOperationNotAloneMessage = require('graphql/validation/rules/LoneAnonymousOperation').anonOperationNotAloneMessage(); 35 | const unusedFragMessage = require('graphql/validation/rules/NoUnusedFragments').unusedFragMessage(fragmentNamePlaceHolder); 36 | const uniqueFragmentNames = require('graphql/validation/rules/UniqueFragmentNames').duplicateFragmentNameMessage(fragmentNamePlaceHolder); 37 | const scalarLeafs = require('graphql/validation/rules/ScalarLeafs').requiredSubselectionMessage(relayTemplatePlaceHolder, relayTemplatePlaceHolder).replace(' { ... }', ''); 38 | const noUndefinedVariables = require('graphql/validation/rules/NoUndefinedVariables').undefinedVarMessage(relayTemplatePlaceHolder); 39 | 40 | const relayValidationFilters = [ 41 | (msg) => msg != anonOperationNotAloneMessage, 42 | (msg) => msg != uniqueFragmentNames, 43 | (msg) => msg.replace(/"[^"]+"/, '"'+fragmentNamePlaceHolder+'"') != unusedFragMessage, 44 | (msg) => msg.replace(/"[^"]+"/g, '"'+relayTemplatePlaceHolder+'"') != scalarLeafs, 45 | (msg) => msg.replace(/"[^"]+"/g, '"$'+relayTemplatePlaceHolder+'"') != noUndefinedVariables, 46 | (msg) => msg != 'Unknown directive "relay".', 47 | (msg) => msg.indexOf('"'+fragmentNamePlaceHolder+'"') == -1 // never show the placeholder errors (which should be caused by some other error) 48 | ]; 49 | 50 | const apolloValidationFilters = [ 51 | (msg) => msg.indexOf('Unknown fragment') == -1 52 | ]; 53 | 54 | const lokkaValidationFilters = [ 55 | (msg) => msg.indexOf('Cannot query field "' + lokkaFragmentPlaceholderField + '"') == -1 // start of graphql/validation/rules/FieldsOnCorrectType 56 | ]; 57 | 58 | const graphqlTemplateFilters = [ 59 | (msg) => msg.indexOf('Variable ') === -1 && msg.indexOf('$__') === -1 60 | ]; 61 | 62 | module.exports = { 63 | 64 | /** 65 | * Creates a relay context that tracks any transformation made to the incoming buffer and request data 66 | */ 67 | createRelayContext : function(textToParse, env) { 68 | return { 69 | textToParse: textToParse, 70 | shifts: [], 71 | templateFromPosition : new HashMap(), 72 | wsEndReplacements: {}, 73 | env: env 74 | }; 75 | }, 76 | 77 | /** Relay uses a shorthand "fragment on Foo" which doesn't follow the GraphQL spec, so we need to name the fragment to follow the grammar. 78 | * This means that once we insert additional text into the buffer, we need to 'unshift' tokens/annotations later in transformResponseData. 79 | */ 80 | transformBufferAndRequestData : function(requestData, relayContext) { 81 | 82 | // first take care of the ${...} templates expressions 83 | this._transformTemplates(relayContext); 84 | 85 | // then apply any shifts, i.e. token insertions, to conform with the GraphQL grammar, e.g. 'fragment on Foo' -> 'fragment on Foo' 86 | let graphQL = this._transformToGraphQL(relayContext); 87 | 88 | // finally transform the request data to align with the transformations that were applied to the original buffer 89 | this._transformRequestData(requestData, relayContext); 90 | 91 | return graphQL; 92 | }, 93 | 94 | /** 95 | * Transforms the buffer into valid GraphQL, e.g. 'fragment on Foo' -> 'fragment on Foo' 96 | */ 97 | _transformToGraphQL : function(relayContext) { 98 | let baseOffset = 0; 99 | let transformedBuffer = relayContext.templatedTextToParse.replace(/(fragment[\s]+)(on)/g, (match, p1, p2, offset) => { 100 | // make sure we don't transform after '#' comments 101 | for(let i = offset - 1; i >= 0; i--) { 102 | const char = relayContext.templatedTextToParse.charCodeAt(i); 103 | if(char == newLine) { 104 | // new line so no comment on this one 105 | break; 106 | } else if(char == comment) { 107 | // we're after a comment, so leave the match as is 108 | return p1 + p2; 109 | } 110 | } 111 | relayContext.shifts.push({pos: baseOffset + offset + p1.length, length: fragmentNamePlaceHolderReplaceLength}); 112 | baseOffset += fragmentNamePlaceHolderReplaceLength; 113 | return p1 + fragmentNamePlaceHolder + ' ' + p2; 114 | }); 115 | relayContext.relayGraphQL = transformedBuffer; 116 | return transformedBuffer; 117 | }, 118 | 119 | /** 120 | * Inline replace JS template expressions (e.g. ${Component.getFragment('viewer')}) with a '#{...}' comment or placeholder '__typename' 121 | * The transformed text is assigned to 'templatedTextToParse' on the context 122 | */ 123 | _transformTemplates : function(relayContext) { 124 | 125 | let templateFromPosition = relayContext.templateFromPosition; 126 | let templateBuffer = new Buffer(relayContext.textToParse); 127 | 128 | let line = 0; 129 | let column = 0; 130 | let braceScope = 0; 131 | let fieldArgumentScope = 0; 132 | let insideComment = false; 133 | for (let i = 0; i < templateBuffer.length; i++) { 134 | let c = templateBuffer[i]; 135 | switch (c) { 136 | case newLine: 137 | line++; 138 | column = 0; 139 | insideComment = false; 140 | break; 141 | case templateLBrace: 142 | if(!insideComment) { 143 | braceScope++; 144 | } 145 | break; 146 | case templateRBrace: 147 | if(!insideComment) { 148 | braceScope--; 149 | } 150 | break; 151 | case leftParen: 152 | if(!insideComment) { 153 | fieldArgumentScope++; 154 | } 155 | break; 156 | case rightParen: 157 | if(!insideComment) { 158 | fieldArgumentScope--; 159 | } 160 | break; 161 | case comment: 162 | insideComment = true; 163 | break; 164 | default: 165 | column++; 166 | } 167 | if (c == templateMark) { 168 | let next = templateBuffer[Math.min(i + 1, templateBuffer.length - 1)]; 169 | if (next == templateLBrace) { 170 | // found the start of a template expression, so replace it as '${...}' -> '' to make sure 171 | // we have an 'always' valid SelectionSet when the template is the only selected field, e.g. in Relay injections and Lokka 172 | let templatePos = i; 173 | let isLokkaFragment = false; 174 | if(relayContext.env == 'lokka') { 175 | // for compatibility with Lokka fragments, transform '...$' to ' $' 176 | let lokkaDotReplacements = []; 177 | for (let lok = i - 1; lok >= 0; lok--) { 178 | if (templateBuffer[lok] == dot) { 179 | lokkaDotReplacements.push(lok); 180 | } else { 181 | break; 182 | } 183 | if (lok == i - 3) { 184 | // three '.'s in a row, replace them with ws 185 | lokkaDotReplacements.forEach(pos => templateBuffer[pos] = ws); 186 | relayContext.wsEndReplacements[i] = { 187 | text: '...', 188 | type: 'keyword' 189 | }; 190 | isLokkaFragment = true; 191 | break; 192 | } 193 | } 194 | } 195 | 196 | let openBraces = 0; 197 | for (let t = i + 1; t < templateBuffer.length; t++) { 198 | const isOpenBrace = (templateBuffer[t] == templateLBrace); 199 | if(isOpenBrace) { 200 | openBraces++; 201 | continue; 202 | } 203 | const isClosingBrace = (templateBuffer[t] == templateRBrace); 204 | if(isClosingBrace) { 205 | openBraces--; 206 | if(openBraces > 0) { 207 | continue; 208 | } 209 | } 210 | const isNewLine = (templateBuffer[t] == newLine); 211 | if (isNewLine || isClosingBrace) { 212 | // we're at the closing brace or new line 213 | i = t; 214 | if(isNewLine) { 215 | i--; //backtrack to not include the new-line into the template 216 | } 217 | if(this._insertPlaceholderFieldWithPaddingOrComment(relayContext, braceScope, fieldArgumentScope, templateBuffer, templatePos, i, isLokkaFragment)) { 218 | // store the original token text for later application in getTokens 219 | templateFromPosition.set(templatePos, relayContext.textToParse.substring(templatePos, i + 1)); 220 | } 221 | break; 222 | } 223 | } 224 | } 225 | } 226 | } 227 | 228 | relayContext.templatedTextToParse = templateBuffer.toString(); 229 | 230 | }, 231 | 232 | /** 233 | * Makes sure we have at least one field selected in a SelectionSet, e.g. 'foo { ${Component.getFragment} }'. 234 | * If we can't fit the field inside the template expression, we change it to a temporary comment, ie. '${...}' -> '#{...}' 235 | * @return false if no replacement was possible, true otherwise 236 | */ 237 | _insertPlaceholderFieldWithPaddingOrComment : function(relayContext, braceScope, fieldArgumentScope, buffer, startPos, endPos, isLokkaFragment) { 238 | const fieldLength = isLokkaFragment ? lokkaFragmentPlaceholderFieldLength : typeNameLength; 239 | if(relayContext.env == 'apollo' && braceScope === 0) { 240 | // treat top level apollo fragments as whitespace 241 | for(let i = startPos; i <= endPos; i++) { 242 | buffer[i] = ws; 243 | } 244 | return true; 245 | } 246 | if(fieldArgumentScope === 1 && relayContext.env == 'graphql-template') { 247 | // template field argument, so insert a placeholder variable, e.g. todos(first: ${v => v.count}) becomes todos(first: $______________) 248 | let char = 0; 249 | for(let i = startPos; i <= endPos; i++) { 250 | if(char === 0) { 251 | buffer[i] = templateMark; 252 | } else { 253 | buffer[i] = underscore; 254 | } 255 | char++; 256 | } 257 | return true; 258 | } 259 | const allowComment = () => { 260 | // the line comment is only a solution if it doesn't remove other tokens on that line, so check if that's the case 261 | if(endPos == buffer.length) { 262 | return true; 263 | } 264 | // look for a return or newline without encountering tokens that mustn't be commented out 265 | for(let i = endPos + 1; i < buffer.length; i++) { 266 | switch (buffer[i]) { 267 | case comment: 268 | case ws: 269 | case tab: 270 | case comma: 271 | continue; 272 | case newLine: 273 | case returnLine: 274 | return true; 275 | default: 276 | return false; 277 | } 278 | } 279 | return false; 280 | }; 281 | if(endPos - startPos < fieldLength) { 282 | // can't fit the field inside the template expression so use a comment for now (expecting the user to keep typing) 283 | if(allowComment()) { 284 | buffer[startPos] = comment; 285 | return true; 286 | } else { 287 | return false; 288 | } 289 | } 290 | if(startPos + fieldLength >= buffer.length) { 291 | // cant fit the field inside the remaining buffer 292 | if(allowComment()) { 293 | buffer[startPos] = comment; 294 | return true; 295 | } else { 296 | return false; 297 | } 298 | } 299 | let t = 0; 300 | const fieldName = isLokkaFragment ? lokkaFragmentPlaceholderField : typeName; 301 | for(let i = startPos; i < buffer.length; i++) { 302 | buffer[i] = fieldName.charCodeAt(t++); 303 | if(t == fieldLength) { 304 | // at end of typeName, so fill with ws to not upset token positions 305 | let w = i + 1; 306 | while(w < buffer.length && w <= endPos) { 307 | buffer[w] = ws; 308 | w++; 309 | } 310 | break; 311 | } 312 | } 313 | return true; 314 | }, 315 | 316 | /** 317 | * Transforms any positions as part of the request data based on the shifts made to the buffer 318 | */ 319 | _transformRequestData : function(requestData, relayContext) { 320 | if(requestData.command == 'getHints' || requestData.command == 'getTokenDocumentation') { 321 | if(relayContext.shifts.length > 0) { 322 | let shiftsByLine = this._getShiftsByLines(relayContext, requestData.line); 323 | let shifts = shiftsByLine.get(requestData.line); 324 | if(shifts) { 325 | shifts.forEach((shift) => { 326 | // move the line cursor to the right to place it after the inserted tokens added during a shift 327 | if(requestData.ch > shift.pos) { 328 | requestData.ch += shift.length; 329 | } 330 | }) 331 | } 332 | } 333 | } 334 | }, 335 | 336 | _getShiftsByLines : function(relayContext, breakAtLine) { 337 | let shiftsByLine = new HashMap(); 338 | let buffer = relayContext.relayGraphQL; 339 | let shiftIndexRef = {value: 0}; 340 | let line = 0; 341 | for(let i = 0; i < buffer.length; i++) { 342 | let c = buffer.charCodeAt(i); 343 | if(c == newLine) { 344 | // get shifts for current line 345 | let lineShifts = this._getShifts(relayContext.shifts, shiftIndexRef, i); 346 | if(lineShifts != null) { 347 | if(lineShifts.length > 0) { 348 | shiftsByLine.set(line, lineShifts); 349 | } 350 | } else { 351 | // null indicates no more shifts 352 | break; 353 | } 354 | if(breakAtLine && breakAtLine == line) { 355 | break; 356 | } 357 | line++ 358 | } 359 | } 360 | if(line == 0) { 361 | // no newlines encountered, so all the shifts belong to line 0 362 | shiftsByLine.set(0, relayContext.shifts); 363 | } 364 | return shiftsByLine; 365 | }, 366 | 367 | _getShifts: function(shifts, shiftIndexRef, beforePos) { 368 | if(shiftIndexRef.value > shifts.length - 1) { 369 | // no more shifts 370 | return null 371 | } 372 | let ret = []; 373 | while(shiftIndexRef.value < shifts.length) { 374 | let shift = shifts[shiftIndexRef.value]; 375 | if(shift.pos < beforePos) { 376 | ret.push(shift); 377 | shiftIndexRef.value++; 378 | } else { 379 | break; 380 | } 381 | } 382 | return ret; 383 | }, 384 | 385 | /** 386 | * Reverses the transformation such that positions of returned tokens, error annotations etc. line up with the original text to parse. 387 | * Also restores any template tokens back to their original text. 388 | */ 389 | transformResponseData: function(responseData, command, relayContext) { 390 | let shifts = relayContext.shifts; 391 | let hasShifts = shifts.length > 0; 392 | let templateFromPosition = relayContext.templateFromPosition; 393 | let hasTemplates = templateFromPosition.count() > 0; 394 | 395 | if(command == 'getTokens') { 396 | if(responseData.tokens && responseData.tokens.length > 0) { 397 | 398 | let tokensForOriginalBuffer = []; 399 | 400 | let shiftIndex = 0; 401 | let shiftDelta = 0; 402 | let shiftPos = hasShifts ? shifts[0].pos : null; 403 | 404 | let lastAddedToken = null; 405 | for(let t = 0; t < responseData.tokens.length; t++) { 406 | 407 | let token = responseData.tokens[t]; 408 | 409 | // apply shifts 410 | if(hasShifts) { 411 | if (token.start == shiftPos) { 412 | // we shifted at this point by inserting one or more tokens, and we don't want them to appear in the response 413 | shiftDelta += shifts[shiftIndex].length; 414 | let skipTokensToPos = shiftPos + shifts[shiftIndex].length; 415 | while (token.end < skipTokensToPos) { 416 | t++; 417 | token = responseData.tokens[t]; 418 | } 419 | shiftIndex++; 420 | if (shiftIndex < shifts.length) { 421 | shiftPos = shifts[shiftIndex].pos; 422 | } else { 423 | shiftPos = -1; // no more shifts 424 | } 425 | token = null; // don't add the last token that made up the shift 426 | } else { 427 | // move the token back into its position before the shift 428 | if (shiftDelta > 0) { 429 | token.start -= shiftDelta; 430 | token.end -= shiftDelta; 431 | } 432 | } 433 | } 434 | 435 | // restore template fragments 436 | if(hasTemplates && token) { 437 | let template = templateFromPosition.get(token.start); 438 | if (template) { 439 | if (token.type != 'comment') { // no merge on comments since they fill up their lines 440 | let hasPadding = token.text.length < template.length; 441 | if (hasPadding) { 442 | // we padded the template replacement, so merge the next ws token with this one 443 | token.end = token.start + template.length 444 | t++; // and skip it 445 | } 446 | } 447 | if(token.type !== 'variable') { 448 | token.type = 'template-fragment'; 449 | } 450 | token.text = template; 451 | if (token.text.length != token.end - token.start) { 452 | console.error('Template replacement produced invalid token text range', token); 453 | } 454 | } 455 | } 456 | 457 | if(token) { 458 | if(lastAddedToken && lastAddedToken.end < token.start) { 459 | // there's a gap in the tokens caused by commas being included in whitespace 460 | // so we need to add the missing token 461 | const gapText = relayContext.textToParse.substring(lastAddedToken.end, token.start); 462 | const gapType = gapText.indexOf(',') != -1 ? 'punctuation' : 'ws'; 463 | const gap = { 464 | start: lastAddedToken.end, 465 | end: token.start, 466 | text: gapText, 467 | type: gapType, 468 | scope: lastAddedToken.scope, 469 | kind: lastAddedToken.kind 470 | }; 471 | tokensForOriginalBuffer.push(gap); 472 | } 473 | tokensForOriginalBuffer.push(token); 474 | lastAddedToken = token; 475 | 476 | if(token.type == 'ws') { 477 | const replacement = relayContext.wsEndReplacements[token.end]; 478 | if (replacement) { 479 | if(replacement.text.length == token.text.length) { 480 | // keep the token and update text and type 481 | token.text = replacement.text; 482 | token.type = replacement.type; 483 | } else { 484 | // split token and add the replacement after it 485 | const replacementToken = Object.assign({}, token); 486 | token.end = token.end - replacement.text.length; 487 | token.text = token.text.substr(0, token.text.length - replacement.text.length); 488 | replacementToken.text = replacement.text; 489 | replacementToken.type = replacement.type; 490 | replacementToken.start = token.end; 491 | tokensForOriginalBuffer.push(replacementToken); 492 | lastAddedToken = replacementToken; 493 | } 494 | } 495 | } 496 | } 497 | 498 | } 499 | 500 | responseData.tokens = tokensForOriginalBuffer; 501 | } 502 | } else if(command == 'getAnnotations') { 503 | if(responseData.annotations && responseData.annotations.length > 0) { 504 | if(hasShifts) { 505 | let shiftsByLine = this._getShiftsByLines(relayContext); 506 | for (let i = 0; i < responseData.annotations.length; i++) { 507 | let annotation = responseData.annotations[i]; 508 | let shiftsToApply = shiftsByLine.get(annotation.from.line); 509 | if (shiftsToApply) { 510 | shiftsToApply.forEach((shift) => { 511 | annotation.from.ch -= shift.length; 512 | annotation.to.ch -= shift.length; 513 | }); 514 | } 515 | } 516 | } 517 | responseData.annotations = this._filterAnnotations(responseData.annotations, relayContext); 518 | } 519 | } else if(command == 'getHints') { 520 | // strip the '{' completion according to https://facebook.github.io/relay/docs/api-reference-relay-ql.html 521 | if(responseData.hints) { 522 | let relayHints = []; 523 | responseData.hints.forEach(hint => { 524 | if(hint.text != '{') { 525 | relayHints.push(hint); 526 | } 527 | }); 528 | responseData.hints = relayHints; 529 | 530 | } 531 | } 532 | 533 | }, 534 | 535 | /** 536 | * Filters annotations for injected Relay GraphQL document-fragments and the specified environment 537 | */ 538 | _filterAnnotations : function(annotations, relayContext) { 539 | let relayAnnotations = annotations.filter((annotation) => 540 | relayValidationFilters.every((filter) => filter(annotation.message)) 541 | ); 542 | if(relayContext.env === 'apollo') { 543 | relayAnnotations = relayAnnotations.filter((annotation) => 544 | apolloValidationFilters.every((filter) => filter(annotation.message)) 545 | ); 546 | } else if(relayContext.env === 'lokka') { 547 | relayAnnotations = relayAnnotations.filter((annotation) => 548 | lokkaValidationFilters.every((filter) => filter(annotation.message)) 549 | ); 550 | } else if(relayContext.env === 'graphql-template') { 551 | relayAnnotations = relayAnnotations.filter((annotation) => 552 | graphqlTemplateFilters.every((filter) => filter(annotation.message)) 553 | ); 554 | } 555 | return relayAnnotations; 556 | } 557 | }; -------------------------------------------------------------------------------- /src/tests/data/getAnnotations.graphql: -------------------------------------------------------------------------------- 1 | ### --------- introspection.graphql --------- #### 2 | 3 | query IntrospectionQuery { 4 | 5 | __schema { 6 | queryType { name } 7 | mutationType { name } 8 | subscriptionType { name } 9 | types { 10 | ...FullType 11 | } 12 | directives { 13 | name 14 | description 15 | args { 16 | ...InputValue 17 | } 18 | locations 19 | } 20 | } 21 | } 22 | 23 | fragment FullType on __Type { 24 | kind 25 | name 26 | description 27 | fields { 28 | name 29 | description 30 | args { 31 | ...InputValue 32 | } 33 | type { 34 | ...TypeRef 35 | } 36 | isDeprecated 37 | deprecationReason 38 | } 39 | inputFields { 40 | ...InputValue 41 | } 42 | interfaces { 43 | ...TypeRef 44 | } 45 | enumValues { 46 | name 47 | description 48 | isDeprecated 49 | deprecationReason 50 | } 51 | possibleTypes { 52 | ...TypeRef 53 | } 54 | } 55 | 56 | fragment InputValue on __InputValue { 57 | name 58 | description 59 | type { ...TypeRef } 60 | defaultValue 61 | } 62 | 63 | fragment TypeRef on __Type { 64 | kind 65 | name 66 | ofType { 67 | kind 68 | name 69 | ofType { 70 | kind 71 | name 72 | ofType { 73 | kind 74 | name 75 | } 76 | } 77 | } 78 | } 79 | 80 | 81 | 82 | ### --------- colors.graphql --------- ### 83 | 84 | query MyQuery { 85 | __schema { 86 | types { 87 | ...FullType 88 | } 89 | } 90 | first: node(id: 1234) { id } 91 | second: node(id: "foo", option: true) { id } 92 | } 93 | 94 | fragment FullType on __Type { 95 | # Note: __Type has a lot more fields than this 96 | name 97 | } 98 | 99 | mutation MyMutation($input: MyInput!) { 100 | foo 101 | } 102 | 103 | 104 | ### --------- kitchen-sink.graphql --------- ### 105 | 106 | query queryName($foo: TestInput, $site: TestEnum = RED) { 107 | testAlias: hasArgs(string: "testString") 108 | ... on Test { 109 | hasArgs( 110 | listEnum: [RED, GREEN, BLUE] 111 | int: 1 112 | listFloat: [1.23, 1.3e-1, -1.35384e+3] 113 | boolean: true 114 | id: 123 115 | object: $foo 116 | enum: $site 117 | ) 118 | } 119 | test @include(if: true) { 120 | union { 121 | __typename 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/tests/data/getAnnotations.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": [ 3 | { 4 | "message": "Unknown argument \"option\" on field \"node\" of type \"Query\".", 5 | "severity": "error", 6 | "type": "validation", 7 | "from": { 8 | "line": 94, 9 | "ch": 36 10 | }, 11 | "to": { 12 | "line": 94, 13 | "ch": 42 14 | } 15 | }, 16 | { 17 | "message": "There can only be one fragment named \"FullType\".", 18 | "severity": "error", 19 | "type": "validation", 20 | "from": { 21 | "line": 23, 22 | "ch": 1 23 | }, 24 | "to": { 25 | "line": 24, 26 | "ch": 0 27 | } 28 | }, 29 | { 30 | "message": "There can only be one fragment named \"FullType\".", 31 | "severity": "error", 32 | "type": "validation", 33 | "from": { 34 | "line": 98, 35 | "ch": 9 36 | }, 37 | "to": { 38 | "line": 98, 39 | "ch": 17 40 | } 41 | }, 42 | { 43 | "message": "Unknown type \"MyInput\".", 44 | "severity": "error", 45 | "type": "validation", 46 | "from": { 47 | "line": 105, 48 | "ch": 24 49 | }, 50 | "to": { 51 | "line": 105, 52 | "ch": 31 53 | } 54 | }, 55 | { 56 | "message": "Cannot query field \"foo\" on type \"Mutation\".", 57 | "severity": "error", 58 | "type": "validation", 59 | "from": { 60 | "line": 105, 61 | "ch": 41 62 | }, 63 | "to": { 64 | "line": 105, 65 | "ch": 44 66 | } 67 | }, 68 | { 69 | "message": "Variable \"$input\" is never used in operation \"MyMutation\".", 70 | "severity": "error", 71 | "type": "validation", 72 | "from": { 73 | "line": 105, 74 | "ch": 16 75 | }, 76 | "to": { 77 | "line": 105, 78 | "ch": 22 79 | } 80 | }, 81 | { 82 | "message": "Unknown type \"TestInput\".", 83 | "severity": "error", 84 | "type": "validation", 85 | "from": { 86 | "line": 108, 87 | "ch": 6 88 | }, 89 | "to": { 90 | "line": 108, 91 | "ch": 15 92 | } 93 | }, 94 | { 95 | "message": "Unknown type \"TestEnum\".", 96 | "severity": "error", 97 | "type": "validation", 98 | "from": { 99 | "line": 109, 100 | "ch": 7 101 | }, 102 | "to": { 103 | "line": 109, 104 | "ch": 15 105 | } 106 | }, 107 | { 108 | "message": "Cannot query field \"hasArgs\" on type \"Query\".", 109 | "severity": "error", 110 | "type": "validation", 111 | "from": { 112 | "line": 110, 113 | "ch": 4 114 | }, 115 | "to": { 116 | "line": 110, 117 | "ch": 11 118 | } 119 | }, 120 | { 121 | "message": "Unknown type \"Test\".", 122 | "severity": "error", 123 | "type": "validation", 124 | "from": { 125 | "line": 111, 126 | "ch": 31 127 | }, 128 | "to": { 129 | "line": 111, 130 | "ch": 35 131 | } 132 | }, 133 | { 134 | "message": "Cannot query field \"test\" on type \"Query\".", 135 | "severity": "error", 136 | "type": "validation", 137 | "from": { 138 | "line": 123, 139 | "ch": 1 140 | }, 141 | "to": { 142 | "line": 123, 143 | "ch": 1 144 | } 145 | } 146 | ] 147 | } -------------------------------------------------------------------------------- /src/tests/data/getHintsForField.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": [ 3 | { 4 | "text": "types", 5 | "type": "[__Type!]!", 6 | "description": "A list of all types supported by this server.", 7 | "isDeprecated": false, 8 | "relay": false 9 | }, 10 | { 11 | "text": "queryType", 12 | "type": "__Type!", 13 | "description": "The type that query operations will be rooted at.", 14 | "isDeprecated": false, 15 | "relay": false 16 | }, 17 | { 18 | "text": "mutationType", 19 | "type": "__Type", 20 | "description": "If this server supports mutation, the type that mutation operations will be rooted at.", 21 | "isDeprecated": false, 22 | "relay": false 23 | }, 24 | { 25 | "text": "subscriptionType", 26 | "type": "__Type", 27 | "description": "If this server support subscription, the type that subscription operations will be rooted at.", 28 | "isDeprecated": false, 29 | "relay": false 30 | }, 31 | { 32 | "text": "directives", 33 | "type": "[__Directive!]!", 34 | "description": "A list of all directives supported by this server.", 35 | "isDeprecated": false, 36 | "relay": false 37 | } 38 | ], 39 | "from": { 40 | "line": 0, 41 | "ch": 25 42 | }, 43 | "to": { 44 | "line": 0, 45 | "ch": 25 46 | } 47 | } -------------------------------------------------------------------------------- /src/tests/data/getHintsForType.json: -------------------------------------------------------------------------------- 1 | { 2 | "hints": [ 3 | { 4 | "text": "Query", 5 | "description": null 6 | }, 7 | { 8 | "text": "Node", 9 | "description": "An object with an ID" 10 | }, 11 | { 12 | "text": "Mutation", 13 | "description": null 14 | }, 15 | { 16 | "text": "__Schema", 17 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations." 18 | }, 19 | { 20 | "text": "__Type", 21 | "description": "The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types." 22 | }, 23 | { 24 | "text": "__Field", 25 | "description": "Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type." 26 | }, 27 | { 28 | "text": "__InputValue", 29 | "description": "Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value." 30 | }, 31 | { 32 | "text": "__EnumValue", 33 | "description": "One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string." 34 | }, 35 | { 36 | "text": "__Directive", 37 | "description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor." 38 | }, 39 | { 40 | "text": "PageInfo", 41 | "description": "Information about pagination in a connection." 42 | } 43 | ], 44 | "from": { 45 | "line": 0, 46 | "ch": 16 47 | }, 48 | "to": { 49 | "line": 0, 50 | "ch": 16 51 | } 52 | } -------------------------------------------------------------------------------- /src/tests/data/getSchema.txt: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | # The id of the object. 3 | id: ID! 4 | } 5 | 6 | # An object with an ID 7 | interface Node { 8 | # The id of the object. 9 | id: ID! 10 | } 11 | 12 | # Information about pagination in a connection. 13 | type PageInfo { 14 | # When paginating forwards, are there more items? 15 | hasNextPage: Boolean! 16 | 17 | # When paginating backwards, are there more items? 18 | hasPreviousPage: Boolean! 19 | 20 | # When paginating backwards, the cursor to continue. 21 | startCursor: String 22 | 23 | # When paginating forwards, the cursor to continue. 24 | endCursor: String 25 | } 26 | 27 | type Query { 28 | # Fetches an object given its ID 29 | node( 30 | # The ID of an object 31 | id: ID! 32 | ): Node 33 | } 34 | -------------------------------------------------------------------------------- /src/tests/data/getTokenDocumentation.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "[__Type!]!", 3 | "description": "A list of all types supported by this server." 4 | } -------------------------------------------------------------------------------- /src/tests/data/getTokens.graphql: -------------------------------------------------------------------------------- 1 | ### --------- introspection.graphql --------- #### 2 | 3 | query IntrospectionQuery { 4 | 5 | __schema { 6 | queryType { name } 7 | mutationType { name } 8 | subscriptionType { name } 9 | types { 10 | ...FullType 11 | } 12 | directives { 13 | name 14 | description 15 | args { 16 | ...InputValue 17 | } 18 | onField 19 | onFragment 20 | onField 21 | } 22 | } 23 | } 24 | 25 | fragment FullType on __Type { 26 | kind 27 | name 28 | description 29 | fields { 30 | name 31 | description 32 | args { 33 | ...InputValue 34 | } 35 | type { 36 | ...TypeRef 37 | } 38 | isDeprecated 39 | deprecationReason 40 | } 41 | inputFields { 42 | ...InputValue 43 | } 44 | interfaces { 45 | ...TypeRef 46 | } 47 | enumValues { 48 | name 49 | description 50 | isDeprecated 51 | deprecationReason 52 | } 53 | possibleTypes { 54 | ...TypeRef 55 | } 56 | } 57 | 58 | fragment InputValue on __InputValue { 59 | name 60 | description 61 | type { ...TypeRef } 62 | defaultValue 63 | } 64 | 65 | fragment TypeRef on __Type { 66 | kind 67 | name 68 | ofType { 69 | kind 70 | name 71 | ofType { 72 | kind 73 | name 74 | ofType { 75 | kind 76 | name 77 | } 78 | } 79 | } 80 | } 81 | 82 | 83 | 84 | ### --------- colors.graphql --------- ### 85 | 86 | query MyQuery { 87 | __schema { 88 | types { 89 | ...FullType 90 | } 91 | } 92 | first: node(id: 1234) { id } 93 | second: node(id: "foo", option: true) { id } 94 | } 95 | 96 | fragment FullType on __Type { 97 | # Note: __Type has a lot more fields than this 98 | name 99 | } 100 | 101 | mutation MyMutation($input: MyInput!) { 102 | # Payload 103 | } 104 | 105 | %invalid% 106 | 107 | ### --------- kitchen-sink.graphql --------- ### 108 | 109 | query queryName($foo: TestInput, $site: TestEnum = RED) { 110 | testAlias: hasArgs(string: "testString") 111 | ... on Test { 112 | hasArgs( 113 | listEnum: [RED, GREEN, BLUE] 114 | int: 1 115 | listFloat: [1.23, 1.3e-1, -1.35384e+3] 116 | boolean: true 117 | id: 123 118 | object: $foo 119 | enum: $site 120 | ) 121 | } 122 | test @include(if: true) { 123 | union { 124 | __typename 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /src/tests/data/getTypeDocumentation.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "__Schema", 3 | "description": "A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.", 4 | "interfaces": [], 5 | "implementations": [], 6 | "fields": [ 7 | { 8 | "name": "types", 9 | "args": [], 10 | "type": "[__Type!]!", 11 | "description": "A list of all types supported by this server." 12 | }, 13 | { 14 | "name": "queryType", 15 | "args": [], 16 | "type": "__Type!", 17 | "description": "The type that query operations will be rooted at." 18 | }, 19 | { 20 | "name": "mutationType", 21 | "args": [], 22 | "type": "__Type", 23 | "description": "If this server supports mutation, the type that mutation operations will be rooted at." 24 | }, 25 | { 26 | "name": "subscriptionType", 27 | "args": [], 28 | "type": "__Type", 29 | "description": "If this server support subscription, the type that subscription operations will be rooted at." 30 | }, 31 | { 32 | "name": "directives", 33 | "args": [], 34 | "type": "[__Directive!]!", 35 | "description": "A list of all directives supported by this server." 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp-modern/graphql.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "file": "todoapp-modern.graphql" 4 | } 5 | } -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp-modern/todoAppModernExpectedSchema.txt: -------------------------------------------------------------------------------- 1 | input AddTodoInput { 2 | text: String! 3 | clientMutationId: String 4 | } 5 | 6 | type AddTodoPayload { 7 | todoEdge: TodoEdge 8 | viewer: User 9 | clientMutationId: String 10 | } 11 | 12 | input ChangeTodoStatusInput { 13 | complete: Boolean! 14 | id: ID! 15 | clientMutationId: String 16 | } 17 | 18 | type ChangeTodoStatusPayload { 19 | todo: Todo 20 | viewer: User 21 | clientMutationId: String 22 | } 23 | 24 | input MarkAllTodosInput { 25 | complete: Boolean! 26 | clientMutationId: String 27 | } 28 | 29 | type MarkAllTodosPayload { 30 | changedTodos: [Todo] 31 | viewer: User 32 | clientMutationId: String 33 | } 34 | 35 | type Mutation { 36 | addTodo(input: AddTodoInput!): AddTodoPayload 37 | changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload 38 | markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload 39 | removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload 40 | removeTodo(input: RemoveTodoInput!): RemoveTodoPayload 41 | renameTodo(input: RenameTodoInput!): RenameTodoPayload 42 | } 43 | 44 | # An object with an ID 45 | interface Node { 46 | # The id of the object. 47 | id: ID! 48 | } 49 | 50 | # Information about pagination in a connection. 51 | type PageInfo { 52 | # When paginating forwards, are there more items? 53 | hasNextPage: Boolean! 54 | 55 | # When paginating backwards, are there more items? 56 | hasPreviousPage: Boolean! 57 | 58 | # When paginating backwards, the cursor to continue. 59 | startCursor: String 60 | 61 | # When paginating forwards, the cursor to continue. 62 | endCursor: String 63 | } 64 | 65 | type Query { 66 | viewer: User 67 | 68 | # Fetches an object given its ID 69 | node( 70 | # The ID of an object 71 | id: ID! 72 | ): Node 73 | } 74 | 75 | input RemoveCompletedTodosInput { 76 | clientMutationId: String 77 | } 78 | 79 | type RemoveCompletedTodosPayload { 80 | deletedTodoIds: [String] 81 | viewer: User 82 | clientMutationId: String 83 | } 84 | 85 | input RemoveTodoInput { 86 | id: ID! 87 | clientMutationId: String 88 | } 89 | 90 | type RemoveTodoPayload { 91 | deletedTodoId: ID 92 | viewer: User 93 | clientMutationId: String 94 | } 95 | 96 | input RenameTodoInput { 97 | id: ID! 98 | text: String! 99 | clientMutationId: String 100 | } 101 | 102 | type RenameTodoPayload { 103 | todo: Todo 104 | clientMutationId: String 105 | } 106 | 107 | type Todo implements Node { 108 | # The ID of an object 109 | id: ID! 110 | text: String 111 | complete: Boolean 112 | } 113 | 114 | # A connection to a list of items. 115 | type TodoConnection { 116 | # Information to aid in pagination. 117 | pageInfo: PageInfo! 118 | 119 | # A list of edges. 120 | edges: [TodoEdge] 121 | } 122 | 123 | # An edge in a connection. 124 | type TodoEdge { 125 | # The item at the end of the edge 126 | node: Todo 127 | 128 | # A cursor for use in pagination 129 | cursor: String! 130 | } 131 | 132 | type User implements Node { 133 | # The ID of an object 134 | id: ID! 135 | todos(status: String = "any", after: String, first: Int, before: String, last: Int): TodoConnection 136 | totalCount: Int 137 | completedCount: Int 138 | } 139 | -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp-modern/todoapp-modern.graphql: -------------------------------------------------------------------------------- 1 | input AddTodoInput { 2 | text: String! 3 | clientMutationId: String 4 | } 5 | 6 | type AddTodoPayload { 7 | todoEdge: TodoEdge 8 | viewer: User 9 | clientMutationId: String 10 | } 11 | 12 | input ChangeTodoStatusInput { 13 | complete: Boolean! 14 | id: ID! 15 | clientMutationId: String 16 | } 17 | 18 | type ChangeTodoStatusPayload { 19 | todo: Todo 20 | viewer: User 21 | clientMutationId: String 22 | } 23 | 24 | input MarkAllTodosInput { 25 | complete: Boolean! 26 | clientMutationId: String 27 | } 28 | 29 | type MarkAllTodosPayload { 30 | changedTodos: [Todo] 31 | viewer: User 32 | clientMutationId: String 33 | } 34 | 35 | type Mutation { 36 | addTodo(input: AddTodoInput!): AddTodoPayload 37 | changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload 38 | markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload 39 | removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload 40 | removeTodo(input: RemoveTodoInput!): RemoveTodoPayload 41 | renameTodo(input: RenameTodoInput!): RenameTodoPayload 42 | } 43 | 44 | # An object with an ID 45 | interface Node { 46 | # The id of the object. 47 | id: ID! 48 | } 49 | 50 | # Information about pagination in a connection. 51 | type PageInfo { 52 | # When paginating forwards, are there more items? 53 | hasNextPage: Boolean! 54 | 55 | # When paginating backwards, are there more items? 56 | hasPreviousPage: Boolean! 57 | 58 | # When paginating backwards, the cursor to continue. 59 | startCursor: String 60 | 61 | # When paginating forwards, the cursor to continue. 62 | endCursor: String 63 | } 64 | 65 | type Query { 66 | viewer: User 67 | 68 | # Fetches an object given its ID 69 | node( 70 | # The ID of an object 71 | id: ID! 72 | ): Node 73 | } 74 | 75 | input RemoveCompletedTodosInput { 76 | clientMutationId: String 77 | } 78 | 79 | type RemoveCompletedTodosPayload { 80 | deletedTodoIds: [String] 81 | viewer: User 82 | clientMutationId: String 83 | } 84 | 85 | input RemoveTodoInput { 86 | id: ID! 87 | clientMutationId: String 88 | } 89 | 90 | type RemoveTodoPayload { 91 | deletedTodoId: ID 92 | viewer: User 93 | clientMutationId: String 94 | } 95 | 96 | input RenameTodoInput { 97 | id: ID! 98 | text: String! 99 | clientMutationId: String 100 | } 101 | 102 | type RenameTodoPayload { 103 | todo: Todo 104 | clientMutationId: String 105 | } 106 | 107 | type Todo implements Node { 108 | # The ID of an object 109 | id: ID! 110 | text: String 111 | complete: Boolean 112 | } 113 | 114 | # A connection to a list of items. 115 | type TodoConnection { 116 | # Information to aid in pagination. 117 | pageInfo: PageInfo! 118 | 119 | # A list of edges. 120 | edges: [TodoEdge] 121 | } 122 | 123 | # An edge in a connection. 124 | type TodoEdge { 125 | # The item at the end of the edge 126 | node: Todo 127 | 128 | # A cursor for use in pagination 129 | cursor: String! 130 | } 131 | 132 | type User implements Node { 133 | # The ID of an object 134 | id: ID! 135 | todos(status: String = "any", after: String, first: Int, before: String, last: Int): TodoConnection 136 | totalCount: Int 137 | completedCount: Int 138 | } 139 | -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp/getAnnotations.graphql: -------------------------------------------------------------------------------- 1 | # anonOperationNotAloneMessage 2 | { 3 | viewer { id } 4 | } 5 | 6 | # unusedFragMessage 7 | fragment Unused on User { 8 | id 9 | } 10 | 11 | # uniqueFragmentNames (see const fragmentNamePlaceHolder) 12 | fragment ____ on User { 13 | id 14 | } 15 | fragment ____ on User { 16 | id 17 | } 18 | 19 | # scalarLeafs 20 | mutation {addTodo} 21 | 22 | # noUndefinedVariables 23 | query Foo($id: ID!) { 24 | node(id: $id) { 25 | id 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp/getApolloAnnotations.graphql: -------------------------------------------------------------------------------- 1 | # apolloValidationFilters 2 | query { 3 | node(id: "") { 4 | ...allowFragmentInOtherFile 5 | } 6 | } 7 | 8 | ${Component.fragments.allowFragmentInOtherFile} 9 | 10 | fragment NotUsedInThisFile on Node { 11 | id 12 | } -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp/getLokkaAnnotations.graphql: -------------------------------------------------------------------------------- 1 | # lokkaValidationFilters 2 | query { 3 | node(id: $id) { 4 | __ #placeholder field for fragment interpolation '...${var}' -> ' __ ' 5 | } 6 | } -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp/graphql.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": { 3 | "file": "schema.json" 4 | } 5 | } -------------------------------------------------------------------------------- /src/tests/data/projects/todoapp/todoAppExpectedSchema.txt: -------------------------------------------------------------------------------- 1 | schema { 2 | query: Root 3 | mutation: Mutation 4 | } 5 | 6 | input AddTodoInput { 7 | text: String! 8 | clientMutationId: String! 9 | } 10 | 11 | type AddTodoPayload { 12 | todoEdge: TodoEdge 13 | viewer: User 14 | clientMutationId: String! 15 | } 16 | 17 | input ChangeTodoStatusInput { 18 | complete: Boolean! 19 | id: ID! 20 | clientMutationId: String! 21 | } 22 | 23 | type ChangeTodoStatusPayload { 24 | todo: Todo 25 | viewer: User 26 | clientMutationId: String! 27 | } 28 | 29 | input MarkAllTodosInput { 30 | complete: Boolean! 31 | clientMutationId: String! 32 | } 33 | 34 | type MarkAllTodosPayload { 35 | changedTodos: [Todo] 36 | viewer: User 37 | clientMutationId: String! 38 | } 39 | 40 | type Mutation { 41 | addTodo(input: AddTodoInput!): AddTodoPayload 42 | changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload 43 | markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload 44 | removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload 45 | removeTodo(input: RemoveTodoInput!): RemoveTodoPayload 46 | renameTodo(input: RenameTodoInput!): RenameTodoPayload 47 | } 48 | 49 | # An object with an ID 50 | interface Node { 51 | # The id of the object. 52 | id: ID! 53 | } 54 | 55 | # Information about pagination in a connection. 56 | type PageInfo { 57 | # When paginating forwards, are there more items? 58 | hasNextPage: Boolean! 59 | 60 | # When paginating backwards, are there more items? 61 | hasPreviousPage: Boolean! 62 | 63 | # When paginating backwards, the cursor to continue. 64 | startCursor: String 65 | 66 | # When paginating forwards, the cursor to continue. 67 | endCursor: String 68 | } 69 | 70 | input RemoveCompletedTodosInput { 71 | clientMutationId: String! 72 | } 73 | 74 | type RemoveCompletedTodosPayload { 75 | deletedTodoIds: [String] 76 | viewer: User 77 | clientMutationId: String! 78 | } 79 | 80 | input RemoveTodoInput { 81 | id: ID! 82 | clientMutationId: String! 83 | } 84 | 85 | type RemoveTodoPayload { 86 | deletedTodoId: ID 87 | viewer: User 88 | clientMutationId: String! 89 | } 90 | 91 | input RenameTodoInput { 92 | id: ID! 93 | text: String! 94 | clientMutationId: String! 95 | } 96 | 97 | type RenameTodoPayload { 98 | todo: Todo 99 | clientMutationId: String! 100 | } 101 | 102 | type Root { 103 | viewer: User 104 | 105 | # Fetches an object given its ID 106 | node( 107 | # The ID of an object 108 | id: ID! 109 | ): Node 110 | } 111 | 112 | type Todo implements Node { 113 | # The ID of an object 114 | id: ID! 115 | text: String 116 | complete: Boolean 117 | } 118 | 119 | # A connection to a list of items. 120 | type TodoConnection { 121 | # Information to aid in pagination. 122 | pageInfo: PageInfo! 123 | 124 | # Information to aid in pagination. 125 | edges: [TodoEdge] 126 | } 127 | 128 | # An edge in a connection. 129 | type TodoEdge { 130 | # The item at the end of the edge 131 | node: Todo 132 | 133 | # A cursor for use in pagination 134 | cursor: String! 135 | } 136 | 137 | type User implements Node { 138 | # The ID of an object 139 | id: ID! 140 | todos(status: String = "any", before: String, after: String, first: Int, last: Int): TodoConnection 141 | totalCount: Int 142 | completedCount: Int 143 | } 144 | -------------------------------------------------------------------------------- /src/tests/data/relay/commentBeforeFragment.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "text": "\n", 5 | "type": "ws", 6 | "start": 0, 7 | "end": 1 8 | }, 9 | { 10 | "text": "# fragment on Ship {", 11 | "type": "comment", 12 | "start": 1, 13 | "end": 32, 14 | "scope": 0, 15 | "kind": "Comment" 16 | }, 17 | { 18 | "text": "\n", 19 | "type": "ws", 20 | "start": 32, 21 | "end": 33 22 | }, 23 | { 24 | "text": " ", 25 | "type": "ws", 26 | "start": 33, 27 | "end": 49, 28 | "scope": 0, 29 | "kind": "Document" 30 | }, 31 | { 32 | "text": "id", 33 | "type": "invalidchar", 34 | "start": 49, 35 | "end": 51, 36 | "scope": 0, 37 | "kind": "Invalid" 38 | }, 39 | { 40 | "text": " ", 41 | "type": "ws", 42 | "start": 51, 43 | "end": 52, 44 | "scope": 0, 45 | "kind": "Document" 46 | }, 47 | { 48 | "text": "@", 49 | "type": "invalidchar", 50 | "start": 52, 51 | "end": 53, 52 | "scope": 0, 53 | "kind": "Invalid" 54 | }, 55 | { 56 | "text": "include", 57 | "type": "invalidchar", 58 | "start": 53, 59 | "end": 60, 60 | "scope": 0, 61 | "kind": "Invalid" 62 | }, 63 | { 64 | "text": "(", 65 | "type": "invalidchar", 66 | "start": 60, 67 | "end": 61, 68 | "scope": 1, 69 | "kind": "Invalid" 70 | }, 71 | { 72 | "text": "if", 73 | "type": "invalidchar", 74 | "start": 61, 75 | "end": 63, 76 | "scope": 1, 77 | "kind": "Invalid" 78 | }, 79 | { 80 | "text": ":", 81 | "type": "invalidchar", 82 | "start": 63, 83 | "end": 64, 84 | "scope": 1, 85 | "kind": "Invalid" 86 | }, 87 | { 88 | "text": " ", 89 | "type": "ws", 90 | "start": 64, 91 | "end": 65, 92 | "scope": 1, 93 | "kind": "Document" 94 | }, 95 | { 96 | "text": "true", 97 | "type": "invalidchar", 98 | "start": 65, 99 | "end": 69, 100 | "scope": 1, 101 | "kind": "Invalid" 102 | }, 103 | { 104 | "text": ")", 105 | "type": "invalidchar", 106 | "start": 69, 107 | "end": 70, 108 | "scope": 1, 109 | "kind": "Invalid" 110 | }, 111 | { 112 | "text": "\n", 113 | "type": "ws", 114 | "start": 70, 115 | "end": 71 116 | }, 117 | { 118 | "text": " ", 119 | "type": "ws", 120 | "start": 71, 121 | "end": 87, 122 | "scope": 1, 123 | "kind": "Document" 124 | }, 125 | { 126 | "text": "name", 127 | "type": "invalidchar", 128 | "start": 87, 129 | "end": 91, 130 | "scope": 1, 131 | "kind": "Invalid" 132 | }, 133 | { 134 | "text": "\n", 135 | "type": "ws", 136 | "start": 91, 137 | "end": 92 138 | }, 139 | { 140 | "text": " ", 141 | "type": "ws", 142 | "start": 92, 143 | "end": 104, 144 | "scope": 1, 145 | "kind": "Document" 146 | }, 147 | { 148 | "text": "}", 149 | "type": "invalidchar", 150 | "start": 104, 151 | "end": 105, 152 | "scope": 1, 153 | "kind": "Invalid" 154 | }, 155 | { 156 | "text": "\n", 157 | "type": "ws", 158 | "start": 105, 159 | "end": 106 160 | }, 161 | { 162 | "text": " ", 163 | "type": "ws", 164 | "start": 106, 165 | "end": 114, 166 | "scope": 1, 167 | "kind": "Document" 168 | } 169 | ] 170 | } -------------------------------------------------------------------------------- /src/tests/data/relay/multiplePlaceholdersPerLine.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "text": "{", 5 | "type": "punctuation", 6 | "start": 0, 7 | "end": 1, 8 | "scope": 1, 9 | "kind": "SelectionSet" 10 | }, 11 | { 12 | "text": " ", 13 | "type": "ws", 14 | "start": 1, 15 | "end": 2, 16 | "scope": 1, 17 | "kind": "SelectionSet" 18 | }, 19 | { 20 | "text": "nodes", 21 | "type": "property", 22 | "start": 2, 23 | "end": 7, 24 | "scope": 1, 25 | "kind": "Field" 26 | }, 27 | { 28 | "text": "(", 29 | "type": "punctuation", 30 | "start": 7, 31 | "end": 8, 32 | "scope": 2, 33 | "kind": "Arguments" 34 | }, 35 | { 36 | "text": "first", 37 | "type": "attribute", 38 | "start": 8, 39 | "end": 13, 40 | "scope": 2, 41 | "kind": "Argument" 42 | }, 43 | { 44 | "text": ":", 45 | "type": "punctuation", 46 | "start": 13, 47 | "end": 14, 48 | "scope": 2, 49 | "kind": "Argument" 50 | }, 51 | { 52 | "text": " ", 53 | "type": "ws", 54 | "start": 14, 55 | "end": 15, 56 | "scope": 2, 57 | "kind": "Argument" 58 | }, 59 | { 60 | "text": "${10}", 61 | "type": "variable", 62 | "start": 15, 63 | "end": 20, 64 | "scope": 2, 65 | "kind": "Variable" 66 | }, 67 | { 68 | "text": ", ", 69 | "type": "punctuation", 70 | "start": 20, 71 | "end": 22, 72 | "scope": 2, 73 | "kind": "Arguments" 74 | }, 75 | { 76 | "text": "foo", 77 | "type": "attribute", 78 | "start": 22, 79 | "end": 25, 80 | "scope": 2, 81 | "kind": "Argument" 82 | }, 83 | { 84 | "text": ":", 85 | "type": "punctuation", 86 | "start": 25, 87 | "end": 26, 88 | "scope": 2, 89 | "kind": "Argument" 90 | }, 91 | { 92 | "text": " ", 93 | "type": "ws", 94 | "start": 26, 95 | "end": 27, 96 | "scope": 2, 97 | "kind": "Argument" 98 | }, 99 | { 100 | "text": "${100}", 101 | "type": "variable", 102 | "start": 27, 103 | "end": 33, 104 | "scope": 2, 105 | "kind": "Variable" 106 | }, 107 | { 108 | "text": ")", 109 | "type": "punctuation", 110 | "start": 33, 111 | "end": 34, 112 | "scope": 1, 113 | "kind": "Field" 114 | }, 115 | { 116 | "text": " ", 117 | "type": "ws", 118 | "start": 34, 119 | "end": 35, 120 | "scope": 1, 121 | "kind": "Field" 122 | }, 123 | { 124 | "text": "{", 125 | "type": "punctuation", 126 | "start": 35, 127 | "end": 36, 128 | "scope": 2, 129 | "kind": "SelectionSet" 130 | }, 131 | { 132 | "text": " ", 133 | "type": "ws", 134 | "start": 36, 135 | "end": 37, 136 | "scope": 2, 137 | "kind": "SelectionSet" 138 | }, 139 | { 140 | "text": "id", 141 | "type": "property", 142 | "start": 37, 143 | "end": 39, 144 | "scope": 2, 145 | "kind": "Field" 146 | }, 147 | { 148 | "text": " ", 149 | "type": "ws", 150 | "start": 39, 151 | "end": 40, 152 | "scope": 2, 153 | "kind": "Field" 154 | }, 155 | { 156 | "text": "}", 157 | "type": "punctuation", 158 | "start": 40, 159 | "end": 41, 160 | "scope": 1, 161 | "kind": "SelectionSet" 162 | }, 163 | { 164 | "text": " ", 165 | "type": "ws", 166 | "start": 41, 167 | "end": 42, 168 | "scope": 1, 169 | "kind": "SelectionSet" 170 | }, 171 | { 172 | "text": "}", 173 | "type": "punctuation", 174 | "start": 42, 175 | "end": 43, 176 | "scope": 0, 177 | "kind": "Document" 178 | } 179 | ] 180 | } -------------------------------------------------------------------------------- /src/tests/data/relay/templateFragment1.graphql: -------------------------------------------------------------------------------- 1 | fragment on User { 2 | totalCount, 3 | ${AddTodoMutation.getFragment('viewer')}, 4 | ${TodoListFooter.getFragment('viewer')}, 5 | } -------------------------------------------------------------------------------- /src/tests/data/relay/templateFragment1.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "text": " ", 5 | "type": "ws", 6 | "start": 0, 7 | "end": 4, 8 | "scope": 0, 9 | "kind": "Document" 10 | }, 11 | { 12 | "text": "fragment", 13 | "type": "keyword", 14 | "start": 4, 15 | "end": 12, 16 | "scope": 0, 17 | "kind": "FragmentDefinition" 18 | }, 19 | { 20 | "text": " ", 21 | "type": "ws", 22 | "start": 12, 23 | "end": 13, 24 | "scope": 0, 25 | "kind": "FragmentDefinition" 26 | }, 27 | { 28 | "text": "on", 29 | "type": "keyword", 30 | "start": 13, 31 | "end": 15, 32 | "scope": 0, 33 | "kind": "TypeCondition" 34 | }, 35 | { 36 | "text": " ", 37 | "type": "ws", 38 | "start": 15, 39 | "end": 16, 40 | "scope": 0, 41 | "kind": "TypeCondition" 42 | }, 43 | { 44 | "text": "User", 45 | "type": "atom", 46 | "start": 16, 47 | "end": 20, 48 | "scope": 0, 49 | "kind": "NamedType" 50 | }, 51 | { 52 | "text": " ", 53 | "type": "ws", 54 | "start": 20, 55 | "end": 21, 56 | "scope": 0, 57 | "kind": "FragmentDefinition" 58 | }, 59 | { 60 | "text": "{", 61 | "type": "punctuation", 62 | "start": 21, 63 | "end": 22, 64 | "scope": 1, 65 | "kind": "SelectionSet" 66 | }, 67 | { 68 | "text": "\n", 69 | "type": "ws", 70 | "start": 22, 71 | "end": 23 72 | }, 73 | { 74 | "text": " ", 75 | "type": "ws", 76 | "start": 23, 77 | "end": 31, 78 | "scope": 1, 79 | "kind": "SelectionSet" 80 | }, 81 | { 82 | "text": "totalCount", 83 | "type": "property", 84 | "start": 31, 85 | "end": 41, 86 | "scope": 1, 87 | "kind": "Field" 88 | }, 89 | { 90 | "text": ",", 91 | "type": "punctuation", 92 | "start": 41, 93 | "end": 42, 94 | "scope": 1, 95 | "kind": "Field" 96 | }, 97 | { 98 | "text": "\n", 99 | "type": "ws", 100 | "start": 42, 101 | "end": 43 102 | }, 103 | { 104 | "text": " ", 105 | "type": "ws", 106 | "start": 43, 107 | "end": 51, 108 | "scope": 1, 109 | "kind": "Field" 110 | }, 111 | { 112 | "text": "__typename", 113 | "type": "property", 114 | "start": 51, 115 | "end": 61, 116 | "scope": 1, 117 | "kind": "Field" 118 | }, 119 | { 120 | "text": " ,", 121 | "type": "punctuation", 122 | "start": 61, 123 | "end": 92, 124 | "scope": 1, 125 | "kind": "Field" 126 | }, 127 | { 128 | "text": "\n", 129 | "type": "ws", 130 | "start": 92, 131 | "end": 93 132 | }, 133 | { 134 | "text": " ", 135 | "type": "ws", 136 | "start": 93, 137 | "end": 101, 138 | "scope": 1, 139 | "kind": "Field" 140 | }, 141 | { 142 | "text": "__typename", 143 | "type": "property", 144 | "start": 101, 145 | "end": 111, 146 | "scope": 1, 147 | "kind": "Field" 148 | }, 149 | { 150 | "text": " ,", 151 | "type": "punctuation", 152 | "start": 111, 153 | "end": 141, 154 | "scope": 1, 155 | "kind": "Field" 156 | }, 157 | { 158 | "text": "\n", 159 | "type": "ws", 160 | "start": 141, 161 | "end": 142 162 | }, 163 | { 164 | "text": " ", 165 | "type": "ws", 166 | "start": 142, 167 | "end": 146, 168 | "scope": 1, 169 | "kind": "Field" 170 | }, 171 | { 172 | "text": "}", 173 | "type": "punctuation", 174 | "start": 146, 175 | "end": 147, 176 | "scope": 0, 177 | "kind": "Document" 178 | } 179 | ] 180 | } -------------------------------------------------------------------------------- /src/tests/data/relay/templateFragment2.graphql: -------------------------------------------------------------------------------- 1 | fragment on User { 2 | totalCount, 3 | ${..}, 4 | ${foo 5 | } -------------------------------------------------------------------------------- /src/tests/data/relay/templateFragment2.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "text": " ", 5 | "type": "ws", 6 | "start": 0, 7 | "end": 4, 8 | "scope": 0, 9 | "kind": "Document" 10 | }, 11 | { 12 | "text": "fragment", 13 | "type": "keyword", 14 | "start": 4, 15 | "end": 12, 16 | "scope": 0, 17 | "kind": "FragmentDefinition" 18 | }, 19 | { 20 | "text": " ", 21 | "type": "ws", 22 | "start": 12, 23 | "end": 13, 24 | "scope": 0, 25 | "kind": "FragmentDefinition" 26 | }, 27 | { 28 | "text": "on", 29 | "type": "keyword", 30 | "start": 13, 31 | "end": 15, 32 | "scope": 0, 33 | "kind": "TypeCondition" 34 | }, 35 | { 36 | "text": " ", 37 | "type": "ws", 38 | "start": 15, 39 | "end": 16, 40 | "scope": 0, 41 | "kind": "TypeCondition" 42 | }, 43 | { 44 | "text": "User", 45 | "type": "atom", 46 | "start": 16, 47 | "end": 20, 48 | "scope": 0, 49 | "kind": "NamedType" 50 | }, 51 | { 52 | "text": " ", 53 | "type": "ws", 54 | "start": 20, 55 | "end": 21, 56 | "scope": 0, 57 | "kind": "FragmentDefinition" 58 | }, 59 | { 60 | "text": "{", 61 | "type": "punctuation", 62 | "start": 21, 63 | "end": 22, 64 | "scope": 1, 65 | "kind": "SelectionSet" 66 | }, 67 | { 68 | "text": "\n", 69 | "type": "ws", 70 | "start": 22, 71 | "end": 23 72 | }, 73 | { 74 | "text": " ", 75 | "type": "ws", 76 | "start": 23, 77 | "end": 31, 78 | "scope": 1, 79 | "kind": "SelectionSet" 80 | }, 81 | { 82 | "text": "totalCount", 83 | "type": "property", 84 | "start": 31, 85 | "end": 41, 86 | "scope": 1, 87 | "kind": "Field" 88 | }, 89 | { 90 | "text": ",", 91 | "type": "punctuation", 92 | "start": 41, 93 | "end": 42, 94 | "scope": 1, 95 | "kind": "Field" 96 | }, 97 | { 98 | "text": "\n", 99 | "type": "ws", 100 | "start": 42, 101 | "end": 43 102 | }, 103 | { 104 | "text": " ", 105 | "type": "ws", 106 | "start": 43, 107 | "end": 51, 108 | "scope": 1, 109 | "kind": "Field" 110 | }, 111 | { 112 | "text": "#{..},", 113 | "type": "comment", 114 | "start": 51, 115 | "end": 57, 116 | "scope": 1, 117 | "kind": "Comment" 118 | }, 119 | { 120 | "text": "\n", 121 | "type": "ws", 122 | "start": 57, 123 | "end": 58 124 | }, 125 | { 126 | "text": " ", 127 | "type": "ws", 128 | "start": 58, 129 | "end": 66, 130 | "scope": 1, 131 | "kind": "Field" 132 | }, 133 | { 134 | "text": "#{foo", 135 | "type": "comment", 136 | "start": 66, 137 | "end": 71, 138 | "scope": 1, 139 | "kind": "Comment" 140 | }, 141 | { 142 | "text": "\n", 143 | "type": "ws", 144 | "start": 71, 145 | "end": 72 146 | }, 147 | { 148 | "text": " ", 149 | "type": "ws", 150 | "start": 72, 151 | "end": 76, 152 | "scope": 1, 153 | "kind": "Field" 154 | }, 155 | { 156 | "text": "}", 157 | "type": "punctuation", 158 | "start": 76, 159 | "end": 77, 160 | "scope": 0, 161 | "kind": "Document" 162 | } 163 | ] 164 | } -------------------------------------------------------------------------------- /src/tests/data/relay/templateFragment3.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "text": "\n", 5 | "type": "ws", 6 | "start": 0, 7 | "end": 1 8 | }, 9 | { 10 | "text": " ", 11 | "type": "ws", 12 | "start": 1, 13 | "end": 13, 14 | "scope": 0, 15 | "kind": "Document" 16 | }, 17 | { 18 | "text": "fragment", 19 | "type": "keyword", 20 | "start": 13, 21 | "end": 21, 22 | "scope": 0, 23 | "kind": "FragmentDefinition" 24 | }, 25 | { 26 | "text": " ", 27 | "type": "ws", 28 | "start": 21, 29 | "end": 22, 30 | "scope": 0, 31 | "kind": "FragmentDefinition" 32 | }, 33 | { 34 | "text": "on", 35 | "type": "keyword", 36 | "start": 22, 37 | "end": 24, 38 | "scope": 0, 39 | "kind": "TypeCondition" 40 | }, 41 | { 42 | "text": " ", 43 | "type": "ws", 44 | "start": 24, 45 | "end": 25, 46 | "scope": 0, 47 | "kind": "TypeCondition" 48 | }, 49 | { 50 | "text": "Todo", 51 | "type": "atom", 52 | "start": 25, 53 | "end": 29, 54 | "scope": 0, 55 | "kind": "NamedType" 56 | }, 57 | { 58 | "text": " ", 59 | "type": "ws", 60 | "start": 29, 61 | "end": 30, 62 | "scope": 0, 63 | "kind": "FragmentDefinition" 64 | }, 65 | { 66 | "text": "@", 67 | "type": "meta", 68 | "start": 30, 69 | "end": 31, 70 | "scope": 0, 71 | "kind": "Directive" 72 | }, 73 | { 74 | "text": "relay", 75 | "type": "meta", 76 | "start": 31, 77 | "end": 36, 78 | "scope": 0, 79 | "kind": "Directive" 80 | }, 81 | { 82 | "text": "(", 83 | "type": "punctuation", 84 | "start": 36, 85 | "end": 37, 86 | "scope": 1, 87 | "kind": "Arguments" 88 | }, 89 | { 90 | "text": "plural", 91 | "type": "attribute", 92 | "start": 37, 93 | "end": 43, 94 | "scope": 1, 95 | "kind": "Argument" 96 | }, 97 | { 98 | "text": ":", 99 | "type": "punctuation", 100 | "start": 43, 101 | "end": 44, 102 | "scope": 1, 103 | "kind": "Argument" 104 | }, 105 | { 106 | "text": " ", 107 | "type": "ws", 108 | "start": 44, 109 | "end": 45, 110 | "scope": 1, 111 | "kind": "Argument" 112 | }, 113 | { 114 | "text": "true", 115 | "type": "builtin", 116 | "start": 45, 117 | "end": 49, 118 | "scope": 1, 119 | "kind": "BooleanValue" 120 | }, 121 | { 122 | "text": ")", 123 | "type": "punctuation", 124 | "start": 49, 125 | "end": 50, 126 | "scope": 0, 127 | "kind": "FragmentDefinition" 128 | }, 129 | { 130 | "text": " ", 131 | "type": "ws", 132 | "start": 50, 133 | "end": 51, 134 | "scope": 0, 135 | "kind": "FragmentDefinition" 136 | }, 137 | { 138 | "text": "{", 139 | "type": "punctuation", 140 | "start": 51, 141 | "end": 52, 142 | "scope": 1, 143 | "kind": "SelectionSet" 144 | }, 145 | { 146 | "text": "\n", 147 | "type": "ws", 148 | "start": 52, 149 | "end": 53 150 | }, 151 | { 152 | "text": " ", 153 | "type": "ws", 154 | "start": 53, 155 | "end": 69, 156 | "scope": 1, 157 | "kind": "SelectionSet" 158 | }, 159 | { 160 | "text": "id", 161 | "type": "property", 162 | "start": 69, 163 | "end": 71, 164 | "scope": 1, 165 | "kind": "Field" 166 | }, 167 | { 168 | "text": ",", 169 | "type": "punctuation", 170 | "start": 71, 171 | "end": 72, 172 | "scope": 1, 173 | "kind": "Field" 174 | }, 175 | { 176 | "text": "\n", 177 | "type": "ws", 178 | "start": 72, 179 | "end": 73 180 | }, 181 | { 182 | "text": " ", 183 | "type": "ws", 184 | "start": 73, 185 | "end": 89, 186 | "scope": 1, 187 | "kind": "Field" 188 | }, 189 | { 190 | "text": "${Todo.getFragment('todo')}", 191 | "type": "template-fragment", 192 | "start": 89, 193 | "end": 116, 194 | "scope": 1, 195 | "kind": "Field" 196 | }, 197 | { 198 | "start": 116, 199 | "end": 117, 200 | "text": ",", 201 | "type": "punctuation", 202 | "scope": 1, 203 | "kind": "Field" 204 | }, 205 | { 206 | "text": "\n", 207 | "type": "ws", 208 | "start": 117, 209 | "end": 118 210 | }, 211 | { 212 | "text": " ", 213 | "type": "ws", 214 | "start": 118, 215 | "end": 130, 216 | "scope": 1, 217 | "kind": "Field" 218 | }, 219 | { 220 | "text": "}", 221 | "type": "punctuation", 222 | "start": 130, 223 | "end": 131, 224 | "scope": 0, 225 | "kind": "Document" 226 | }, 227 | { 228 | "text": "\n", 229 | "type": "ws", 230 | "start": 131, 231 | "end": 132 232 | }, 233 | { 234 | "text": " ", 235 | "type": "ws", 236 | "start": 132, 237 | "end": 140, 238 | "scope": 0, 239 | "kind": "Document" 240 | } 241 | ] 242 | } -------------------------------------------------------------------------------- /src/tests/data/relay/templateFragment4.json: -------------------------------------------------------------------------------- 1 | { 2 | "tokens": [ 3 | { 4 | "text": "\n", 5 | "type": "ws", 6 | "start": 0, 7 | "end": 1 8 | }, 9 | { 10 | "text": " ", 11 | "type": "ws", 12 | "start": 1, 13 | "end": 13, 14 | "scope": 0, 15 | "kind": "Document" 16 | }, 17 | { 18 | "text": "fragment", 19 | "type": "keyword", 20 | "start": 13, 21 | "end": 21, 22 | "scope": 0, 23 | "kind": "FragmentDefinition" 24 | }, 25 | { 26 | "text": " ", 27 | "type": "ws", 28 | "start": 21, 29 | "end": 22, 30 | "scope": 0, 31 | "kind": "FragmentDefinition" 32 | }, 33 | { 34 | "text": "on", 35 | "type": "keyword", 36 | "start": 22, 37 | "end": 24, 38 | "scope": 0, 39 | "kind": "TypeCondition" 40 | }, 41 | { 42 | "text": " ", 43 | "type": "ws", 44 | "start": 24, 45 | "end": 25, 46 | "scope": 0, 47 | "kind": "TypeCondition" 48 | }, 49 | { 50 | "text": "Todo", 51 | "type": "atom", 52 | "start": 25, 53 | "end": 29, 54 | "scope": 0, 55 | "kind": "NamedType" 56 | }, 57 | { 58 | "text": " ", 59 | "type": "ws", 60 | "start": 29, 61 | "end": 30, 62 | "scope": 0, 63 | "kind": "FragmentDefinition" 64 | }, 65 | { 66 | "text": "@", 67 | "type": "meta", 68 | "start": 30, 69 | "end": 31, 70 | "scope": 0, 71 | "kind": "Directive" 72 | }, 73 | { 74 | "text": "relay", 75 | "type": "meta", 76 | "start": 31, 77 | "end": 36, 78 | "scope": 0, 79 | "kind": "Directive" 80 | }, 81 | { 82 | "text": "(", 83 | "type": "punctuation", 84 | "start": 36, 85 | "end": 37, 86 | "scope": 1, 87 | "kind": "Arguments" 88 | }, 89 | { 90 | "text": "plural", 91 | "type": "attribute", 92 | "start": 37, 93 | "end": 43, 94 | "scope": 1, 95 | "kind": "Argument" 96 | }, 97 | { 98 | "text": ":", 99 | "type": "punctuation", 100 | "start": 43, 101 | "end": 44, 102 | "scope": 1, 103 | "kind": "Argument" 104 | }, 105 | { 106 | "text": " ", 107 | "type": "ws", 108 | "start": 44, 109 | "end": 45, 110 | "scope": 1, 111 | "kind": "Argument" 112 | }, 113 | { 114 | "text": "true", 115 | "type": "builtin", 116 | "start": 45, 117 | "end": 49, 118 | "scope": 1, 119 | "kind": "BooleanValue" 120 | }, 121 | { 122 | "text": ")", 123 | "type": "punctuation", 124 | "start": 49, 125 | "end": 50, 126 | "scope": 0, 127 | "kind": "FragmentDefinition" 128 | }, 129 | { 130 | "text": " ", 131 | "type": "ws", 132 | "start": 50, 133 | "end": 51, 134 | "scope": 0, 135 | "kind": "FragmentDefinition" 136 | }, 137 | { 138 | "text": "{", 139 | "type": "punctuation", 140 | "start": 51, 141 | "end": 52, 142 | "scope": 1, 143 | "kind": "SelectionSet" 144 | }, 145 | { 146 | "text": "\n", 147 | "type": "ws", 148 | "start": 52, 149 | "end": 53 150 | }, 151 | { 152 | "text": " ", 153 | "type": "ws", 154 | "start": 53, 155 | "end": 69, 156 | "scope": 1, 157 | "kind": "SelectionSet" 158 | }, 159 | { 160 | "text": "id", 161 | "type": "property", 162 | "start": 69, 163 | "end": 71, 164 | "scope": 1, 165 | "kind": "Field" 166 | }, 167 | { 168 | "text": ",", 169 | "type": "punctuation", 170 | "start": 71, 171 | "end": 72, 172 | "scope": 1, 173 | "kind": "Field" 174 | }, 175 | { 176 | "text": "\n", 177 | "type": "ws", 178 | "start": 72, 179 | "end": 73 180 | }, 181 | { 182 | "text": " ", 183 | "type": "ws", 184 | "start": 73, 185 | "end": 89, 186 | "scope": 1, 187 | "kind": "Field" 188 | }, 189 | { 190 | "text": "${Todo.getFragment('todo', {foo: 'bar'})}", 191 | "type": "template-fragment", 192 | "start": 89, 193 | "end": 130, 194 | "scope": 1, 195 | "kind": "Field" 196 | }, 197 | { 198 | "start": 130, 199 | "end": 131, 200 | "text": ",", 201 | "type": "punctuation", 202 | "scope": 1, 203 | "kind": "Field" 204 | }, 205 | { 206 | "text": "\n", 207 | "type": "ws", 208 | "start": 131, 209 | "end": 132 210 | }, 211 | { 212 | "text": " ", 213 | "type": "ws", 214 | "start": 132, 215 | "end": 144, 216 | "scope": 1, 217 | "kind": "Field" 218 | }, 219 | { 220 | "text": "}", 221 | "type": "punctuation", 222 | "start": 144, 223 | "end": 145, 224 | "scope": 0, 225 | "kind": "Document" 226 | }, 227 | { 228 | "text": "\n", 229 | "type": "ws", 230 | "start": 145, 231 | "end": 146 232 | }, 233 | { 234 | "text": " ", 235 | "type": "ws", 236 | "start": 146, 237 | "end": 154, 238 | "scope": 0, 239 | "kind": "Document" 240 | } 241 | ] 242 | } -------------------------------------------------------------------------------- /src/tests/spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Jim Kynde Meyer 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 'use scrict'; 9 | 10 | const request = require('supertest'); 11 | const app = require('../languageservice'); 12 | const fs = require('fs'); 13 | const path = require('path'); 14 | 15 | const url = '/js-graphql-language-service'; 16 | 17 | 18 | describe('GET /js-graphql-language-service', function(){ 19 | it('responds with json', function(done){ 20 | request(app) 21 | .get(url) 22 | .set('Accept', 'application/json') 23 | .expect('Content-Type', /json/) 24 | .expect(200, done); 25 | }) 26 | }); 27 | 28 | 29 | const getTokensGraphQL = fs.readFileSync(require.resolve('./data/getTokens.graphql'), 'utf-8'); 30 | describe('getTokens', function(){ 31 | it('responds with expected tokens', function(done){ 32 | request(app) 33 | .post(url) 34 | .set('Content-Type', 'application/json') 35 | .send({ command: 'getTokens', buffer: getTokensGraphQL}) 36 | .expect(require('./data/getTokens.json')) 37 | .expect(200, done); 38 | }) 39 | }); 40 | 41 | 42 | describe('getHints for Type', function(){ 43 | it('responds with expected hint', function(done){ 44 | request(app) 45 | .post(url) 46 | .set('Content-Type', 'application/json') 47 | .send({ command: 'getHints', buffer: 'fragment foo on User { id }', line: 0, ch: 16 /* before 'User' */}) 48 | .expect(require('./data/getHintsForType.json')) 49 | .expect(200, done); 50 | }) 51 | }); 52 | 53 | 54 | describe('getHints for Field', function(){ 55 | it('responds with expected hint', function(done){ 56 | request(app) 57 | .post(url) 58 | .set('Content-Type', 'application/json') 59 | .send({ command: 'getHints', buffer: 'fragment F on __Schema { types }', line: 0, ch: 25 /* before 'types' */}) 60 | .expect(require('./data/getHintsForField.json')) 61 | .expect(200, done); 62 | }) 63 | }); 64 | 65 | 66 | describe('getTokenDocumentation', function(){ 67 | it('responds with expected doc', function(done){ 68 | request(app) 69 | .post(url) 70 | .set('Content-Type', 'application/json') 71 | .send({ command: 'getTokenDocumentation', buffer: 'fragment F on __Schema { types }', line: 0, ch: 25}) 72 | .expect(require('./data/getTokenDocumentation.json')) 73 | .expect(200, done); 74 | }) 75 | }); 76 | 77 | 78 | describe('getTypeDocumentation', function(){ 79 | it('responds with expected doc', function(done){ 80 | request(app) 81 | .post(url) 82 | .set('Content-Type', 'application/json') 83 | .send({ command: 'getTypeDocumentation', type: '__Schema'}) 84 | .expect(require('./data/getTypeDocumentation.json')) 85 | .expect(200, done); 86 | }) 87 | }); 88 | 89 | const getAnnotationsGraphQL = fs.readFileSync(require.resolve('./data/getAnnotations.graphql'), 'utf-8'); 90 | describe('getAnnotations', function(){ 91 | it('responds with expected annotations', function(done){ 92 | request(app) 93 | .post(url) 94 | .set('Content-Type', 'application/json') 95 | .send({ command: 'getAnnotations', buffer: getAnnotationsGraphQL}) 96 | .expect(require('./data/getAnnotations.json')) 97 | .expect(200, done); 98 | }) 99 | }); 100 | 101 | describe('getAST', function(){ 102 | it('responds with expected AST', function(done){ 103 | request(app) 104 | .post(url) 105 | .set('Content-Type', 'application/json') 106 | .send({ command: 'getAST', buffer: getAnnotationsGraphQL}) 107 | .expect(require('./data/getAST.json')) 108 | .expect(200, done); 109 | }) 110 | }); 111 | 112 | const getSchemaText = fs.readFileSync(require.resolve('./data/getSchema.txt'), 'utf-8'); 113 | describe('getSchema', function(){ 114 | it('responds with expected schema', function(done){ 115 | request(app) 116 | .post(url) 117 | .set('Content-Type', 'application/json') 118 | .send({ command: 'getSchema'}) 119 | .expect(getSchemaText) 120 | .expect(200, done); 121 | }) 122 | }); 123 | 124 | 125 | // ---- Relay.QL tagged templates ---- 126 | 127 | const templateFragment1GraphQL = fs.readFileSync(require.resolve('./data/relay/templateFragment1.graphql'), 'utf-8'); 128 | describe('getTokens relay template fragments #1', function(){ 129 | it('responds with expected tokens', function(done){ 130 | request(app) 131 | .post(url) 132 | .set('Content-Type', 'application/json') 133 | .send({ command: 'getTokens', buffer: templateFragment1GraphQL, env: 'relay'}) 134 | .expect(require('./data/relay/templateFragment1.json')) 135 | .expect(200, done); 136 | }) 137 | }); 138 | 139 | const templateFragment2GraphQL = fs.readFileSync(require.resolve('./data/relay/templateFragment2.graphql'), 'utf-8'); 140 | describe('getTokens relay template fragments #2', function(){ 141 | it('responds with expected tokens', function(done){ 142 | request(app) 143 | .post(url) 144 | .set('Content-Type', 'application/json') 145 | .send({ command: 'getTokens', buffer: templateFragment2GraphQL, env: 'relay'}) 146 | .expect(require('./data/relay/templateFragment2.json')) 147 | .expect(200, done); 148 | }) 149 | }); 150 | 151 | describe('getTokens relay template fragments #3', function(){ 152 | it('responds with expected tokens', function(done){ 153 | request(app) 154 | .post(url) 155 | .set('Content-Type', 'application/json') 156 | .send({ command: 'getTokens', buffer: "\n fragment on Todo @relay(plural: true) {\n id,\n ${Todo.getFragment('todo')},\n }\n ", env: 'relay'}) 157 | .expect(require('./data/relay/templateFragment3.json')) 158 | .expect(200, done); 159 | }) 160 | }); 161 | 162 | describe('getTokens relay template fragments #4', function(){ 163 | it('responds with expected tokens', function(done){ 164 | request(app) 165 | .post(url) 166 | .set('Content-Type', 'application/json') 167 | .send({ command: 'getTokens', buffer: "\n fragment on Todo @relay(plural: true) {\n id,\n ${Todo.getFragment('todo', {foo: 'bar'})},\n }\n ", env: 'relay'}) 168 | .expect(require('./data/relay/templateFragment4.json')) 169 | .expect(200, done); 170 | }) 171 | }); 172 | 173 | describe('getTokens relay comment before fragment', function(){ 174 | it('responds with expected tokens', function(done){ 175 | request(app) 176 | .post(url) 177 | .set('Content-Type', 'application/json') 178 | .send({ command: 'getTokens', buffer: "\n# fragment on Ship {\n id @include(if: true)\n name\n }\n ", env: 'relay'}) 179 | .expect(require('./data/relay/commentBeforeFragment.json')) 180 | .expect(200, done); 181 | }) 182 | }); 183 | 184 | describe('getTokens multiple place holders per line', function(){ 185 | it('responds with valid token ranges', function(done){ 186 | request(app) 187 | .post(url) 188 | .set('Content-Type', 'application/json') 189 | .send({ command: 'getTokens', buffer: "{ nodes(first: ${10}, foo: ${100}) { id } }", env: 'graphql-template'}) 190 | .expect(require('./data/relay/multiplePlaceholdersPerLine.json')) 191 | .expect(200, done); 192 | }) 193 | }); 194 | 195 | 196 | // ---- TodoApp project ---- 197 | 198 | const todoAppProjectDir = path.join(__dirname, './data/projects/todoapp/'); 199 | describe('setting projectDir to Todo App', function(){ 200 | it('responds with the watched project directory', function(done){ 201 | request(app) 202 | .post(url) 203 | .set('Content-Type', 'application/json') 204 | .send({ command: 'setProjectDir', projectDir: todoAppProjectDir}) 205 | .expect(JSON.stringify({projectDir: todoAppProjectDir })) 206 | .expect(200, done); 207 | }) 208 | }); 209 | 210 | const getTodoAppSchemaText = fs.readFileSync(require.resolve('./data/projects/todoapp/todoAppExpectedSchema.txt'), 'utf-8'); 211 | describe('getSchema with TodoApp project dir set', function(){ 212 | it('responds with the TodoApp schema', function(done){ 213 | request(app) 214 | .post(url) 215 | .set('Content-Type', 'application/json') 216 | .send({ command: 'getSchema'}) 217 | .expect(getTodoAppSchemaText) 218 | .expect(200, done); 219 | }) 220 | }); 221 | 222 | describe('getTokens for schema buffer', function(){ 223 | it('responds with expected tokens', function(done){ 224 | request(app) 225 | .post(url) 226 | .set('Content-Type', 'application/json') 227 | .send({ command: 'getTokens', buffer: getTodoAppSchemaText}) 228 | .expect(require('./data/projects/todoapp/getSchemaTokens.json')) 229 | .expect(200, done); 230 | }) 231 | }); 232 | 233 | const getRelayAnnotationsGraphQL = fs.readFileSync(require.resolve('./data/projects/todoapp/getAnnotations.graphql'), 'utf-8'); 234 | describe('Relay getAnnotations', function(){ 235 | it('responds with filtered annotations', function(done){ 236 | request(app) 237 | .post(url) 238 | .set('Content-Type', 'application/json') 239 | .send({ command: 'getAnnotations', buffer: getRelayAnnotationsGraphQL, env: 'relay'}) 240 | .expect({ annotations: []}) 241 | .expect(200, done); 242 | }) 243 | }); 244 | 245 | const getApolloAnnotationsGraphQL = fs.readFileSync(require.resolve('./data/projects/todoapp/getApolloAnnotations.graphql'), 'utf-8'); 246 | describe('Apollo getAnnotations', function(){ 247 | it('responds with filtered annotations', function(done){ 248 | request(app) 249 | .post(url) 250 | .set('Content-Type', 'application/json') 251 | .send({ command: 'getAnnotations', buffer: getApolloAnnotationsGraphQL, env: 'apollo'}) 252 | .expect({ annotations: []}) 253 | .expect(200, done); 254 | }) 255 | }); 256 | 257 | const getLokkaAnnotationsGraphQL = fs.readFileSync(require.resolve('./data/projects/todoapp/getLokkaAnnotations.graphql'), 'utf-8'); 258 | describe('Lokka getAnnotations', function(){ 259 | it('responds with filtered annotations', function(done){ 260 | request(app) 261 | .post(url) 262 | .set('Content-Type', 'application/json') 263 | .send({ command: 'getAnnotations', buffer: getLokkaAnnotationsGraphQL, env: 'lokka'}) 264 | .expect({ annotations: []}) 265 | .expect(200, done); 266 | }) 267 | }); 268 | 269 | // ---- TodoApp Relay Modern project ---- 270 | 271 | const todoAppModernProjectDir = path.join(__dirname, './data/projects/todoapp-modern/'); 272 | describe('setting projectDir to Todo App modern', function(){ 273 | it('responds with the watched project directory', function(done){ 274 | request(app) 275 | .post(url) 276 | .set('Content-Type', 'application/json') 277 | .send({ command: 'setProjectDir', projectDir: todoAppModernProjectDir}) 278 | .expect(JSON.stringify({projectDir: todoAppModernProjectDir })) 279 | .expect(200, done); 280 | }) 281 | }); 282 | 283 | const getTodoAppModernSchemaText = fs.readFileSync(require.resolve('./data/projects/todoapp-modern/todoAppModernExpectedSchema.txt'), 'utf-8'); 284 | describe('getSchema with TodoApp modern project dir set', function(){ 285 | it('responds with the TodoApp schema', function(done){ 286 | request(app) 287 | .post(url) 288 | .set('Content-Type', 'application/json') 289 | .send({ command: 'getSchema'}) 290 | .expect(getTodoAppModernSchemaText) 291 | .expect(200, done); 292 | }) 293 | }); 294 | --------------------------------------------------------------------------------