From b9dec272d89d9c590727fd10d62e4a47e0317fc7 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 24 Aug 2015 01:59:14 -0700 Subject: [PATCH] GraphiQL An interactive in-browser GraphQL IDE. --- .eslintrc | 211 ++++++++ .flowconfig | 12 + .gitignore | 12 + LICENSE | 26 + README.md | 44 ++ css/app.css | 466 ++++++++++++++++++ css/codemirror.css | 325 ++++++++++++ css/foldgutter.css | 20 + css/lint.css | 73 +++ css/show-hint.css | 67 +++ example/README.md | 7 + example/build.sh | 7 + example/index.html | 37 ++ example/index.js | 21 + example/package.json | 18 + example/server.js | 160 ++++++ package.json | 73 +++ resources/build.sh | 7 + resources/checkgit.sh | 27 + resources/mocha-bootload.js | 64 +++ src/codemirror/__tests__/graphql-hint-test.js | 129 +++++ src/codemirror/__tests__/graphql-lint-test.js | 82 +++ src/codemirror/__tests__/graphql-mode-test.js | 62 +++ src/codemirror/__tests__/kitchen-sink.graphql | 39 ++ src/codemirror/__tests__/testSchema.js | 147 ++++++ src/codemirror/hint/graphql-hint.js | 432 ++++++++++++++++ src/codemirror/hint/lexicalDistance.js | 58 +++ src/codemirror/lint/graphql-lint.js | 54 ++ src/codemirror/lint/json-lint.js | 37 ++ src/codemirror/lint/jsonLint.js | 241 +++++++++ src/codemirror/mode/graphql-mode.js | 406 +++++++++++++++ src/components/DocExplorer.js | 162 ++++++ src/components/ExecuteButton.js | 47 ++ src/components/QueryEditor.js | 160 ++++++ src/components/ResultViewer.js | 68 +++ src/components/VariableEditor.js | 100 ++++ src/index.js | 11 + src/utility/fillLeafs.js | 163 ++++++ 38 files changed, 4075 insertions(+) create mode 100644 .eslintrc create mode 100644 .flowconfig create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 css/app.css create mode 100644 css/codemirror.css create mode 100644 css/foldgutter.css create mode 100644 css/lint.css create mode 100644 css/show-hint.css create mode 100644 example/README.md create mode 100644 example/build.sh create mode 100644 example/index.html create mode 100644 example/index.js create mode 100644 example/package.json create mode 100644 example/server.js create mode 100644 package.json create mode 100644 resources/build.sh create mode 100644 resources/checkgit.sh create mode 100644 resources/mocha-bootload.js create mode 100644 src/codemirror/__tests__/graphql-hint-test.js create mode 100644 src/codemirror/__tests__/graphql-lint-test.js create mode 100644 src/codemirror/__tests__/graphql-mode-test.js create mode 100644 src/codemirror/__tests__/kitchen-sink.graphql create mode 100644 src/codemirror/__tests__/testSchema.js create mode 100644 src/codemirror/hint/graphql-hint.js create mode 100644 src/codemirror/hint/lexicalDistance.js create mode 100644 src/codemirror/lint/graphql-lint.js create mode 100644 src/codemirror/lint/json-lint.js create mode 100644 src/codemirror/lint/jsonLint.js create mode 100644 src/codemirror/mode/graphql-mode.js create mode 100644 src/components/DocExplorer.js create mode 100644 src/components/ExecuteButton.js create mode 100644 src/components/QueryEditor.js create mode 100644 src/components/ResultViewer.js create mode 100644 src/components/VariableEditor.js create mode 100644 src/index.js create mode 100644 src/utility/fillLeafs.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000000..54d836c54a4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,211 @@ +{ + "parser": "babel-eslint", + "plugins": [ "react" ], + + "env": { + "browser": true, + "es6": true, + "node": true + }, + + "ecmaFeatures": { + "arrowFunctions": true, + "binaryLiterals": true, + "blockBindings": true, + "classes": true, + "defaultParams": true, + "destructuring": true, + "experimentalObjectRestSpread": true, + "forOf": true, + "generators": true, + "globalReturn": true, + "jsx": true, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralDuplicateProperties": true, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "octalLiterals": true, + "regexUFlag": true, + "regexYFlag": true, + "restParams": true, + "spread": true, + "superInFunctions": true, + "templateStrings": true, + "unicodeCodePointEscapes": true + }, + + "rules": { + "array-bracket-spacing": [2, "always", {"arraysInArrays": false}], + "arrow-parens": [0, "as-needed"], + "arrow-spacing": 2, + "block-scoped-var": 0, + "brace-style": [2, "1tbs", {"allowSingleLine": true}], + "callback-return": 2, + "camelcase": [2, {"properties": "always"}], + "comma-dangle": 0, + "comma-spacing": 0, + "comma-style": [2, "last"], + "complexity": 0, + "computed-property-spacing": [2, "never"], + "consistent-return": 0, + "consistent-this": 0, + "curly": [2, "all"], + "default-case": 0, + "dot-location": [2, "property"], + "dot-notation": 0, + "eol-last": 2, + "eqeqeq": 2, + "func-names": 0, + "func-style": 0, + "generator-star-spacing": [0, {"before": true, "after": false}], + "guard-for-in": 2, + "handle-callback-err": [2, "error"], + "id-length": 0, + "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], + "indent": [2, 2, {"SwitchCase": 1}], + "init-declarations": 0, + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "linebreak-style": 2, + "lines-around-comment": 0, + "max-depth": 0, + "max-len": [2, 80, 4], + "max-nested-callbacks": 0, + "max-params": 0, + "max-statements": 0, + "new-cap": 0, + "new-parens": 2, + "newline-after-var": 0, + "no-alert": 2, + "no-array-constructor": 2, + "no-bitwise": 0, + "no-caller": 2, + "no-catch-shadow": 0, + "no-class-assign": 2, + "no-cond-assign": 2, + "no-console": 1, + "no-const-assign": 2, + "no-constant-condition": 2, + "no-continue": 0, + "no-control-regex": 0, + "no-debugger": 1, + "no-delete-var": 2, + "no-div-regex": 2, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-else-return": 2, + "no-empty": 2, + "no-empty-character-class": 2, + "no-empty-label": 2, + "no-eq-null": 0, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implicit-coercion": 2, + "no-implied-eval": 2, + "no-inline-comments": 0, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-invalid-this": 0, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 0, + "no-lone-blocks": 2, + "no-lonely-if": 2, + "no-loop-func": 0, + "no-mixed-requires": [2, true], + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": 0, + "no-native-reassign": 0, + "no-negated-in-lhs": 2, + "no-nested-ternary": 0, + "no-new": 2, + "no-new-func": 0, + "no-new-object": 2, + "no-new-require": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 2, + "no-path-concat": 2, + "no-plusplus": 0, + "no-process-env": 0, + "no-process-exit": 0, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-restricted-modules": 0, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 0, + "no-sequences": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-sync": 2, + "no-ternary": 0, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 0, + "no-underscore-dangle": 0, + "no-unexpected-multiline": 2, + "no-unneeded-ternary": 2, + "no-unreachable": 2, + "no-unused-expressions": 2, + "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], + "no-use-before-define": 0, + "no-useless-call": 2, + "no-var": 0, + "no-void": 2, + "no-warning-comments": 0, + "no-with": 2, + "object-curly-spacing": [0, "always"], + "object-shorthand": [2, "always"], + "one-var": [2, "never"], + "operator-assignment": [2, "always"], + "operator-linebreak": [2, "after"], + "padded-blocks": 0, + "prefer-const": 0, + "prefer-reflect": 0, + "prefer-spread": 0, + "quote-props": [2, "as-needed"], + "quotes": [2, "single"], + "radix": 2, + "require-yield": 2, + "semi": [2, "always"], + "semi-spacing": [2, {"before": false, "after": true}], + "sort-vars": 0, + "space-after-keywords": [2, "always"], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], + "space-in-parens": 0, + "space-infix-ops": [2, {"int32Hint": false}], + "space-return-throw-case": 2, + "space-unary-ops": [2, {"words": true, "nonwords": false}], + "spaced-comment": [2, "always"], + "strict": 0, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "vars-on-top": 0, + "wrap-iife": 2, + "wrap-regex": 0, + "yoda": [2, "never", {"exceptRange": true}] + } +} diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 00000000000..f04f2f6076d --- /dev/null +++ b/.flowconfig @@ -0,0 +1,12 @@ +[ignore] +.*/css/.* +.*/dist/.* +.*/coverage/.* +.*/resources/.* +.*/node_modules/.* + +[include] + +[libs] + +[options] diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..e57a2f595e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.swp +*~ +.*.haste_cache.* +.DS_Store +npm-debug.log + +node_modules +coverage +dist +graphiql.js +graphiql.min.js +graphiql.css diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000000..ad274db4ece --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +LICENSE AGREEMENT For GraphiQL software + +Facebook, Inc. (“Facebook”) owns all right, title and interest, including all +intellectual property and other proprietary rights, in and to the GraphiQL +software. Subject to your compliance with these terms, you are hereby granted a +non-exclusive, worldwide, royalty-free copyright license to (1) use and copy the +GraphiQL software; and (2) reproduce and distribute the GraphiQL software as +part of your own software (“Your Software”). Facebook reserves all rights not +expressly granted to you in this license agreement. + +THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO +EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICES, DIRECTORS OR EMPLOYEES BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You will include in Your Software (e.g., in the file(s), documentation or other +materials accompanying your software): (1) the disclaimer set forth above; (2) +this sentence; and (3) the following copyright notice: + +Copyright (c) 2015, Facebook, Inc. All rights reserved. diff --git a/README.md b/README.md new file mode 100644 index 00000000000..4f2a84efb86 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +GraphiQL +======== + +*/ˈɡrafək(ə)l/* An interactive in-browser GraphQL IDE. + + +### Getting started + +``` +npm install --save graphiql +``` + +GraphiQL provides a React component responsible for rendering the UI, which +should be provided with a function for fetching from GraphQL, we recommend using +the [fetch](https://fetch.spec.whatwg.org/) standard API. + +```js +import React from 'react'; +import GraphiQL from 'graphiql'; +import fetch from 'isomorphic-fetch'; + +function graphQLFetcher(graphQLParams) { + return fetch(window.location.origin + '/graphql', { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(graphQLParams), + }).then(response => response.json()); +} + +React.render(, document.body); +``` + +Build for the web with [webpack](http://webpack.github.io/) or +[browserify](http://browserify.org/), or use the pre-bundled graphiql.js file. +See the example in the git repository to see how to use the pre-bundled file. + + +### Features + +* Syntax highlighting +* Intelligent type ahead of fields, arguments, types, and more. +* Real-time error highlighting and reporting. +* Automatic query completion. +* Run and inspect query results diff --git a/css/app.css b/css/app.css new file mode 100644 index 00000000000..e56b79799d9 --- /dev/null +++ b/css/app.css @@ -0,0 +1,466 @@ +html, body { + height: 100%; + margin: 0; + overflow: hidden; + width: 100%; +} + +#graphiql-container { + color: #141823; + width: 100%; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + height: 100%; + font-family: helvetica, arial, sans-serif; + font-size: 14px; +} + +#graphiql-container .title { + font-size: 18px; +} + +#graphiql-container .title em { + font-family: georgia; + font-size: 19px; +} + +#graphiql-container .topBar { + background: -webkit-linear-gradient(#f7f7f7, #e2e2e2); + background: linear-gradient(#f7f7f7, #e2e2e2); + border-bottom: solid 1px #d0d0d0; + cursor: default; + height: 34px; + padding: 7px 14px 6px; + -webkit-user-select: none; + user-select: none; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + align-items: center; +} + +#graphiql-container .editorBar { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: row; + flex-direction: row; + -webkit-flex: 1; + flex: 1; +} + +#graphiql-container .queryWrap { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + flex: 1; +} + +#graphiql-container .resultWrap { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + -webkit-flex: 1; + flex: 1; + border-left: solid 1px #e0e0e0; +} + +#graphiql-container .query-editor { + -webkit-flex: 1; + flex: 1; + position: relative; +} + +#graphiql-container .variable-editor { + -webkit-flex: 0; + flex: 0; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + position: relative; +} + +#graphiql-container .variable-editor-title { + background: #eeeeee; + border-bottom: solid 1px #d6d6d6; + border-top: solid 1px #e0e0e0; + color: #777; + font-variant: small-caps; + font-weight: bold; + letter-spacing: 1px; + line-height: 14px; + padding: 6px 0 8px 43px; + text-transform: lowercase; + -webkit-user-select: none; + user-select: none; +} + +#graphiql-container .codemirrorWrap { + -webkit-flex: 1; + flex: 1; + position: relative; +} + +#graphiql-container .result-window { + -webkit-flex: 1; + flex: 1; + position: relative; +} + +#graphiql-container .docExplorerWrap { + position: absolute; + top: 0; + right: 0; + height: 100%; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); + z-index: 2; +} + +#graphiql-container .doc-explorer { + height: 100%; +} + +#graphiql-container .doc-explorer-title-bar { + position: relative; + height: 34px; + border-left: solid 1px #d6d6d6; + letter-spacing: 1px; + line-height: 14px; + padding: 7px 14px 6px; + -webkit-user-select: none; + user-select: none; +} + +#graphiql-container .doc-explorer-title { + padding: 10px 0; + font-size: 14px; + text-align: right; +} + +#graphiql-container .doc-explorer-toggle-button { + position: relative; + float: left; + padding: 1px 8px; + line-height: 22px; + background-color: #f6f7f8; + border-radius: 2px; + border: 1px solid; + border-color: #cccccc #c5c6c8 #b6b7b9; + font-weight: bold; + top: 50%; + -webkit-transform: translate(0, -50%); + transform: translate(0, -50%); +} + +#graphiql-container .doc-explorer-contents { + position: relative; + height: 100%; + background-color: #ffffff; + border-left: 1px solid #d6d6d6; + border-top: 1px solid #d6d6d6; + padding: 14px 14px; + overflow: hidden; + font-family: 'lucida grande', tahoma, verdana, arial, sans-serif; + font-size: 12px; +} + +#graphiql-container .doc-call-def { + margin-bottom: 20px; +} + +#graphiql-container .doc-type-title { + border-bottom: 1px solid #e0e0e0; + display: block; + padding: 10px 0; + margin-bottom: 10px; + font-size: 14px; + font-weight: bold; +} + +#graphiql-container .footer { + background: #f6f7f8; + border-left: solid 1px #e0e0e0; + border-top: solid 1px #e0e0e0; + margin-left: 12px; + position: relative; +} + +#graphiql-container .footer:before { + background: #eeeeee; + bottom: 0; + content: " "; + left: -13px; + position: absolute; + top: -1px; + width: 12px; +} + +#graphiql-container .result-window .CodeMirror { + background: #f6f7f8; +} + +#graphiql-container .result-window .CodeMirror-gutters { + background-color: #eeeeee; + border-color: #e0e0e0; + cursor: col-resize; +} + +#graphiql-container .result-window .CodeMirror-foldgutter, +#graphiql-container .result-window .CodeMirror-foldgutter-open:after, +#graphiql-container .result-window .CodeMirror-foldgutter-folded:after { + padding-left: 3px; +} + +#graphiql-container .execute-button { + background: -webkit-linear-gradient(#fdfdfd, #d2d3d6); + background: linear-gradient(#fdfdfd, #d2d3d6); + border: solid 1px rgba(0,0,0,0.25); + border-radius: 17px; + box-shadow: 0 1px 0 #fff; + cursor: pointer; + fill: #444; + float: left; + height: 34px; + margin: 0 14px 0 28px; + padding: 0; + width: 34px; +} + +#graphiql-container .execute-button:active { + background: -webkit-linear-gradient(#e6e6e6, #c0c0c0); + background: linear-gradient(#e6e6e6, #c0c0c0); + box-shadow: + 0 1px 0 #fff, + inset 0 0 2px rgba(0, 0, 0, 0.3), + inset 0 0 6px rgba(0, 0, 0, 0.2); +} + +#graphiql-container .execute-button:focus { + outline: 0; +} + +#graphiql-container .CodeMirror-scroll { + -webkit-overflow-scrolling: touch; +} + +#graphiql-container .CodeMirror { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + font-size: 13px; + font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; + color: #141823; +} + +#graphiql-container .CodeMirror-lines { + padding: 20px 0; +} + +.CodeMirror-hint-information .content { + -webkit-box-orient: vertical; + color: #141823; + display: -webkit-box; + font-family: helvetica, arial, sans-serif; + font-size: 13px; + -webkit-line-clamp: 3; + line-height: 16px; + max-height: 48px; + overflow: hidden; + text-overflow: -o-ellipsis-lastline; +} + +.CodeMirror-hint-information .content p:first-child { + margin-top: 0; +} + +.CodeMirror-hint-information .content p:last-child { + margin-bottom: 0; +} + +.CodeMirror-hint-information .infoType { + color: #30a; + margin-right: 0.5em; + display: inline; + cursor: pointer; +} + +.autoInsertedLeaf.cm-property { + padding: 2px 4px 1px; + margin: -2px -4px -1px; + border-radius: 2px; + border-bottom: solid 2px rgba(255, 255, 255, 0); + -webkit-animation-duration: 6s; + -moz-animation-duration: 6s; + animation-duration: 6s; + -webkit-animation-name: insertionFade; + -moz-animation-name: insertionFade; + animation-name: insertionFade; +} + +@-moz-keyframes insertionFade { + from, to { + background: rgba(255, 255, 255, 0); + border-color: rgba(255, 255, 255, 0); + } + + 15%, 85% { + background: #fbffc9; + border-color: #f0f3c0; + } +} + +@-webkit-keyframes insertionFade { + from, to { + background: rgba(255, 255, 255, 0); + border-color: rgba(255, 255, 255, 0); + } + + 15%, 85% { + background: #fbffc9; + border-color: #f0f3c0; + } +} + +@keyframes insertionFade { + from, to { + background: rgba(255, 255, 255, 0); + border-color: rgba(255, 255, 255, 0); + } + + 15%, 85% { + background: #fbffc9; + border-color: #f0f3c0; + } +} + +div.CodeMirror-lint-tooltip { + background-color: white; + color: #141823; + border: 0; + border-radius: 2px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + font-family: helvetica, arial, sans-serif; + font-size: 13px; + line-height: 16px; + padding: 6px 10px; + opacity: 0; + transition: opacity 0.15s; + -moz-transition: opacity 0.15s; + -webkit-transition: opacity 0.15s; + -o-transition: opacity 0.15s; + -ms-transition: opacity 0.15s; +} + +div.CodeMirror-lint-message-error, div.CodeMirror-lint-message-warning { + padding-left: 23px; +} + +/* COLORS */ + +#graphiql-container .CodeMirror-foldmarker { + border-radius: 4px; + background: #08f; + background: -webkit-linear-gradient(#43A8FF, #0F83E8); + background: linear-gradient(#43A8FF, #0F83E8); + + color: white; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), inset 0 0 0 1px rgba(0, 0, 0, 0.1); + font-family: arial; + line-height: 0; + padding: 0px 4px 1px; + font-size: 12px; + margin: 0 3px; + text-shadow: 0 -1px rgba(0, 0, 0, 0.1); +} + +#graphiql-container div.CodeMirror span.CodeMirror-matchingbracket { + color: #555; + text-decoration: underline; +} + +#graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket { + color: #f00; +} + +/* Comment */ +.cm-comment { + color: #999; +} + +/* Punctuation */ +.cm-punctuation { + color: #555; +} + +/* Keyword */ +.cm-keyword { + color: #B11A04; +} + +/* OperationName, FragmentName */ +.cm-def { + color: #D2054E; +} + +/* FieldName */ +.cm-property { + color: #1F61A0; +} + +/* FieldAlias */ +.cm-qualifier { + color: #1C92A9; +} + +/* ArgumentName and ObjectFieldName */ +.cm-attribute { + color: #8B2BB9; +} + +/* Number */ +.cm-number { + color: #2882F9; +} + +/* String */ +.cm-string { + color: #D64292; +} + +/* Boolean */ +.cm-builtin { + color: #D47509; +} + +/* EnumValue */ +.cm-string-2 { + color: #0B7FC7; +} + +/* Variable */ +.cm-variable { + color: #397D13; +} + +/* Directive */ +.cm-meta { + color: #B33086; +} + +/* Type */ +.cm-atom { + color: #CA9800; +} diff --git a/css/codemirror.css b/css/codemirror.css new file mode 100644 index 00000000000..ceacd130479 --- /dev/null +++ b/css/codemirror.css @@ -0,0 +1,325 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +@-moz-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@-webkit-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} + +/* Can style cursor different in overwrite (non-insert) mode */ +div.CodeMirror-overwrite div.CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + margin-bottom: -30px; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + height: 100%; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + border-right: none; + width: 0; +} + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror ::selection { background: #d7d4f0; } +.CodeMirror ::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/css/foldgutter.css b/css/foldgutter.css new file mode 100644 index 00000000000..ad19ae2d3ee --- /dev/null +++ b/css/foldgutter.css @@ -0,0 +1,20 @@ +.CodeMirror-foldmarker { + color: blue; + text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; + font-family: arial; + line-height: .3; + cursor: pointer; +} +.CodeMirror-foldgutter { + width: .7em; +} +.CodeMirror-foldgutter-open, +.CodeMirror-foldgutter-folded { + cursor: pointer; +} +.CodeMirror-foldgutter-open:after { + content: "\25BE"; +} +.CodeMirror-foldgutter-folded:after { + content: "\25B8"; +} diff --git a/css/lint.css b/css/lint.css new file mode 100644 index 00000000000..414a9a0e066 --- /dev/null +++ b/css/lint.css @@ -0,0 +1,73 @@ +/* The lint marker gutter */ +.CodeMirror-lint-markers { + width: 16px; +} + +.CodeMirror-lint-tooltip { + background-color: infobackground; + border: 1px solid black; + border-radius: 4px 4px 4px 4px; + color: infotext; + font-family: monospace; + font-size: 10pt; + overflow: hidden; + padding: 2px 5px; + position: fixed; + white-space: pre; + white-space: pre-wrap; + z-index: 100; + max-width: 600px; + opacity: 0; + transition: opacity .4s; + -moz-transition: opacity .4s; + -webkit-transition: opacity .4s; + -o-transition: opacity .4s; + -ms-transition: opacity .4s; +} + +.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { + background-position: left bottom; + background-repeat: repeat-x; +} + +.CodeMirror-lint-mark-error { + background-image: + url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") + ; +} + +.CodeMirror-lint-mark-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; + display: inline-block; + height: 16px; + width: 16px; + vertical-align: middle; + position: relative; +} + +.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { + padding-left: 18px; + background-position: top left; + background-repeat: no-repeat; +} + +.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); +} + +.CodeMirror-lint-marker-multiple { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); + background-repeat: no-repeat; + background-position: right bottom; + width: 100%; height: 100%; +} diff --git a/css/show-hint.css b/css/show-hint.css new file mode 100644 index 00000000000..961a2870b4a --- /dev/null +++ b/css/show-hint.css @@ -0,0 +1,67 @@ +.CodeMirror-hints { + background: white; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + font-family: 'Consolas'; + font-size: 13px; + list-style: none; + margin: 0; + margin-left: -6px; + max-height: 14.5em; + overflow-y: auto; + overflow: hidden; + padding: 0; + position: absolute; + z-index: 10; +} + +.CodeMirror-hints-wrapper { + background: white; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); + margin-left: -6px; + position: absolute; + z-index: 10; +} + +.CodeMirror-hints-wrapper .CodeMirror-hints { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + position: relative; + margin-left: 0; + z-index: 0; +} + +.CodeMirror-hint { + border-top: solid 1px #f7f7f7; + color: #141823; + cursor: pointer; + margin: 0; + max-width: 300px; + overflow: hidden; + padding: 2px 6px; + white-space: pre; +} + +li.CodeMirror-hint-active { + background-color: #08f; + border-top-color: white; + color: white; +} + +.CodeMirror-hint-information { + border-top: solid 1px #c0c0c0; + max-width: 300px; + padding: 4px 6px; + position: relative; + z-index: 1; +} + +.CodeMirror-hint-information:first-child { + border-bottom: solid 1px #c0c0c0; + border-top: none; + margin-bottom: -1px; +} diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000000..f116046bff9 --- /dev/null +++ b/example/README.md @@ -0,0 +1,7 @@ +Example GraphiQL Install +======================== + +1. Navigate to this directory in Terminal +2. `npm install` +3. `npm start` +4. Open your browser to [http://localhost:8080/]() diff --git a/example/build.sh b/example/build.sh new file mode 100644 index 00000000000..7d5712d117e --- /dev/null +++ b/example/build.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +rm -rf dist/ && mkdir -p dist/ && +babel server.js --optional runtime -o dist/server.js && +cp node_modules/graphiql/graphiql.min.js dist/graphiql.min.js && +cp node_modules/graphiql/graphiql.css dist/graphiql.css && +cat index.html > dist/index.html diff --git a/example/index.html b/example/index.html new file mode 100644 index 00000000000..4c7cd97e3e4 --- /dev/null +++ b/example/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + Loading... + + + diff --git a/example/index.js b/example/index.js new file mode 100644 index 00000000000..81283474406 --- /dev/null +++ b/example/index.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/* global React, GraphiQL */ +import 'whatwg-fetch'; /* global fetch */ + +function graphQLFetcher(graphQLParams) { + return fetch(window.location.origin + '/graphql', { + method: 'post', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(graphQLParams), + }).then(response => response.json()); +} + +React.render(, document.body); diff --git a/example/package.json b/example/package.json new file mode 100644 index 00000000000..08890691334 --- /dev/null +++ b/example/package.json @@ -0,0 +1,18 @@ +{ + "description": "An example using GraphiQL", + "scripts": { + "start": "node dist/server.js", + "prestart": "npm run build", + "build": ". build.sh" + }, + "dependencies": { + "babel-runtime": "^5.8.20", + "express": "^4.12.4", + "express-graphql": "^0.3.0", + "graphiql": "../", + "graphql": "^0.4.2" + }, + "devDependencies": { + "babel": "^5.8.21" + } +} diff --git a/example/server.js b/example/server.js new file mode 100644 index 00000000000..7a21c565fd1 --- /dev/null +++ b/example/server.js @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +import express from 'express'; +import graphqlHTTP from 'express-graphql'; +import { + GraphQLSchema, + GraphQLObjectType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLBoolean, + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLID, + GraphQLList, +} from 'graphql'; + +var app = express(); +app.use(express.static(__dirname)); +app.use('/graphql', graphqlHTTP(() => ({ + schema: TestSchema +}))); +app.listen(8080); +console.log('Started on http://localhost:8080/'); + +// Schema defined here + + +// Test Schema + +var TestEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + RED: {}, + GREEN: {}, + BLUE: {}, + } +}); + +var TestInputObject = new GraphQLInputObjectType({ + name: 'TestInput', + fields: () => ({ + string: { type: GraphQLString }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + // List + listString: { type: new GraphQLList(GraphQLString) }, + listInt: { type: new GraphQLList(GraphQLInt) }, + listFloat: { type: new GraphQLList(GraphQLFloat) }, + listBoolean: { type: new GraphQLList(GraphQLBoolean) }, + listID: { type: new GraphQLList(GraphQLID) }, + listEnum: { type: new GraphQLList(TestEnum) }, + listObject: { type: new GraphQLList(TestInputObject) }, + }) +}); + +var UnionFirst = new GraphQLObjectType({ + name: 'First', + fields: () => ({ + first: { + type: TestType, + resolve: () => ({}) + } + }) +}); + +var UnionSecond = new GraphQLObjectType({ + name: 'Second', + fields: () => ({ + second: { + type: TestType, + resolve: () => ({}) + } + }) +}); + +var TestUnion = new GraphQLUnionType({ + name: 'TestUnion', + types: [ UnionFirst, UnionSecond ], + resolveType() { + return UnionFirst; + } +}); + +var TestType = new GraphQLObjectType({ + name: 'Test', + fields: () => ({ + test: { + type: TestType, + resolve: () => ({}) + }, + union: { + type: TestUnion, + resolve: () => ({}) + }, + id: { + type: GraphQLInt, + resolve: () => ({}) + }, + isTest: { + type: GraphQLBoolean, + resolve: () => { + return true; + } + }, + hasArgs: { + type: GraphQLString, + resolve(value, args) { + return JSON.stringify(args); + }, + args: { + string: { type: GraphQLString }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + // List + listString: { type: new GraphQLList(GraphQLString) }, + listInt: { type: new GraphQLList(GraphQLInt) }, + listFloat: { type: new GraphQLList(GraphQLFloat) }, + listBoolean: { type: new GraphQLList(GraphQLBoolean) }, + listID: { type: new GraphQLList(GraphQLID) }, + listEnum: { type: new GraphQLList(TestEnum) }, + listObject: { type: new GraphQLList(TestInputObject) }, + } + }, + }) +}); + +var TestMutationType = new GraphQLObjectType({ + name: 'MutationType', + description: 'This is a simple mutation type', + fields: { + setString: { + type: GraphQLString, + description: 'Set the string field', + args: { + value: { type: GraphQLString } + } + } + } +}); + +const TestSchema = new GraphQLSchema({ + query: TestType, + mutation: TestMutationType +}); diff --git a/package.json b/package.json new file mode 100644 index 00000000000..fb301a22465 --- /dev/null +++ b/package.json @@ -0,0 +1,73 @@ +{ + "name": "graphiql", + "version": "0.0.0", + "description": "An interactive in-browser GraphQL IDE.", + "contributors": [ + "Hyohyeon Jeong ", + "Lee Byron (http://leebyron.com/)" + ], + "license": "See LICENSE", + "main": "dist/index.js", + "files": [ + "dist", + "graphiql.js", + "graphiql.min.js", + "graphiql.css", + "README.md", + "LICENSE" + ], + "babel": { + "optional": [ + "runtime" + ], + "loose": [ + "es6.classes", + "es6.destructuring", + "es6.modules", + "es6.properties.computed", + "es6.spread", + "es6.templateLiterals" + ] + }, + "browserify-shim": { + "react": "global:React" + }, + "options": { + "mocha": "--full-trace --require resources/mocha-bootload src/**/*-test.js" + }, + "scripts": { + "test": "npm run lint && npm run testonly", + "testonly": "mocha $npm_package_options_mocha", + "lint": "eslint src", + "check": "flow check", + "build": ". resources/build.sh", + "preversion": ". ./resources/checkgit.sh && npm test", + "prepublish": "npm run build" + }, + "dependencies": { + "babel-runtime": "^5.8.20", + "codemirror": "^5.6.0", + "marked": "^0.3.5", + "react": "^0.13.3" + }, + "peerDependencies": { + "graphql": "^0.4.2" + }, + "devDependencies": { + "babel": "5.8.21", + "babel-core": "5.8.22", + "babel-eslint": "4.0.10", + "browserify": "11.0.1", + "browserify-shim": "3.8.10", + "chai": "3.2.0", + "chai-subset": "1.0.1", + "eslint": "1.2.1", + "eslint-plugin-react": "3.2.3", + "flow-bin": "0.14.0", + "graphql": "0.4.2", + "jsdom": "3.0.0", + "mocha": "2.2.5", + "uglify-js": "^2.4.24", + "uglifyify": "^3.0.1" + } +} diff --git a/resources/build.sh b/resources/build.sh new file mode 100644 index 00000000000..4302e6d95cc --- /dev/null +++ b/resources/build.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +rm -rf dist/ && mkdir -p dist/ && +babel src --ignore __tests__ --out-dir dist/ && +browserify -g browserify-shim -s GraphiQL dist/index.js > graphiql.js && +browserify -g browserify-shim -g uglifyify -s GraphiQL dist/index.js | uglifyjs -c --screw-ie8 > graphiql.min.js && +cat css/*.css > graphiql.css diff --git a/resources/checkgit.sh b/resources/checkgit.sh new file mode 100644 index 00000000000..f6798e23d40 --- /dev/null +++ b/resources/checkgit.sh @@ -0,0 +1,27 @@ +# +# This script determines if current git state is the up to date master. If so +# it exits normally. If not it prompts for an explicit continue. This script +# intends to protect from versioning for NPM without first pushing changes +# and including any changes on master. +# + +# First fetch to ensure git is up to date. Fail-fast if this fails. +git fetch; +if [[ $? -ne 0 ]]; then exit 1; fi; + +# Extract useful information. +GITBRANCH=$(git branch -v 2> /dev/null | sed '/^[^*]/d'); +GITBRANCHNAME=$(echo "$GITBRANCH" | sed 's/* \([A-Za-z0-9_\-]*\).*/\1/'); +GITBRANCHSYNC=$(echo "$GITBRANCH" | sed 's/* [^[]*.\([^]]*\).*/\1/'); + +# Check if master is checked out +if [ "$GITBRANCHNAME" != "master" ]; then + read -p "Git not on master but $GITBRANCHNAME. Continue? (y|N) " yn; + if [ "$yn" != "y" ]; then exit 1; fi; +fi; + +# Check if branch is synced with remote +if [ "$GITBRANCHSYNC" != "" ]; then + read -p "Git not up to date but $GITBRANCHSYNC. Continue? (y|N) " yn; + if [ "$yn" != "y" ]; then exit 1; fi; +fi; diff --git a/resources/mocha-bootload.js b/resources/mocha-bootload.js new file mode 100644 index 00000000000..3dfe9e2b81c --- /dev/null +++ b/resources/mocha-bootload.js @@ -0,0 +1,64 @@ +/* eslint-disable no-console, object-shorthand */ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +require('babel/register')({ + optional: [ 'runtime', 'es7.asyncFunctions' ] +}); + +var jsdom = require('jsdom'); + +// setup the simplest document possible +var doc = jsdom.jsdom(''); + +// get the window object out of the document +var win = doc.defaultView; + +// set globals for mocha that make access to document and window feel +// natural in the test environment +global.document = doc; +global.window = win; + +global.document.createRange = function () { + return { + setEnd: function () {}, + setStart: function () {}, + getBoundingClientRect: function () { + return { right: 0 }; + } + }; +}; + +// take all properties of the window object and also attach it to the +// mocha global object +propagateToGlobal(win); + +// from mocha-jsdom +// https://github.com/rstacruz/mocha-jsdom/blob/master/index.js#L80 +function propagateToGlobal(window) { + for (var key in window) { + if (!window.hasOwnProperty(key)) { + continue; + } + if (key in global) { + continue; + } + global[key] = window[key]; + } +} + +var chai = require('chai'); + +var chaiSubset = require('chai-subset'); +chai.use(chaiSubset); + +process.on('unhandledRejection', function (error) { + console.error('Unhandled Promise Rejection:'); + console.error(error && error.stack || error); +}); diff --git a/src/codemirror/__tests__/graphql-hint-test.js b/src/codemirror/__tests__/graphql-hint-test.js new file mode 100644 index 00000000000..1b1b56485f5 --- /dev/null +++ b/src/codemirror/__tests__/graphql-hint-test.js @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import CodeMirror from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; +import '../hint/graphql-hint'; +import { TestSchema } from './testSchema'; +import { graphql } from 'graphql'; +import { introspectionQuery, buildClientSchema } from 'graphql/utilities'; + +/* eslint-disable max-len */ + +async function createEditorWithHint() { + return CodeMirror(document.createElement('div'), { + mode: 'graphql', + hintOptions: { + schema: await getClientSchema(), + closeOnUnfocus: false, + completeSingle: false + } + }); +} + +async function getHintSuggestions(queryString, cursor) { + let editor = await createEditorWithHint(); + return new Promise(resolve => { + let graphqlHint = CodeMirror.hint.graphql; + CodeMirror.hint.graphql = (cm, options) => { + let result = graphqlHint(cm, options); + if (result) { + resolve(result); + } + + return result; + }; + + editor.doc.setValue(queryString); + editor.doc.setCursor(cursor); + editor.execCommand('autocomplete'); + }); +} + +async function getClientSchema() { + return await graphql(TestSchema, introspectionQuery) + .then((response) => { + return buildClientSchema(response.data); + }); +} + +function checkSuggestions(source, suggestions) { + var titles = suggestions + .map(suggestion => suggestion.text) + .filter(title => title !== '__schema' && title !== '__type'); + expect(titles).to.deep.equal(source); +} + +describe('graphql-hint', () => { + it('attaches a GraphQL hint function with correct mode/hint options', async () => { + let editor = await createEditorWithHint(); + expect( + editor.getHelpers(editor.getCursor(), 'hint') + ).to.not.have.lengthOf(0); + }); + + it('provides correct initial keywords', async () => { + let suggestions = await getHintSuggestions('', { line: 0, ch: 0 }); + const initialKeywords = [ 'query', 'mutation', 'fragment', '{' ]; + checkSuggestions(initialKeywords, suggestions.list); + }); + + it('provides correct field name suggestions', async () => { + let suggestions = await getHintSuggestions('{ ', { line: 0, ch: 2 }); + const fieldConfig = TestSchema.getQueryType().getFields(); + const fieldNames = Object.keys(fieldConfig); + checkSuggestions(fieldNames, suggestions.list); + }); + + it('provides correct field name suggestion indentation', async () => { + let suggestions = await getHintSuggestions('{\n ', { line: 1, ch: 2 }); + expect(suggestions.from).to.deep.equal({ line: 1, ch: 2 }); + expect(suggestions.to).to.deep.equal({ line: 1, ch: 2 }); + }); + + it('provides correct argument suggestions', async () => { + let suggestions = await getHintSuggestions( + '{ hasArgs ( ', { line: 0, ch: 12 }); + const argumentNames = + TestSchema.getQueryType().getFields().hasArgs.args.map(arg => arg.name); + checkSuggestions(argumentNames, suggestions.list); + }); + + it('provides correct directive suggestions', async () => { + let suggestions = await getHintSuggestions( + '{ test (@', { line: 0, ch: 9 }); + const directiveNames = [ 'include', 'skip' ]; + checkSuggestions(directiveNames, suggestions.list); + }); + + it('provides correct typeCondition suggestions', async () => { + let suggestions = await getHintSuggestions( + '{ union { ... on ', { line: 0, ch: 17 }); + const typeConditionNames = + TestSchema.getQueryType().getFields().union.type + .getPossibleTypes().map(type => type.name); + checkSuggestions(typeConditionNames, suggestions.list); + }); + + it('provides correct ENUM suggestions', async () => { + let suggestions = await getHintSuggestions( + '{ hasArgs (enum: ', { line: 0, ch: 17 }); + const enumNames = + TestSchema.getType('TestEnum').getValues().map(value => value.name); + checkSuggestions(enumNames, suggestions.list); + }); + + it('provides correct testInput suggestions', async () => { + let suggestions = await getHintSuggestions( + '{ hasArgs (object: { ', { line: 0, ch: 21 }); + const testInputNames = Object.keys(TestSchema.getType('TestInput').getFields()); + checkSuggestions(testInputNames, suggestions.list); + }); +}); diff --git a/src/codemirror/__tests__/graphql-lint-test.js b/src/codemirror/__tests__/graphql-lint-test.js new file mode 100644 index 00000000000..8fd8de81207 --- /dev/null +++ b/src/codemirror/__tests__/graphql-lint-test.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import CodeMirror from 'codemirror'; +import 'codemirror/addon/lint/lint'; +import '../lint/graphql-lint'; +import { TestSchema } from './testSchema'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +/* eslint-disable max-len */ + +function createEditorWithLint(lintConfig) { + return CodeMirror(document.createElement('div'), { + mode: 'graphql', + lint: lintConfig ? lintConfig : true + }); +} + +function printLintErrors(queryString) { + let editor = createEditorWithLint({ + schema: TestSchema + }); + + return new Promise((resolve, reject) => { + editor.state.lint.options.onUpdateLinting = (errors) => { + if (errors && errors[0]) { + if (!errors[0].message.match('Unexpected EOF')) { + resolve(errors); + } + } + reject(); + }; + editor.doc.setValue(queryString); + }).then((errors) => { + return errors; + }).catch(() => { + return []; + }); +} + +describe('graphql-lint', () => { + it('attaches a GraphQL lint function with correct mode/lint options', () => { + let editor = createEditorWithLint(); + expect( + editor.getHelpers(editor.getCursor(), 'lint') + ).to.not.have.lengthOf(0); + }); + + it('catches syntax errors', async () => { + expect( + (await printLintErrors(`qeury`))[0].message + ).to.contain( +`Unexpected Name "qeury"` + ); + }); + + it('catches field validation errors', async () => { + expect( + (await printLintErrors(`query queryName { title }`))[0].message + ).to.contain( +`Cannot query field "title" on "Test".` + ); + }); + + var kitchenSink = readFileSync( + join(__dirname, '/kitchen-sink.graphql'), + { encoding: 'utf8' } + ); + + it('returns no syntactic/validation errors after parsing kitchen-sink query', async () => { + let errors = await printLintErrors(kitchenSink); + expect(errors).to.have.lengthOf(0); + }); +}); diff --git a/src/codemirror/__tests__/graphql-mode-test.js b/src/codemirror/__tests__/graphql-mode-test.js new file mode 100644 index 00000000000..106594b7804 --- /dev/null +++ b/src/codemirror/__tests__/graphql-mode-test.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import CodeMirror from 'codemirror'; +import 'codemirror/addon/runmode/runmode'; +import '../mode/graphql-mode'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +describe('graphql-mode', () => { + it('provides correct tokens and styles after parsing', () => { + let queryStr = 'query name { }'; + let tokens = []; + let styles = []; + + CodeMirror.runMode(queryStr, 'graphql', (token, style) => { + if (style) { + tokens.push(token); + styles.push(style); + } + }); + + expect(tokens).to.deep.equal([ + 'query', 'name', '{', '}' + ]); + expect(styles).to.deep.equal([ + 'keyword', 'def', 'punctuation', 'punctuation' + ]); + }); + + it('returns "invalidchar" message when there is no matching token', () => { + CodeMirror.runMode('qauery name', 'graphql', (token, style) => { + if (token.trim()) { + expect(style).to.equal('invalidchar'); + } + }); + + CodeMirror.runMode('query %', 'graphql', (token, style) => { + if (token === '%') { + expect(style).to.equal('invalidchar'); + } + }); + }); + + var kitchenSink = readFileSync( + join(__dirname, '/kitchen-sink.graphql'), + { encoding: 'utf8' } + ); + + it('parses kitchen-sink query without invalidchar', () => { + CodeMirror.runMode(kitchenSink, 'graphql', token => { + expect(token).to.not.equal('invalidchar'); + }); + }); +}); diff --git a/src/codemirror/__tests__/kitchen-sink.graphql b/src/codemirror/__tests__/kitchen-sink.graphql new file mode 100644 index 00000000000..98a4dec9cc8 --- /dev/null +++ b/src/codemirror/__tests__/kitchen-sink.graphql @@ -0,0 +1,39 @@ +# Copyright (c) 2015, Facebook, Inc. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. An additional grant +# of patent rights can be found in the PATENTS file in the same directory. + +query queryName($foo: TestInput, $site: TestEnum = RED) { + testAlias: hasArgs(string: "testString") + ... on Test { + hasArgs( + listEnum: [RED, GREEN, BLUE] + int: 1 + listFloat: [1.23, 1.3e-1, -1.35384e+3] + boolean: true + id: 123 + object: $foo + enum: $site + ) + } + test @include(if: true) { + union { + __typename + } + } + ...frag +} + +mutation mutationName { + setString(value: "newString") +} + +fragment frag on Test { + test @include(if: true) { + union { + __typename + } + } +} diff --git a/src/codemirror/__tests__/testSchema.js b/src/codemirror/__tests__/testSchema.js new file mode 100644 index 00000000000..45d278dd3e9 --- /dev/null +++ b/src/codemirror/__tests__/testSchema.js @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +import { + GraphQLSchema, + GraphQLObjectType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLBoolean, + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLID, + GraphQLList, +} from 'graphql'; + +// Test Schema + +var TestEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + RED: {}, + GREEN: {}, + BLUE: {}, + } +}); + +var TestInputObject = new GraphQLInputObjectType({ + name: 'TestInput', + fields: () => ({ + string: { type: GraphQLString }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + // List + listString: { type: new GraphQLList(GraphQLString) }, + listInt: { type: new GraphQLList(GraphQLInt) }, + listFloat: { type: new GraphQLList(GraphQLFloat) }, + listBoolean: { type: new GraphQLList(GraphQLBoolean) }, + listID: { type: new GraphQLList(GraphQLID) }, + listEnum: { type: new GraphQLList(TestEnum) }, + listObject: { type: new GraphQLList(TestInputObject) }, + }) +}); + +var UnionFirst = new GraphQLObjectType({ + name: 'First', + fields: () => ({ + first: { + type: TestType, + resolve: () => ({}) + } + }) +}); + +var UnionSecond = new GraphQLObjectType({ + name: 'Second', + fields: () => ({ + second: { + type: TestType, + resolve: () => ({}) + } + }) +}); + +var TestUnion = new GraphQLUnionType({ + name: 'TestUnion', + types: [ UnionFirst, UnionSecond ], + resolveType() { + return UnionFirst; + } +}); + +var TestType = new GraphQLObjectType({ + name: 'Test', + fields: () => ({ + test: { + type: TestType, + resolve: () => ({}) + }, + union: { + type: TestUnion, + resolve: () => ({}) + }, + id: { + type: GraphQLInt, + resolve: () => ({}) + }, + isTest: { + type: GraphQLBoolean, + resolve: () => { + return true; + } + }, + hasArgs: { + type: GraphQLString, + resolve(value, args) { + return JSON.stringify(args); + }, + args: { + string: { type: GraphQLString }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + // List + listString: { type: new GraphQLList(GraphQLString) }, + listInt: { type: new GraphQLList(GraphQLInt) }, + listFloat: { type: new GraphQLList(GraphQLFloat) }, + listBoolean: { type: new GraphQLList(GraphQLBoolean) }, + listID: { type: new GraphQLList(GraphQLID) }, + listEnum: { type: new GraphQLList(TestEnum) }, + listObject: { type: new GraphQLList(TestInputObject) }, + } + }, + }) +}); + +var TestMutationType = new GraphQLObjectType({ + name: 'MutationType', + description: 'This is a simple mutation type', + fields: { + setString: { + type: GraphQLString, + description: 'Set the string field', + args: { + value: { type: GraphQLString } + } + } + } +}); + +export const TestSchema = new GraphQLSchema({ + query: TestType, + mutation: TestMutationType +}); diff --git a/src/codemirror/hint/graphql-hint.js b/src/codemirror/hint/graphql-hint.js new file mode 100644 index 00000000000..e5d822749ab --- /dev/null +++ b/src/codemirror/hint/graphql-hint.js @@ -0,0 +1,432 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import CodeMirror from 'codemirror'; +import { + isInputType, + isCompositeType, + isAbstractType, + getNullableType, + getNamedType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLBoolean, +} from 'graphql/type'; +import { + SchemaMetaFieldDef, + TypeMetaFieldDef, + TypeNameMetaFieldDef, +} from 'graphql/type/introspection'; +import lexicalDistance from './lexicalDistance'; + + +CodeMirror.registerHelper('hint', 'graphql', (editor, options) => { + var schema = options.schema; + if (!schema) { + return; + } + + var cur = editor.getCursor(); + var token = editor.getTokenAt(cur); + var typeInfo = getTypeInfo(schema, token.state); + + var state = token.state; + var kind = state.kind; + var step = state.step; + + if (token.type === 'comment') { + return; + } + + // Definition kinds + if (kind === 'Document') { + return hintList(editor, options, cur, token, [ + { text: 'query' }, + { text: 'mutation' }, + { text: 'fragment' }, + { text: '{' }, + ]); + } + + // Field names + if (kind === 'SelectionSet' || kind === 'Field' || kind === 'AliasedField') { + if (typeInfo.parentType) { + var fields; + if (typeInfo.parentType.getFields) { + var fieldObj = typeInfo.parentType.getFields(); + fields = Object.keys(fieldObj).map(fieldName => fieldObj[fieldName]); + } else { + fields = []; + } + if (isAbstractType(typeInfo.parentType)) { + fields.push(TypeNameMetaFieldDef); + } + if (typeInfo.parentType === schema.getQueryType()) { + fields.push(SchemaMetaFieldDef, TypeMetaFieldDef); + } + return hintList(editor, options, cur, token, fields.map(field => ({ + text: field.name, + type: field.type, + description: field.description + }))); + } + } + + // Argument names + if (kind === 'Arguments' || kind === 'Argument' && step === 0) { + var argDefs = typeInfo.argDefs; + if (argDefs) { + return hintList(editor, options, cur, token, argDefs.map(argDef => ({ + text: argDef.name, + type: argDef.type, + description: argDef.description + }))); + } + } + + // Input Object fields + if (kind === 'ObjectValue' || kind === 'ObjectField' && step === 0) { + if (typeInfo.objectFieldDefs) { + var objectFields = Object.keys(typeInfo.objectFieldDefs) + .map(fieldName => typeInfo.objectFieldDefs[fieldName]); + return hintList(editor, options, cur, token, objectFields.map(field => ({ + text: field.name, + type: field.type, + description: field.description + }))); + } + } + + // Input values: Enum and Boolean + if (kind === 'EnumValue' || + kind === 'ListValue' && step === 1 || + kind === 'ObjectField' && step === 2 || + kind === 'Argument' && step === 2) { + var namedInputType = getNamedType(typeInfo.inputType); + if (namedInputType instanceof GraphQLEnumType) { + var valueMap = namedInputType.getValues(); + var values = Object.keys(valueMap).map(valueName => valueMap[valueName]); + return hintList(editor, options, cur, token, values.map(value => ({ + text: value.name, + type: namedInputType, + description: value.description + }))); + } else if (namedInputType === GraphQLBoolean) { + return hintList(editor, options, cur, token, [ + { text: 'true', type: GraphQLBoolean, description: 'Not false.' }, + { text: 'false', type: GraphQLBoolean, description: 'Not true.' }, + ]); + } + } + + // Fragment type conditions + if (kind === 'FragmentDefinition' && step === 3 || + kind === 'InlineFragment' && step === 2 || + kind === 'NamedType' && ( + state.prevState.kind === 'FragmentDefinition' || + state.prevState.kind === 'InlineFragment')) { + var possibleTypes; + if (typeInfo.parentType) { + possibleTypes = isAbstractType(typeInfo.parentType) ? + typeInfo.parentType.getPossibleTypes() : + [ typeInfo.parentType ]; + } else { + var typeMap = schema.getTypeMap(); + possibleTypes = Object.keys(typeMap) + .map(typeName => typeMap[typeName]) + .filter(isCompositeType); + } + return hintList(editor, options, cur, token, possibleTypes.map(type => ({ + text: type.name, + description: type.description + }))); + } + + // Variable definition types + if (kind === 'VariableDefinition' && step === 2 || + kind === 'ListType' && step === 1 || + kind === 'NamedType' && ( + state.prevState.kind === 'VariableDefinition' || + state.prevState.kind === 'ListType')) { + var inputTypeMap = schema.getTypeMap(); + var inputTypes = Object.keys(inputTypeMap) + .map(typeName => inputTypeMap[typeName]) + .filter(isInputType); + return hintList(editor, options, cur, token, inputTypes.map(type => ({ + text: type.name, + description: type.description + }))); + } + + // Directive names + if (kind === 'Directive') { + var directives = schema.getDirectives().filter(directive => + (directive.onField && state.prevState.kind === 'Field') || + (directive.onFragment && + (state.prevState.kind === 'FragmentDefinition' || + state.prevState.kind === 'InlineFragment' || + state.prevState.kind === 'FragmentSpread')) || + (directive.onOperation && + (state.prevState.kind === 'Query' || + state.prevState.kind === 'Mutation')) + ); + return hintList(editor, options, cur, token, directives.map(directive => ({ + text: directive.name, + description: directive.description + }))); + } +}); + + +// Utility for collecting rich type information given any token's state +// from the graphql-mode parser. +function getTypeInfo(schema, tokenState) { + var info = { + type: null, + parentType: null, + inputType: null, + directiveDef: null, + fieldDef: null, + argDef: null, + argDefs: null, + objectFieldDefs: null, + }; + + forEachState(tokenState, state => { + switch (state.kind) { + case 'Query': case 'ShortQuery': + info.type = schema.getQueryType(); + break; + case 'Mutation': + info.type = schema.getMutationType(); + break; + case 'InlineFragment': + case 'FragmentDefinition': + info.type = state.type && schema.getType(state.type); + break; + case 'Field': + info.fieldDef = info.type && state.name ? + getFieldDef(schema, info.parentType, state.name) : + null; + info.type = info.fieldDef && info.fieldDef.type; + break; + case 'SelectionSet': + info.parentType = getNamedType(info.type); + break; + case 'Directive': + info.directiveDef = state.name && schema.getDirective(state.name); + break; + case 'Arguments': + info.argDefs = + state.prevState.kind === 'Field' ? + info.fieldDef && info.fieldDef.args : + state.prevState.kind === 'Directive' ? + info.directiveDef && info.directiveDef.args : + null; + break; + case 'Argument': + info.argDef = null; + if (info.argDefs) { + for (var i = 0; i < info.argDefs.length; i++) { + if (info.argDefs[i].name === state.name) { + info.argDef = info.argDefs[i]; + break; + } + } + } + info.inputType = info.argDef && info.argDef.type; + break; + case 'ListValue': + var nullableType = getNullableType(info.inputType); + info.inputType = nullableType instanceof GraphQLList ? + nullableType.ofType : + null; + break; + case 'ObjectValue': + var objectType = getNamedType(info.inputType); + info.objectFieldDefs = objectType instanceof GraphQLInputObjectType ? + objectType.getFields() : + null; + break; + case 'ObjectField': + var objectField = state.name && info.objectFieldDefs ? + info.objectFieldDefs[state.name] : + null; + info.inputType = objectField && objectField.type; + break; + } + }); + + return info; +} + +// Utility for iterating through a state stack bottom-up. +function forEachState(stack, fn) { + var reverseStateStack = []; + var state = stack; + while (state && state.kind) { + reverseStateStack.push(state); + state = state.prevState; + } + for (var i = reverseStateStack.length - 1; i >= 0; i--) { + fn(reverseStateStack[i]); + } +} + +// Gets the field definition given a type and field name +function getFieldDef(schema, type, fieldName) { + if (fieldName === SchemaMetaFieldDef.name && schema.getQueryType() === type) { + return SchemaMetaFieldDef; + } + if (fieldName === TypeMetaFieldDef.name && schema.getQueryType() === type) { + return TypeMetaFieldDef; + } + if (fieldName === TypeNameMetaFieldDef.name && isCompositeType(type)) { + return TypeNameMetaFieldDef; + } + if (type.getFields) { + return type.getFields()[fieldName]; + } +} + +// Create the expected hint response given a possible list and a token +function hintList(editor, options, cursor, token, list) { + var hints = filterAndSortList(list, normalizeText(token.string)); + if (!hints) { + return; + } + + var tokenStart = token.type === null ? token.end : + /\w/.test(token.string[0]) ? token.start : + token.start + 1; + + var results = { + list: hints, + from: CodeMirror.Pos(cursor.line, tokenStart), + to: CodeMirror.Pos(cursor.line, token.end), + }; + + // GraphiQL displays the custom typeahead which appends information to the + // end of the UI. + if (options.displayInfo) { + var wrapper; + var information; + + // When a hint result is selected, we touch the UI. + CodeMirror.on(results, 'select', (ctx, el) => { + // Only the first time (usually when the hint UI is first displayed) + // do we create the wrapping node. + if (!wrapper) { + // Wrap the existing hint UI, so we have a place to put information. + var hintsUl = el.parentNode; + var container = hintsUl.parentNode; + wrapper = document.createElement('div'); + container.appendChild(wrapper); + + // CodeMirror vertically inverts the hint UI if there is not enough + // space below the cursor. Since this modified UI appends to the bottom + // of CodeMirror's existing UI, it could cover the cursor. This adjusts + // the positioning of the hint UI to accomodate. + var top = hintsUl.style.top; + var bottom = ''; + var cursorTop = editor.cursorCoords().top; + if (parseInt(top, 10) < cursorTop) { + top = ''; + bottom = (window.innerHeight - cursorTop + 3) + 'px'; + } + + // Style the wrapper, remove positioning from hints. Note that usage + // of this option will need to specify CSS to remove some styles from + // the existing hint UI. + wrapper.className = 'CodeMirror-hints-wrapper'; + wrapper.style.left = hintsUl.style.left; + wrapper.style.top = top; + wrapper.style.bottom = bottom; + hintsUl.style.left = ''; + hintsUl.style.top = ''; + + // This "information" node will contain the additional info about the + // highlighted typeahead option. + information = document.createElement('div'); + information.className = 'CodeMirror-hint-information'; + if (bottom) { + wrapper.appendChild(information); + wrapper.appendChild(hintsUl); + } else { + wrapper.appendChild(hintsUl); + wrapper.appendChild(information); + } + + // When CodeMirror attempts to remove the hint UI, we detect that it was + // removed from our wrapper and in turn remove the wrapper from the + // original container. + var onRemoveFn; + wrapper.addEventListener('DOMNodeRemoved', onRemoveFn = event => { + if (event.target === hintsUl) { + wrapper.removeEventListener('DOMNodeRemoved', onRemoveFn); + wrapper.parentNode.removeChild(wrapper); + wrapper = null; + information = null; + onRemoveFn = null; + } + }); + } + + // Now that the UI has been set up, add info to information. + var renderInfoFn = + ctx.renderInfo || options.renderInfo || defaultRenderInfo; + renderInfoFn(information, ctx); + }); + } + + return results; +} + +function defaultRenderInfo(elem, ctx) { + elem.innerHTML = ctx.description || 'Self descriptive.'; +} + +// Given a list of hint entries and currently typed text, sort and filter to +// provide a concise list. +function filterAndSortList(list, text) { + var sorted = !text ? list : list.map( + entry => ({ + proximity: getProximity(normalizeText(entry.text), text), + entry + }) + ).filter( + pair => pair.proximity <= 2 + ).sort( + (a, b) => + (a.proximity - b.proximity) || + (a.entry.text.length - b.entry.text.length) + ).map( + pair => pair.entry + ); + + return sorted.length > 0 ? sorted : list; +} + +function normalizeText(text) { + return text.toLowerCase().replace(/\W/g, ''); +} + +// Determine a numeric proximity for a suggestion based on current text. +function getProximity(suggestion, text) { + // start with lexical distance + var proximity = lexicalDistance(text, suggestion); + if (suggestion.length > text.length) { + // do not penalize long suggestions. + proximity -= suggestion.length - text.length - 1; + // penalize suggestions not starting with this phrase + proximity += suggestion.indexOf(text) === 0 ? 0 : 0.5; + } + return proximity; +} diff --git a/src/codemirror/hint/lexicalDistance.js b/src/codemirror/hint/lexicalDistance.js new file mode 100644 index 00000000000..9df888d11aa --- /dev/null +++ b/src/codemirror/hint/lexicalDistance.js @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +/** + * Computes the lexical distance between strings A and B. + * + * The "distance" between two strings is given by counting the minimum number + * of edits needed to transform string A into string B. An edit can be an + * insertion, deletion, or substitution of a single character, or a swap of two + * adjacent characters. + * + * This distance can be useful for detecting typos in input or sorting + * + * @param {string} a + * @param {string} b + * @return {int} distance in number of edits + */ +export default function lexicalDistance(a, b) { + var i; + var j; + var d = []; + var aLength = a.length; + var bLength = b.length; + + for (i = 0; i <= aLength; i++) { + d[i] = [ i ]; + } + + for (j = 1; j <= bLength; j++) { + d[0][j] = j; + } + + for (i = 1; i <= aLength; i++) { + for (j = 1; j <= bLength; j++) { + var cost = a[i - 1] === b[j - 1] ? 0 : 1; + + d[i][j] = Math.min( + d[i - 1][j] + 1, + d[i][j - 1] + 1, + d[i - 1][j - 1] + cost + ); + + if (i > 1 && j > 1 && + a[i - 1] === b[j - 2] && + a[i - 2] === b[j - 1]) { + d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost); + } + } + } + + return d[aLength][bLength]; +} diff --git a/src/codemirror/lint/graphql-lint.js b/src/codemirror/lint/graphql-lint.js new file mode 100644 index 00000000000..ddfce0ca061 --- /dev/null +++ b/src/codemirror/lint/graphql-lint.js @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import CodeMirror from 'codemirror'; +import { parse } from 'graphql/language'; +import { validate } from 'graphql/validation'; + + +CodeMirror.registerHelper('lint', 'graphql', function (text, options, editor) { + var schema = options.schema; + try { + var ast = parse(text); + } catch (error) { + var location = error.locations[0]; + var pos = CodeMirror.Pos(location.line - 1, location.column); + var token = editor.getTokenAt(pos); + return [ { + message: error.message, + severity: 'error', + type: 'syntax', + from: CodeMirror.Pos(location.line - 1, token.start), + to: CodeMirror.Pos(location.line - 1, token.end), + } ]; + } + var errors = schema ? validate(schema, ast) : []; + return flatMap(errors, error => errorAnnotations(editor, error)); +}); + +function errorAnnotations(editor, error) { + return error.nodes.map(node => { + var highlightNode = + node.kind !== 'Variable' && node.name ? node.name : + node.variable ? node.variable : + node; + return { + message: error.message, + severity: 'error', + type: 'validation', + from: editor.posFromIndex(highlightNode.loc.start), + to: editor.posFromIndex(highlightNode.loc.end), + }; + }); +} + +// General utility for flat-mapping. +function flatMap(array: Array, mapper: (item: T) => Array): Array { + return Reflect.apply(Array.prototype.concat, [], array.map(mapper)); +} diff --git a/src/codemirror/lint/json-lint.js b/src/codemirror/lint/json-lint.js new file mode 100644 index 00000000000..755f8bd3ad6 --- /dev/null +++ b/src/codemirror/lint/json-lint.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import CodeMirror from 'codemirror'; +import { jsonLint } from './jsonLint'; + + +CodeMirror.registerHelper('lint', 'json', text => { + var err = jsonLint(text); + if (err) { + return [ { + message: err.message, + severity: 'error', + from: getLocation(text, err.start), + to: getLocation(text, err.end) + } ]; + } + return []; +}); + +function getLocation(source, position) { + var line = 0; + var column = position; + var lineRegexp = /\r\n|[\n\r]/g; + var match; + while ((match = lineRegexp.exec(source)) && match.index < position) { + line += 1; + column = position - (match.index + match[0].length); + } + return CodeMirror.Pos(line, column); +} diff --git a/src/codemirror/lint/jsonLint.js b/src/codemirror/lint/jsonLint.js new file mode 100644 index 00000000000..0b9f0035d2a --- /dev/null +++ b/src/codemirror/lint/jsonLint.js @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + + +/** + * This JSON parser simply walks the input, but does not generate an AST + * or Value. Instead it returns either an syntax error object, or null. + * + * The returned syntax error object: + * + * - message: string + * - start: int - the start inclusive offset of the syntax error + * - end: int - the end exclusive offset of the syntax error + * + */ +export function jsonLint(str, looseMode) { + string = str; + strLen = str.length; + end = -1; + try { + ch(); + lex(); + if (looseMode) { + readVal(); + } else { + readObj(); + } + expect('EOF'); + } catch (err) { + return err; + } +} + +var string; +var strLen; +var start; +var end; +var code; +var kind; + +function readObj() { + expect('{'); + if (!skip('}')) { + do { + expect('String'); + expect(':'); + readVal(); + } while (skip(',')); + expect('}'); + } +} + +function readArr() { + expect('['); + if (!skip(']')) { + do { + readVal(); + } while (skip(',')); + expect(']'); + } +} + +function readVal() { + switch (kind) { + case '[': return readArr(); + case '{': return readObj(); + case 'String': return lex(); + default: return expect('Value'); + } +} + +function syntaxError(message) { + return { message, start, end }; +} + +function expect(str) { + if (kind === str) { + return lex(); + } + throw syntaxError(`Expected ${str} but got ${string.slice(start, end)}.`); +} + +function skip(k) { + if (kind === k) { + lex(); + return true; + } +} + +function ch() { + if (end < strLen) { + end++; + code = end === strLen ? 0 : string.charCodeAt(end); + } +} + +function lex() { + while (code === 9 || code === 10 || code === 13 || code === 32) { + ch(); + } + + if (code === 0) { + kind = 'EOF'; + return; + } + + start = end; + + switch (code) { + // " + case 34: + kind = 'String'; + return readString(); + // - + case 45: + // 0-9 + case 48: case 49: case 50: case 51: case 52: + case 53: case 54: case 55: case 56: case 57: + kind = 'Value'; + return readNumber(); + // f + case 102: + if (string.slice(start, start + 5) !== 'false') { + break; + } + end += 4; ch(); + + kind = 'Value'; + return; + // n + case 110: + if (string.slice(start, start + 4) !== 'null') { + break; + } + end += 3; ch(); + + kind = 'Value'; + return; + // t + case 116: + if (string.slice(start, start + 4) !== 'true') { + break; + } + end += 3; ch(); + + kind = 'Value'; + return; + } + + kind = string[start]; + ch(); +} + +function readString() { + ch(); + while (code !== 34) { + ch(); + if (code === 92) { // \ + ch(); + switch (code) { + case 34: // ' + case 47: // / + case 92: // \ + case 98: // b + case 102: // f + case 110: // n + case 114: // r + case 116: // t + ch(); + break; + case 117: // u + ch(); + readHex(); + readHex(); + readHex(); + readHex(); + break; + default: + throw syntaxError('Bad character escape sequence.'); + } + } + } + + if (code === 34) { + ch(); + return; + } + + throw syntaxError('Unterminated string.'); +} + +function readHex() { + if ( + (code >= 48 && code <= 57) || // 0-9 + (code >= 65 && code <= 70) || // A-F + (code >= 97 && code <= 102) // a-f + ) { + return ch(); + } + throw syntaxError('Expected hexadecimal digit.'); +} + +function readNumber() { + if (code === 45) { // - + ch(); + } + + if (code === 48) { // 0 + ch(); + } else { + readDigits(); + } + + if (code === 46) { // . + ch(); + readDigits(); + } + + if (code === 69 || code === 101) { // E e + ch(); + if (code === 43 || code === 45) { // + - + ch(); + } + readDigits(); + } +} + +function readDigits() { + if (code < 48 || code > 57) { // 0 - 9 + throw syntaxError('Expected decimal digit.'); + } + do { + ch(); + } while (code >= 48 && code <= 57); // 0 - 9 +} diff --git a/src/codemirror/mode/graphql-mode.js b/src/codemirror/mode/graphql-mode.js new file mode 100644 index 00000000000..f7eea10e9f8 --- /dev/null +++ b/src/codemirror/mode/graphql-mode.js @@ -0,0 +1,406 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import CodeMirror from 'codemirror'; + + +/** + * The GraphQL mode is defined as a tokenizer along with a list of rules, each + * of which is either a function or an array. + * + * * Function: Provided a token and the stream, returns an expected next step. + * * Array: A list of steps to take in order. + * + * A step is either another rule, or a terminal description of a token. If it + * is a rule, that rule is pushed onto the stack and the parsing continues from + * that point. + * + * If it is a terminal description, the token is checked against it using a + * `match` function. If the match is successful, the token is colored and the + * rule is stepped forward. If the match is unsuccessful, the remainder of the + * rule is skipped and the previous rule is advanced. + * + * This parsing algorithm allows for incremental online parsing within various + * levels of the syntax tree and results in a structured `state` linked-list + * which contains the relevant information to produce valuable typeaheads. + */ +CodeMirror.defineMode('graphql', config => { + return { + config, + token: getToken, + indent, + startState() { + var initialState = { level: 0 }; + pushRule(initialState, 'Document'); + return initialState; + }, + electricInput: /^\s*[})\]]/, + fold: 'brace', + lineComment: '#', + closeBrackets: { + pairs: '()[]{}""', + explode: '()[]{}' + }, + }; +}); + +function getToken(stream, state) { + if (state.needsAdvance) { + state.needsAdvance = false; + advanceRule(state); + } + + // Remember initial indentation + if (stream.sol()) { + state.indentLevel = Math.floor(stream.indentation() / this.config.tabSize); + } + + // Consume spaces and ignored characters + if (stream.eatSpace() || stream.eatWhile(',')) { + return null; + } + + // Tokenize line comment + if (stream.match(this.lineComment)) { + stream.skipToEnd(); + return 'comment'; + } + + // Lex a token from the stream + var token = lex(stream); + + // If there's no matching token, skip ahead. + if (!token) { + stream.match(/\w+|./); + return 'invalidchar'; + } + + // Save state before continuing. + saveState(state); + + // Handle changes in expected indentation level + if (token.kind === 'Punctuation') { + if (/^[{([]/.test(token.value)) { + // Push on the stack of levels one level deeper than the current level. + state.levels = (state.levels || []).concat(state.indentLevel + 1); + } else if (/^[})\]]/.test(token.value)) { + // Pop from the stack of levels. + // If the top of the stack is lower than the current level, lower the + // current level to match. + var levels = state.levels = (state.levels || []).slice(0, -1); + if (levels.length > 0 && levels[levels.length - 1] < state.indentLevel) { + state.indentLevel = levels[levels.length - 1]; + } + } + } + + while (state.rule) { + // If this is a forking rule, determine what rule to use based on + // the current token, otherwise expect based on the current step. + var expected = + typeof state.rule === 'function' ? + state.step === 0 ? state.rule(token, stream) : null : + state.rule[state.step]; + + if (expected) { + // Un-wrap optional/list ParseRules. + if (expected.ofRule) { + expected = expected.ofRule; + } + + // A string represents a Rule + if (typeof expected === 'string') { + pushRule(state, expected); + continue; + } + + // Otherwise, match a Terminal. + if (expected.match && expected.match(token)) { + if (expected.update) { + expected.update(state, token); + } + // If this token was a punctuator, advance the parse rule, otherwise + // mark the state to be advanced before the next token. This ensures + // that tokens which can be appended to keep the appropriate state. + if (token.kind === 'Punctuation') { + advanceRule(state); + } else { + state.needsAdvance = true; + } + return expected.style; + } + } + + unsuccessful(state); + } + + // The parser does not know how to interpret this token, do not affect state. + restoreState(state); + return 'invalidchar'; +} + +function indent(state, textAfter) { + var levels = state.levels; + // If there is no stack of levels, use the current level. + // Otherwise, use the top level, pre-emptively dedenting for close braces. + var level = !levels || levels.length === 0 ? state.indentLevel : + levels[levels.length - 1] - (this.electricInput.test(textAfter) ? 1 : 0); + return level * this.config.indentUnit; +} + +var stateCache = {}; + +// Save the current state in the cache. +function saveState(state) { + Object.assign(stateCache, state); +} + +// Restore from the state cache. +function restoreState(state) { + Object.assign(state, stateCache); +} + +// Push a new rule onto the state. +function pushRule(state, ruleKind) { + state.prevState = Object.assign({}, state); + state.kind = ruleKind; + state.name = null; + state.type = null; + state.rule = ParseRules[ruleKind]; + state.step = 0; +} + +// Pop the current rule from the state. +function popRule(state) { + state.kind = state.prevState.kind; + state.name = state.prevState.name; + state.type = state.prevState.type; + state.rule = state.prevState.rule; + state.step = state.prevState.step; + state.prevState = state.prevState.prevState; +} + +// Advance the step of the current rule. +function advanceRule(state) { + // Advance the step in the rule. If the rule is completed, pop + // the rule and advance the parent rule as well (recursively). + state.step++; + while ( + state.rule && + !(Array.isArray(state.rule) && state.step < state.rule.length) + ) { + popRule(state); + // Do not advance a List step so it has the opportunity to repeat itself. + if ( + state.rule && + !(Array.isArray(state.rule) && state.rule[state.step].isList) + ) { + state.step++; + } + } +} + +// Unwind the state after an unsuccessful match. +function unsuccessful(state) { + // Fall back to the parent rule until you get to an optional or list rule or + // until the entire stack of rules is empty. + while ( + state.rule && + !(Array.isArray(state.rule) && state.rule[state.step].ofRule) + ) { + popRule(state); + } + + // If there is still a rule, it must be an optional or list rule. + // Consider this rule a success so that we may move past it. + if (state.rule) { + advanceRule(state); + } +} + +// Given a stream, returns a { kind, value } pair, or null. +function lex(stream) { + var kinds = Object.keys(LexRules); + for (var i = 0; i < kinds.length; i++) { + var match = stream.match(LexRules[kinds[i]]); + if (match) { + return { kind: kinds[i], value: match[0] }; + } + } +} + +// An optional rule. +function opt(ofRule) { + return { ofRule }; +} + +// A list of another rule. +function list(ofRule) { + return { ofRule, isList: true }; +} + +// Token of a kind +function t(kind, style) { + return { style, match: token => token.kind === kind }; +} + +// Punctuator +function p(value, style) { + return { + style: style || 'punctuation', + match: token => token.kind === 'Punctuation' && token.value === value + }; +} + +// A keyword Token +function word(value) { + return { + style: 'keyword', + match: token => token.kind === 'Name' && token.value === value + }; +} + +// A Name Token which will decorate the state with a `name` +function name(style) { + return { + style, + match: token => token.kind === 'Name', + update(state, token) { + state.name = token.value; + } + }; +} + +// A Name Token which will decorate the state with a `type` +function type(style) { + return { + style, + match: token => token.kind === 'Name', + update(state, token) { + state.type = token.value; + } + }; +} + +/** + * The lexer rules. These are exactly as described by the spec. + */ +var LexRules = { + // The Name token. + Name: /^[_A-Za-z][_0-9A-Za-z]*/, + + // All Punctuation used in GraphQL + Punctuation: /^(?:!|\$|\(|\)|\.\.\.|:|=|@|\[|\]|\{|\})/, + + // Combines the IntValue and FloatValue tokens. + Number: /^-?(?:0|(?:[1-9][0-9]*))(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?/, + + // Note the closing quote is made optional as an IDE experience improvment. + String: /^"(?:[^"\\]|\\(?:b|f|n|r|t|u[0-9a-fA-F]{4}))*"?/, +}; + +/** + * The parser rules. These are very close to, but not exactly the same as the + * spec. Minor deviations allow for a simpler implementation. The resulting + * parser can parse everything the spec declares possible. + */ +var ParseRules = { + Document: [ list('Definition') ], + Definition(token) { + switch (token.value) { + case 'query': return 'Query'; + case 'mutation': return 'Mutation'; + case 'fragment': return 'FragmentDefinition'; + case '{': return 'ShortQuery'; + } + }, + // Note: instead of "Operation", these rules have been separated out. + Query: [ + word('query'), + name('def'), + opt('VariableDefinitions'), + list('Directive'), + 'SelectionSet' + ], + ShortQuery: [ 'SelectionSet' ], + Mutation: [ + word('mutation'), + name('def'), + opt('VariableDefinitions'), + list('Directive'), + 'SelectionSet' + ], + VariableDefinitions: [ p('('), list('VariableDefinition'), p(')') ], + VariableDefinition: [ 'Variable', p(':'), 'Type', opt('DefaultValue') ], + Variable: [ p('$', 'variable'), name('variable') ], + DefaultValue: [ p('='), 'Value' ], + SelectionSet: [ p('{'), list('Selection'), p('}') ], + Selection(token, stream) { + return token.value === '...' ? + stream.match(/[\s\u00a0,]*on\b/, false) ? + 'InlineFragment' : 'FragmentSpread' : + stream.match(/[\s\u00a0,]*:/, false) ? 'AliasedField' : 'Field'; + }, + // Note: this minor deviation of "AliasedField" simplifies the lookahead. + AliasedField: [ name('qualifier'), p(':'), 'Field' ], + Field: [ + name('property'), opt('Arguments'), list('Directive'), opt('SelectionSet') + ], + Arguments: [ p('('), list('Argument'), p(')') ], + Argument: [ name('attribute'), p(':'), 'Value' ], + FragmentSpread: [ p('...'), name('def'), list('Directive') ], + InlineFragment: [ + p('...'), + word('on'), + type('atom'), + list('Directive'), + 'SelectionSet' + ], + FragmentDefinition: [ + word('fragment'), + name('def'), + word('on'), + type('atom'), + list('Directive'), + 'SelectionSet' + ], + // Variables could be parsed in cases where only Const is expected by spec. + Value(token) { + switch (token.kind) { + case 'Number': return 'NumberValue'; + case 'String': return 'StringValue'; + case 'Punctuation': + switch (token.value) { + case '[': return 'ListValue'; + case '{': return 'ObjectValue'; + case '$': return 'Variable'; + } + return null; + case 'Name': + switch (token.value) { + case 'true': case 'false': return 'BooleanValue'; + } + return 'EnumValue'; + } + }, + NumberValue: [ t('Number', 'number') ], + StringValue: [ t('String', 'string') ], + BooleanValue: [ t('Name', 'builtin') ], + EnumValue: [ name('string-2') ], + ListValue: [ p('['), list('Value'), p(']') ], + ObjectValue: [ p('{'), list('ObjectField'), p('}') ], + ObjectField: [ name('attribute'), p(':'), 'Value' ], + Type(token) { + return token.value === '[' ? 'ListType' : 'NamedType'; + }, + // NonNullType has been merged into ListType and NamedType to simplify. + ListType: [ p('['), 'NamedType', p(']'), opt(p('!')) ], + NamedType: [ name('atom'), opt(p('!')) ], + Directive: [ p('@', 'meta'), name('meta'), opt('Arguments') ], +}; diff --git a/src/components/DocExplorer.js b/src/components/DocExplorer.js new file mode 100644 index 00000000000..f1678ec9154 --- /dev/null +++ b/src/components/DocExplorer.js @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import React from 'react'; + + +/** + * DocExplorer + * + * + * Props: + * + * + */ +export class DocExplorer extends React.Component { + constructor() { + super(); + + this.EXPLORER_WIDTH = 400; + this.currentlyInspectedType = null; + this.state = { + width: 'initial', + expanded: false + }; + + this.startPage = 'Welcome to GraphQL Documentation Explorer!'; + this.content = ''; + } + + _renderTypeDefinitions(type) { + function renderField(field) { + return ( + + ); + } + + var fields = type.getFields(); + var fieldsJSX = []; + Object.keys(fields).forEach(fieldName => { + fieldsJSX.push( + renderField(fields[fieldName]) + ); + }); + + return ( + {fieldsJSX} + ); + } + + // TODO: This part could be optionalized + _generateStartPage(schema) { + var queryType = schema.getQueryType(); + var mutationType = schema.getMutationType(); + var directives = schema.getDirectives(); + + function renderDirectives() { + return 'test'; + } + + var queryJSX = queryType ? +
+
+ Query +
+ {this._renderTypeDefinitions(queryType)} +
+ : ''; + + var mutationJSX = mutationType ? +
+
+ Mutation +
+ {this._renderTypeDefinitions(mutationType)} +
: ''; + + var directivesJSX = directives ? +
+
+ Directive +
+ {renderDirectives(directives)} +
+ : ''; + + return ( +
+ {queryJSX} + {mutationJSX} + {directivesJSX} +
+ ); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.schema && nextProps.schema !== this.props.schema) { + this.startPage = this._generateStartPage(nextProps.schema); + } + + if (nextProps.typeName !== this.props.typeName) { + var typeName = nextProps.typeName; + if (typeName.endsWith('!')) { + typeName = typeName.slice(0, typeName.length - 1); + } + this.currentlyInspectedType = this.props.schema.getTypeMap[typeName]; + + this.setState({ + width: this.EXPLORER_WIDTH, + expanded: true + }); + } + } + + _onToggleBtnClick() { + this.setState({ + width: this.state.expanded ? 'initial' : this.EXPLORER_WIDTH, + expanded: !this.state.expanded + }); + } + + render() { + if (this.state.expanded) { + this.content = this.currentlyInspectedType ? + this.currentlyInspectedType.description || + 'Type description not found.' : + this.startPage; + } else { + this.content = ''; + } + + return ( +
+
+ +
+ Documentation Explorer +
+
+
+ {this.content} +
+
+ ); + } +} diff --git a/src/components/ExecuteButton.js b/src/components/ExecuteButton.js new file mode 100644 index 00000000000..ef5e7ff9367 --- /dev/null +++ b/src/components/ExecuteButton.js @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import React from 'react'; + + +/** + * ExecuteButton + * + * What a nice round shiny button. Cmd/Ctrl-Enter is the shortcut. + */ +export class ExecuteButton extends React.Component { + render() { + return ( + + ); + } + + componentDidMount() { + this.keyHandler = event => { + if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) { + event.preventDefault(); + if (this.props.onClick) { + this.props.onClick(); + } + } + }; + document.addEventListener('keydown', this.keyHandler, true); + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.keyHandler, true); + } +} diff --git a/src/components/QueryEditor.js b/src/components/QueryEditor.js new file mode 100644 index 00000000000..2e7cd69cdc1 --- /dev/null +++ b/src/components/QueryEditor.js @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import React from 'react'; +import marked from 'marked'; +import CodeMirror from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/comment/comment'; +import 'codemirror/addon/edit/matchbrackets'; +import 'codemirror/addon/edit/closebrackets'; +import 'codemirror/addon/fold/foldgutter'; +import 'codemirror/addon/fold/brace-fold'; +import 'codemirror/addon/lint/lint'; +import 'codemirror/keymap/sublime'; +import '../codemirror/hint/graphql-hint'; +import '../codemirror/lint/graphql-lint'; +import '../codemirror/mode/graphql-mode'; + + +/** + * QueryEditor + * + * Maintains an instance of CodeMirror responsible for editing a GraphQL query. + * + * Props: + * + * - schema: A GraphQLSchema instance enabling editor linting and hinting. + * - value: The text of the editor. + * - onEdit: A function called when the editor changes, given the edited text. + * + */ +export class QueryEditor extends React.Component { + constructor(props) { + super(); + + // Keep a cached version of the value, this cache will be updated when the + // editor is updated, which can later be used to protect the editor from + // unnecessary updates during the update lifecycle. + this.cachedValue = props.value || ''; + } + + /** + * Public API for retrieving the CodeMirror instance from this + * React component. + */ + getCodeMirror() { + return this.editor; + } + + componentDidMount() { + var onHintInformationRender = this.props.onHintInformationRender; + this.editor = CodeMirror(React.findDOMNode(this), { + value: this.props.value || '', + lineNumbers: true, + tabSize: 2, + mode: 'graphql', + theme: 'graphiql', + keyMap: 'sublime', + autoCloseBrackets: true, + matchBrackets: true, + showCursorWhenSelecting: true, + foldGutter: { + minFoldSize: 4 + }, + lint: { + schema: this.props.schema, + }, + hintOptions: { + schema: this.props.schema, + closeOnUnfocus: false, + completeSingle: false, + displayInfo: true, + renderInfo(elem, ctx) { + var description = marked( + ctx.description || 'Self descriptive.', + { smartypants: true } + ); + var type = ctx.type ? + '' + String(ctx.type) + '' : + ''; + elem.innerHTML = '
' + + (description.slice(0, 3) === '

