├── .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 |
5 |
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 | 
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 |
--------------------------------------------------------------------------------