' ? + '

' + type + description.slice(3) : + type + description) + '

'; + if (onHintInformationRender) { + onHintInformationRender(elem); + } + } + }, + gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ], + extraKeys: { + 'Cmd-Space': () => this.editor.showHint({ completeSingle: true }), + 'Ctrl-Space': () => this.editor.showHint({ completeSingle: true }), + + // Editor improvements + 'Ctrl-Left': 'goSubwordLeft', + 'Ctrl-Right': 'goSubwordRight', + 'Alt-Left': 'goGroupLeft', + 'Alt-Right': 'goGroupRight', + } + }); + + this.editor.on('change', this._onEdit.bind(this)); + this.editor.on('keyup', this._onKeyUp.bind(this)); + } + + componentWillUnmount() { + this.editor = null; + } + + componentDidUpdate(prevProps) { + // Ensure the changes caused by this update are not interpretted as + // user-input changes which could otherwise result in an infinite + // event loop. + this.ignoreChangeEvent = true; + if (this.props.schema !== prevProps.schema) { + this.editor.options.lint.schema = this.props.schema; + this.editor.options.hintOptions.schema = this.props.schema; + CodeMirror.signal(this.editor, 'change', this.editor); + } + if (this.props.value !== prevProps.value && + this.props.value !== this.cachedValue) { + this.cachedValue = this.props.value; + this.editor.setValue(this.props.value); + } + this.ignoreChangeEvent = false; + } + + _onKeyUp(cm, event) { + var code = event.keyCode; + if ( + (code >= 65 && code <= 90) || // letters + (!event.shiftKey && code >= 48 && code <= 57) || // numbers + (event.shiftKey && code === 189) || // underscore + (event.shiftKey && code === 50) || // @ + (event.shiftKey && code === 57) // ( + ) { + this.editor.execCommand('autocomplete'); + } + } + + _onEdit() { + if (!this.ignoreChangeEvent) { + this.cachedValue = this.editor.getValue(); + if (this.props.onEdit) { + this.props.onEdit(this.cachedValue); + } + } + } + + render() { + return
; + } +} diff --git a/src/components/ResultViewer.js b/src/components/ResultViewer.js new file mode 100644 index 00000000000..eae99ba1441 --- /dev/null +++ b/src/components/ResultViewer.js @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import React from 'react'; +import CodeMirror from 'codemirror'; +import 'codemirror/addon/fold/foldgutter'; +import 'codemirror/addon/fold/brace-fold'; +import 'codemirror/keymap/sublime'; +import 'codemirror/mode/javascript/javascript'; + + +/** + * ResultViewer + * + * Maintains an instance of CodeMirror for viewing a GraphQL response. + * + * Props: + * + * - value: The text of the editor. + * + */ +export class ResultViewer extends React.Component { + componentDidMount() { + this.viewer = CodeMirror(React.findDOMNode(this), { + value: this.props.value || '', + readOnly: true, + theme: 'graphiql', + mode: { + name: 'javascript', + json: true + }, + keyMap: 'sublime', + foldGutter: { + minFoldSize: 4 + }, + gutters: [ 'CodeMirror-foldgutter' ], + extraKeys: { + // Editor improvements + 'Ctrl-Left': 'goSubwordLeft', + 'Ctrl-Right': 'goSubwordRight', + 'Alt-Left': 'goGroupLeft', + 'Alt-Right': 'goGroupRight', + } + }); + } + + componentWillUnmount() { + this.viewer = null; + } + + shouldComponentUpdate(nextProps) { + return this.props.value !== nextProps.value; + } + + componentDidUpdate() { + this.viewer.setValue(this.props.value || ''); + } + + render() { + return
; + } +} diff --git a/src/components/VariableEditor.js b/src/components/VariableEditor.js new file mode 100644 index 00000000000..b274095a419 --- /dev/null +++ b/src/components/VariableEditor.js @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import React from 'react'; +import CodeMirror from 'codemirror'; +import 'codemirror/addon/fold/brace-fold'; +import 'codemirror/addon/fold/foldgutter'; +import 'codemirror/addon/lint/lint'; +import 'codemirror/keymap/sublime'; +import 'codemirror/mode/javascript/javascript'; +import '../codemirror/lint/json-lint'; + + +/** + * VariableEditor + * + * An instance of CodeMirror for editing variables defined in QueryEditor. + * + * Props: + * + * - value: The text of the editor. + * - onEdit: A function called when the editor changes, given the edited text. + * + */ +export class VariableEditor extends React.Component { + constructor(props) { + super(); + + // Keep a cached version of the value, this cache will be updated when the + // editor is updated, which can later be used to protect the editor from + // unnecessary updates during the update lifecycle. + this.cachedValue = props.value || ''; + } + + componentDidMount() { + this.editor = CodeMirror(React.findDOMNode(this), { + value: this.props.value || '', + lineNumbers: true, + theme: 'graphiql', + mode: { + name: 'javascript', + json: true + }, + lint: true, + autoCloseBrackets: true, + matchBrackets: true, + showCursorWhenSelecting: true, + keyMap: 'sublime', + foldGutter: { + minFoldSize: 4 + }, + gutters: [ 'CodeMirror-linenumbers', 'CodeMirror-foldgutter' ], + extraKeys: { + // Editor improvements + 'Ctrl-Left': 'goSubwordLeft', + 'Ctrl-Right': 'goSubwordRight', + 'Alt-Left': 'goGroupLeft', + 'Alt-Right': 'goGroupRight', + } + }); + + this.editor.on('change', this._onEdit.bind(this)); + } + + componentWillUnmount() { + this.editor = null; + } + + componentDidUpdate(prevProps) { + // Ensure the changes caused by this update are not interpretted as + // user-input changes which could otherwise result in an infinite + // event loop. + this.ignoreChangeEvent = true; + if (this.props.value !== prevProps.value && + this.props.value !== this.cachedValue) { + this.cachedValue = this.props.value; + this.editor.setValue(this.props.value); + } + this.ignoreChangeEvent = false; + } + + _onEdit() { + if (!this.ignoreChangeEvent) { + this.cachedValue = this.editor.getValue(); + if (this.props.onEdit) { + this.props.onEdit(this.cachedValue); + } + } + } + + render() { + return
; + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000000..0fc524e1d0a --- /dev/null +++ b/src/index.js @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +// The primary React component to use. +module.exports = require('./components/GraphiQL').GraphiQL; diff --git a/src/utility/fillLeafs.js b/src/utility/fillLeafs.js new file mode 100644 index 00000000000..9afac8ddfe6 --- /dev/null +++ b/src/utility/fillLeafs.js @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { TypeInfo } from 'graphql/utilities'; +import { parse, visit, print } from 'graphql/language'; +import { isLeafType, getNamedType } from 'graphql/type'; + + +/** + * Given a document string which may not be valid due to terminal fields not + * representing leaf values (Spec Section: "Leaf Field Selections"), and a + * function which provides reasonable default field names for a given type, + * this function will attempt to produce a schema which is valid after filling + * in selection sets for the invalid fields. + * + * Note that there is no guarantee that the result will be a valid query, this + * utility represents a "best effort" which may be useful within IDE tools. + */ +export function fillLeafs(schema, docString, getDefaultFieldNames) { + var ast; + try { + ast = parse(docString); + } catch (error) { + return docString; + } + + var fieldNameFn = getDefaultFieldNames || defaultGetDefaultFieldNames; + var insertions = []; + + let typeInfo = new TypeInfo(schema); + visit(ast, { + leave(node) { + typeInfo.leave(node); + }, + enter(node) { + typeInfo.enter(node); + if (node.kind === 'Field' && !node.selectionSet) { + var fieldType = typeInfo.getType(); + var selectionSet = buildSelectionSet(fieldType, fieldNameFn); + if (selectionSet) { + var indent = getIndentation(docString, node.loc.start); + insertions.push({ + index: node.loc.end, + string: ' ' + print(selectionSet).replace(/\n/g, '\n' + indent) + }); + } + } + } + }); + + // Apply the insertions, but also return the insertions metadata. + return { + insertions, + result: withInsertions(docString, insertions), + }; +} + +// The default function to use for producing the default fields from a type. +// This function first looks for some common patterns, and falls back to +// including all leaf-type fields. +function defaultGetDefaultFieldNames(type) { + var fields = type.getFields(); + + // Is there an `id` field? + if (fields['id']) { + return [ 'id' ]; + } + + // Is there an `edges` field? + if (fields['edges']) { + return [ 'edges' ]; + } + + // Is there an `node` field? + if (fields['node']) { + return [ 'node' ]; + } + + // Include all leaf-type fields. + var leafFieldNames = []; + Object.keys(fields).forEach(fieldName => { + if (isLeafType(fields[fieldName].type)) { + leafFieldNames.push(fieldName); + } + }); + return leafFieldNames; +} + +// Given a GraphQL type, and a function which produces field names, recursively +// generate a SelectionSet which includes default fields. +function buildSelectionSet(type, getDefaultFieldNames) { + // Unknown types and leaf types do not have selection sets. + if (!type || isLeafType(type)) { + return; + } + + // Get an array of field names to use. + var fieldNames = getDefaultFieldNames(type); + + // If there are no field names to use, return no selection set. + if (!Array.isArray(fieldNames) || fieldNames.length === 0) { + return; + } + + // Build a selection set of each field, calling buildSelectionSet recursively. + return { + kind: 'SelectionSet', + selections: fieldNames.map(fieldName => { + var fieldDef = type.getFields()[fieldName]; + var fieldType = fieldDef ? getNamedType(fieldDef.type) : null; + return { + kind: 'Field', + name: { + kind: 'Name', + value: fieldName + }, + selectionSet: buildSelectionSet(fieldType, getDefaultFieldNames) + }; + }) + }; +} + +// Given an initial string, and a list of "insertion" { index, string } objects, +// return a new string with these insertions applied. +function withInsertions(initial, insertions) { + if (insertions.length === 0) { + return initial; + } + var edited = ''; + var prevIndex = 0; + insertions.forEach(({ index, string }) => { + edited += initial.slice(prevIndex, index) + string; + prevIndex = index; + }); + edited += initial.slice(prevIndex); + return edited; +} + +// Given a string and an index, look backwards to find the string of whitespace +// following the next previous line break. +function getIndentation(str, index) { + var indentStart = index; + var indentEnd = index; + while (indentStart) { + var c = str.charCodeAt(indentStart - 1); + // line break + if (c === 10 || c === 13 || c === 0x2028 || c === 0x2029) { + break; + } + indentStart--; + // not white space + if (c !== 9 && c !== 11 && c !== 12 && c !== 32 && c !== 160) { + indentEnd = indentStart; + } + } + return str.substring(indentStart, indentEnd); +}