diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 9f926a87dba..00000000000 --- a/.babelrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "presets": [ - "es2015", - "react" - ], - "plugins": [ - "syntax-async-functions", - "transform-class-properties", - "transform-object-rest-spread" - ] -} diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000000..4c86570bcd4 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,5 @@ +last 2 versions +Firefox ESR +not dead +not IE 11 +not ios 10 diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000000..4f3b76b096b --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/master/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000000..4a40bb63b8e --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "graphql/graphiql" }], + "commit": false, + "linked": [], + "access": "public", + "baseBranch": "main", + "ignore": ["example-*"], + "updateInternalDependencies": "patch", + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "updateInternalDependents": "always" + } +} diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 00000000000..445e25f19e8 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,22 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: '30...100' + + status: + project: + default: + target: 50% # the minimum required coverage value + threshold: .1% # the leniency in hitting the target, allows 1% drop + patch: + default: + informational: true + +comment: # this is a top-level key + layout: 'reach, diff, flags, files' + behavior: default + require_changes: true # if true: only post the comment if coverage changes diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..0385e34309a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000000..b248138fd9b --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +.changeset/ + +# ignore working-group dir markdown so it's easier for people to edit from the UI +working-group/ +packages/codemirror-graphql/src/__tests__/schema-kitchen-sink.graphql +CHANGELOG.md +**/CHANGELOG.md diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 37c516fcca3..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,256 +0,0 @@ -{ - "parser": "babel-eslint", - - "plugins": [ - "babel", - "react" - ], - - "settings": { - "react": { - "version": "0.15.0" - } - }, - - "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": { - "babel/no-await-in-loop": 2, - - "react/no-deprecated": 2, - "react/no-did-mount-set-state": 2, - "react/no-did-update-set-state": 2, - "react/no-direct-mutation-state": 2, - "react/no-string-refs": 2, - "react/no-unknown-property": 2, - "react/prefer-es6-class": 2, - "react/prefer-stateless-function": 2, - "react/prop-types": [2, {ignore: ["children"]}], - "react/react-in-jsx-scope": 2, - "react/self-closing-comp": 2, - "react/sort-comp": [2, {"order": [ - "static-methods", - "lifecycle", - "render", - "everything-else" - ]}], - "react/jsx-boolean-value": 2, - "react/jsx-closing-bracket-location": [2, {"nonEmpty": "after-props"}], - "react/jsx-curly-spacing": 2, - "react/jsx-equals-spacing": 2, - "react/jsx-handler-names": 2, - "react/jsx-indent-props": [2, 2], - "react/jsx-indent": [2, 2], - "react/jsx-key": 2, - "react/jsx-no-duplicate-props": 2, - "react/jsx-no-literals": 2, - "react/jsx-no-undef": 2, - "react/jsx-pascal-case": 2, - "react/jsx-space-before-closing": 2, - "react/jsx-uses-react": 2, - "react/jsx-uses-vars": 2, - - "array-bracket-spacing": [2, "always"], - "arrow-parens": [2, "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": [2, {"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}], - "keyword-spacing": 2, - "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-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": [2, {"allowLoop": true}], - "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-useless-escape": 2, - "no-useless-return": 2, - "no-var": 2, - "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": 2, - "prefer-reflect": 0, - "prefer-spread": 0, - "quote-props": [2, "as-needed", {"numbers": true}], - "quotes": [2, "single"], - "radix": 2, - "require-yield": 2, - "semi": [2, "always"], - "semi-spacing": [2, {"before": false, "after": true}], - "sort-vars": 0, - "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-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/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000000..0ed4b2c7de2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,487 @@ +/** + * Copyright (c) Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +const RESTRICTED_IMPORTS = [ + { name: 'graphql/type', message: 'use `graphql`' }, + { name: 'graphql/language', message: 'use `graphql`' }, + { name: 'graphql/type/introspection', message: 'use `graphql`' }, + { name: 'graphql/type/definition', message: 'use `graphql`' }, + { name: 'graphql/type/directives', message: 'use `graphql`' }, + { name: 'graphql/version', message: 'use `graphql`' }, + { + name: 'monaco-editor', + message: + '`monaco-editor` imports all languages; use `monaco-graphql/esm/monaco-editor` instead to import only `json` and `graphql` languages', + }, +]; + +module.exports = { + root: true, + reportUnusedDisableDirectives: true, + ignorePatterns: [ + 'react-app-env.d.ts', + 'next-env.d.ts', + 'changesets/**/*.md', + '**/CHANGELOG.md', + ], + overrides: [ + { + // Rules for all code files + files: ['**/*.{js,jsx,ts,tsx}'], + parserOptions: { + ecmaVersion: 6, + }, + settings: { + react: { + version: 'detect', + }, + }, + // https://github.com/sindresorhus/globals/blob/master/globals.json + env: { + atomtest: true, + es6: true, + node: true, + browser: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:react/jsx-runtime', + 'prettier', + ], + plugins: [ + 'promise', + 'sonarjs', + 'unicorn', + '@arthurgeron/react-usememo', + 'sonar', + '@shopify', + ], + globals: { + atom: false, + document: false, + window: false, + monaco: true, + Map: true, + Set: true, + }, + rules: { + '@shopify/prefer-early-return': ['error', { maximumStatements: 2 }], + '@shopify/prefer-class-properties': 'off', // enable after https://github.com/Shopify/web-configs/issues/387 will be fixed + 'sonarjs/no-inverted-boolean-check': 'error', + '@arthurgeron/react-usememo/require-usememo': [ + 'error', + { checkHookCalls: false }, + ], + // Possible Errors (http://eslint.org/docs/rules/#possible-errors) + 'no-console': 'error', + 'no-constant-binary-expression': 'error', + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-extra-parens': 'off', + 'no-template-curly-in-string': 'off', + 'valid-jsdoc': 'off', + + // Best Practices (http://eslint.org/docs/rules/#best-practices) + 'accessor-pairs': 'error', + 'array-callback-return': 'off', + 'block-scoped-var': 'off', + 'class-methods-use-this': 'off', + complexity: 'off', + 'consistent-return': 'off', + curly: 'error', + 'default-case': 'off', + 'dot-notation': 'error', + eqeqeq: ['error', 'allow-null'], + 'guard-for-in': 'off', + 'no-alert': 'error', + 'no-await-in-loop': 'error', + 'no-caller': 'error', + 'no-case-declarations': 'off', + 'no-div-regex': 'error', + 'no-else-return': ['error', { allowElseIf: false }], + 'no-eq-null': 'off', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-floating-decimal': 'off', // prettier --list-different + 'no-implicit-coercion': 'error', + 'no-implicit-globals': 'off', + 'no-implied-eval': 'error', + 'no-invalid-this': 'off', + 'no-iterator': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-loop-func': 'off', + 'no-magic-numbers': 'off', + 'no-multi-str': 'off', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-new': 'error', + 'no-octal-escape': 'error', + 'no-param-reassign': 'error', + 'no-proto': 'error', + 'no-restricted-properties': 'off', + 'no-return-assign': 'error', + 'no-return-await': 'error', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unmodified-loop-condition': 'off', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-useless-return': 'off', + '@typescript-eslint/prefer-optional-chain': 'error', + 'no-warning-comments': 'off', + radix: 'error', + 'require-await': 'off', + 'vars-on-top': 'off', + yoda: 'error', + 'unicorn/prefer-string-slice': 'error', + 'sonarjs/no-identical-functions': 'error', + 'sonarjs/no-unused-collection': 'error', + 'sonarjs/no-extra-arguments': 'error', + 'unicorn/no-useless-undefined': 'error', + 'no-var': 'error', + // Strict Mode (http://eslint.org/docs/rules/#strict-mode) + strict: 'off', + + // Variables (http://eslint.org/docs/rules/#variables) + 'init-declarations': 'off', + 'no-catch-shadow': 'error', + 'no-label-var': 'error', + 'no-restricted-globals': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + 'no-undef-init': 'off', + 'no-undefined': 'off', + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + varsIgnorePattern: '^React$', + argsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + + 'no-use-before-define': 'off', + + 'unicorn/no-useless-switch-case': 'error', + // Node.js and CommonJS (http://eslint.org/docs/rules/#nodejs-and-commonjs) + 'callback-return': 'off', + 'global-require': 'off', + 'handle-callback-err': 'error', + 'no-mixed-requires': 'error', + 'no-new-require': 'error', + 'no-path-concat': 'error', + 'no-process-env': 'off', + 'no-process-exit': 'off', + 'no-restricted-modules': 'off', + 'no-sync': 'off', + + // Stylistic Issues (http://eslint.org/docs/rules/#stylistic-issues) + camelcase: 'off', + 'capitalized-comments': 'off', + 'consistent-this': 'off', + 'func-name-matching': 'off', + 'func-names': 'off', + 'func-style': 'off', + 'id-blacklist': 'off', + 'id-length': 'off', + 'id-match': 'off', + indent: 'off', + 'line-comment-position': 'off', + 'linebreak-style': 'off', // prettier --list-different + 'lines-around-comment': 'off', + 'lines-around-directive': 'off', + 'max-depth': 'off', + 'max-lines': 'off', + 'max-nested-callbacks': 'off', + 'max-params': 'off', + 'max-statements-per-line': 'off', + 'max-statements': 'off', + 'multiline-ternary': 'off', + 'new-cap': 'off', + 'newline-after-var': 'off', + 'newline-before-return': 'off', + 'newline-per-chained-call': 'off', + 'no-bitwise': 'error', + 'no-continue': 'off', + 'no-inline-comments': 'off', + 'no-mixed-operators': 'off', + 'no-negated-condition': 'off', + 'unicorn/no-negated-condition': 'error', + 'no-nested-ternary': 'off', + 'no-new-object': 'error', + 'no-plusplus': 'off', + 'no-restricted-syntax': [ + 'error', + { + // ❌ useMemo(…, []) + selector: + 'CallExpression[callee.name=useMemo][arguments.1.type=ArrayExpression][arguments.1.elements.length=0]', + message: + "`useMemo` with an empty dependency array can't provide a stable reference, use `useRef` instead.", + }, + { + // ❌ event.keyCode + selector: + 'MemberExpression > .property[type=Identifier][name=keyCode]', + message: 'Use `.key` instead of `.keyCode`', + }, + ], + 'no-ternary': 'off', + 'no-underscore-dangle': 'off', + 'no-unneeded-ternary': 'off', + 'object-curly-newline': 'off', + 'object-property-newline': 'off', + 'one-var-declaration-per-line': 'off', + 'one-var': ['error', 'never'], + 'operator-assignment': 'error', + 'operator-linebreak': 'off', + 'require-jsdoc': 'off', + 'sort-keys': 'off', + 'sort-vars': 'off', + 'spaced-comment': ['error', 'always', { markers: ['/'] }], + 'wrap-regex': 'off', + 'unicorn/prefer-dom-node-remove': 'error', + // ECMAScript 6 (http://eslint.org/docs/rules/#ecmascript-6) + 'arrow-body-style': 'off', + '@typescript-eslint/no-restricted-imports': [ + 'error', + ...RESTRICTED_IMPORTS, + ], + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'off', + 'no-useless-rename': 'error', + 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], + 'object-shorthand': [ + 'error', + 'always', + { avoidExplicitReturnArrows: true }, + ], + 'prefer-numeric-literals': 'off', + 'prefer-template': 'off', + 'sort-imports': 'off', + 'symbol-description': 'error', + + 'sonarjs/no-ignored-return': 'error', + 'unicorn/no-array-push-push': 'error', + 'import/no-extraneous-dependencies': 'error', + 'import/no-duplicates': 'error', + 'import/no-named-as-default': 'error', + 'prefer-object-spread': 'error', + // React rules + 'react/no-unused-state': 'error', + 'react/jsx-curly-brace-presence': 'error', + 'react/jsx-boolean-value': 'error', + 'react/jsx-handler-names': 'error', + 'react/jsx-pascal-case': 'error', + 'react/no-did-mount-set-state': 'error', + 'react/no-did-update-set-state': 'error', + 'react/prop-types': 'off', + 'react/prefer-es6-class': 'error', + 'react/prefer-stateless-function': 'error', + 'react/self-closing-comp': 'error', + 'react/jsx-no-useless-fragment': 'error', + 'react/jsx-filename-extension': [ + 'error', + { extensions: ['.tsx', '.jsx'], allow: 'as-needed' }, + ], + + 'unicorn/no-typeof-undefined': 'error', + 'unicorn/prefer-at': 'error', + 'unicorn/consistent-destructuring': 'error', + 'prefer-destructuring': [ + 'error', + { VariableDeclarator: { object: true } }, + ], + 'promise/no-multiple-resolved': 'error', + 'unicorn/no-zero-fractions': 'error', + 'sonarjs/no-redundant-jump': 'error', + 'unicorn/prefer-logical-operator-over-ternary': 'error', + 'logical-assignment-operators': [ + 'error', + 'always', + { enforceForIfStatements: true }, + ], + 'unicorn/prefer-regexp-test': 'error', + 'unicorn/prefer-export-from': ['error', { ignoreUsedVariables: true }], + 'unicorn/throw-new-error': 'error', + 'unicorn/prefer-includes': 'error', + 'unicorn/no-array-for-each': 'error', + 'unicorn/prefer-dom-node-append': 'error', + 'no-lonely-if': 'error', + 'unicorn/no-lonely-if': 'error', + 'unicorn/prefer-optional-catch-binding': 'error', + 'unicorn/prefer-array-flat-map': 'error', + 'no-unused-expressions': 'off', + '@typescript-eslint/no-unused-expressions': 'error', + 'sonarjs/no-small-switch': 'error', + 'sonarjs/no-duplicated-branches': 'error', + 'sonar/prefer-promise-shorthand': 'error', + 'sonar/no-dead-store': 'error', + 'unicorn/prefer-node-protocol': 'error', + 'import/no-unresolved': ['error', { ignore: ['^node:'] }], + 'unicorn/prefer-string-replace-all': 'error', + 'unicorn/no-hex-escape': 'off', // TODO: enable + // doesn't catch a lot of cases; we use ESLint builtin `no-restricted-syntax` to forbid `.keyCode` + 'unicorn/prefer-keyboard-event-key': 'off', + + 'unicorn/prefer-switch': 'error', + 'unicorn/prefer-dom-node-text-content': 'error', + quotes: ['error', 'single', { avoidEscape: true }], // Matches Prettier, but also replaces backticks with single quotes + // TODO: Fix all errors for the following rules included in recommended config + '@typescript-eslint/no-var-requires': 'off', + }, + }, + { + // Rules that requires type information + files: ['**/*.{ts,tsx}'], + excludedFiles: ['**/*.{md,mdx}/*.{ts,tsx}'], + // extends: ['plugin:@typescript-eslint/recommended-type-checked'], + rules: { + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/non-nullable-type-assertion-style': 'error', + '@typescript-eslint/consistent-type-assertions': 'error', + // TODO: Fix all errors for the following rules included in recommended config + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/triple-slash-reference': 'off', + '@typescript-eslint/no-namespace': 'off', + }, + parserOptions: { + project: [ + 'packages/*/tsconfig.json', + 'examples/*/tsconfig.json', + 'packages/graphiql/cypress/tsconfig.json', + 'tsconfig.eslint.json', + ], + }, + }, + // Cypress plugin, global, etc., only for cypress directory + // https://github.com/cypress-io/eslint-plugin-cypress + // cypress clashes with jest expect() + { + files: ['**/cypress/**'], + extends: 'plugin:cypress/recommended', + rules: { + // Because innerText doesn't return hidden elements and returns new line (\n) characters + 'unicorn/prefer-dom-node-text-content': 'off', + }, + }, + { + // Rules for unit tests + files: [ + '**/__{tests,mocks}__/*.{js,jsx,ts,tsx}', + '**/*.spec.{ts,js.jsx.tsx}', + ], + extends: ['plugin:jest/recommended'], + rules: { + 'jest/no-conditional-expect': 'off', + 'jest/expect-expect': ['error', { assertFunctionNames: ['expect*'] }], + '@arthurgeron/react-usememo/require-usememo': 'off', + }, + }, + { + // Resources are typically our helper scripts; make life easier there + files: ['resources/**', '**/resources/**', 'scripts/**'], + rules: { + 'no-console': 'off', + }, + }, + { + // Disable rules for examples folder + files: ['examples/**'], + rules: { + 'no-console': 'off', + 'no-new': 'off', + 'no-alert': 'off', + 'import/no-unresolved': 'off', + }, + }, + { + // Rule for ignoring imported dependencies from tests files + files: ['**/__tests__/**', 'webpack.config.js'], + rules: { + 'import/no-extraneous-dependencies': 'off', + }, + }, + { + // Rule for allowing import `vscode` package + files: [ + 'packages/vscode-graphql/**', + 'packages/vscode-graphql-execution/**', + ], + rules: { + 'import/no-unresolved': ['error', { ignore: ['^node:', 'vscode'] }], + }, + }, + { + // Rule prefer await to then without React packages because it's ugly to have `async IIFE` inside `useEffect` + files: ['packages/**'], + excludedFiles: ['packages/graphiql/**', 'packages/graphiql-react/**'], + rules: { + 'promise/prefer-await-to-then': 'error', + }, + }, + { + // Monaco-GraphQL rules + files: ['packages/monaco-graphql/**'], + rules: { + '@typescript-eslint/no-restricted-imports': [ + 'error', + ...RESTRICTED_IMPORTS.filter(({ name }) => name !== 'monaco-editor'), + { + name: 'monaco-editor', + message: + '`monaco-editor` imports all languages; use locale `monaco-editor.ts` instead to import only `json` and `graphql` languages', + }, + ], + }, + }, + { + // Parsing Markdown/MDX + files: ['**/*.{md,mdx}'], + parser: 'eslint-mdx', + plugins: ['mdx'], + processor: 'mdx/remark', + settings: { + 'mdx/code-blocks': true, + }, + }, + { + // ❗ALWAYS LAST + // Rules for codeblocks inside Markdown/MDX + files: ['**/*.{md,mdx}/*.{js,jsx,ts,tsx}'], + rules: { + 'import/no-extraneous-dependencies': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'import/no-unresolved': 'off', + 'no-console': 'off', + 'no-undef': 'off', + 'react/jsx-no-undef': 'off', + 'react-hooks/rules-of-hooks': 'off', + '@arthurgeron/react-usememo/require-usememo': 'off', + 'sonar/no-dead-store': 'off', + '@typescript-eslint/no-restricted-imports': 'off', + }, + }, + ], +}; diff --git a/.flowconfig b/.flowconfig deleted file mode 100644 index f04f2f6076d..00000000000 --- a/.flowconfig +++ /dev/null @@ -1,12 +0,0 @@ -[ignore] -.*/css/.* -.*/dist/.* -.*/coverage/.* -.*/resources/.* -.*/node_modules/.* - -[include] - -[libs] - -[options] diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..06bd266a7e7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +*.js text eol=lf +*.jsx text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.md text eol=lf +*.html text eol=lf +*.css text eol=lf +*.sh text eol=lf +*.yml text eol=lf diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..e9c1bea3c3c --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,6 @@ + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..82ff30c4aea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,29 @@ +blank_issues_enabled: true +contact_links: + - name: 'πŸ“š Support/Q&A' + url: https://github.com/graphql/graphiql/discussions/categories/q-a-support + about: >- + Some questions are already answered in our github discussions Q&A section If you don't find an answer, [create a support ticket](ttps://github.com/graphql/graphiql/discussions/new?category=q-a-support) + + + - name: 'πŸ’¬ `graphiql` Discord Support' + url: https://discord.gg/NP5vbPeUFp + about: '`graphiql` Discord Support Channel' + + - name: 'πŸ’¬ `vscode-graphql` Discord Support' + url: https://discord.gg/bHrQtxGNzQ + about: '`vscode-graphql` Discord Support Channel' + + - name: 'πŸ’¬ GraphQL LSP & CLI Discord Support' + url: https://discord.gg/wkQCKwazxj + about: >- + GraphQL LSP (Language Server Protocol) Server Discord Support Channel For `graphql-language-service-server` and/or `graphql-language-service-cli` + + + - name: 'πŸ’¬ `monaco-graphql` Discord Support' + url: https://discord.gg/r4BxrAG6fN + about: '`monaco-graphql` Discord Support Channel' + + - name: 'πŸ’¬ `codedemirror-graphql` Discord Support' + url: https://discord.gg/cffZwk8NJW + about: '`codemirror-graphql` Discord Support Channel' diff --git a/.github/ISSUE_TEMPLATE/graphiql-bug.yml b/.github/ISSUE_TEMPLATE/graphiql-bug.yml new file mode 100644 index 00000000000..eeb396d03d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/graphiql-bug.yml @@ -0,0 +1,65 @@ +name: GraphiQL 🐞 Bug +description: File a bug with the graphiql web editor +title: '[graphiql] ' +labels: [bug, needs triage, graphiql] +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: false + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Do '...' in [repro codesandbox/stackblitz]() + 4. See error... + validations: + required: false + + - type: textarea + attributes: + label: Environment + description: | + examples: + * **GraphiQL Version**: latest + * **OS**: Ubuntu 20.04 + * **Browser**: Chrome 106 + * **Bundler**: vite x.y + * **`react` Version**: 18 + * **`graphql` Version**: 16 + value: | + * GraphiQL Version: + * OS: + * Browser: + * Bundler: + * `react` Version: + * `graphql` Version: + validations: + required: false + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/graphiql-feature.md b/.github/ISSUE_TEMPLATE/graphiql-feature.md new file mode 100644 index 00000000000..f9dc3092ce0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/graphiql-feature.md @@ -0,0 +1,53 @@ +--- +name: GraphiQL Feature Request +about: Request a feature for the graphiql web editor +title: '[graphiql] <title>' +labels: [graphiql, enhancement] +--- + +<!-- + +- Some features can be built with the new sidebar plugins + + We encourage exploring the plugin API prior to opening a feature request: + + SDK hooks + + https://graphiql-test.netlify.app/typedoc/modules/graphiql_react.html + + Sidebar Plugins + + api: + https://graphiql-test.netlify.app/typedoc/modules/graphiql_react.html#graphiqlplugin-2 + + examples: + + https://github.com/graphql/graphiql/tree/main/packages/graphiql-plugin-explorer + + + In the event that the plugin API doesn't allow you to build a feature, it + may be that expanding the plugin API *itself* is the best place for the + feature to be introduced! + + Consider this flexible solution when opening a new feature request + since it also unlocks new opportunities. + +- Prior to opening a feature request, please search for existing requests. + + If you find an existing feature that matches your needs, use the πŸ‘ emote + to show your support for it. If the specifics of your use case are not + covered in the existing feature request but the idea seems similar enough, + please take the time to *add new conversation* which helps the feature's + design evolve. + +- If you do not find any other existing requests for the feature you desire, + you should open a new feature request. Please take the time to help us + understand your use-case as precisely as possible. Be sure to demonstrate + that you've evaluated existing features and found them unsuitable and were + unable to implement the functionality with the plugin API. + + Be flexible in your design and consider slight variations which might + necessitate a specific API design. We also hope you'll be willing to engage + in the on-going design discussion prior to opening a pull-request. +borrowed from the apollo server template +--> diff --git a/.github/ISSUE_TEMPLATE/language-server-bug.yml b/.github/ISSUE_TEMPLATE/language-server-bug.yml new file mode 100644 index 00000000000..8b179448158 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/language-server-bug.yml @@ -0,0 +1,61 @@ +name: Generic IDE (LSP Server) Bug 🐞 +description: File any non syntax highlighting related bugs in vscode or any IDE lsp runtime (vim, intellij, emacs, sublime, etc), with the `graphql-language-service-server` / `graphql-lsp` CLI +title: '[lsp-server] 🐞 <title>' +labels: [bug, lsp-server] +body: + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + + - type: textarea + attributes: + label: Current Behavior + description: A concise description of the issue you're experiencing. + validations: + required: false + + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false + + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this (redacted) graphql config with [filename]... + 3. Do '...' with [lsp client] + 4. See errors in output channel, etc... + validations: + required: false + + - type: textarea + attributes: + label: Environment + description: | + examples: + * **LSP Server Version**: latest + * **OS**: Ubuntu 20.04 + * **LSP Client**: vscode (`vscode-graphql`, etc), intellij-lsp, nvim coc, etc + value: | + * LSP Server Version: + * OS: + * LSP Client: + validations: + required: false + + - type: textarea + attributes: + label: Anything else? + description: | + Links? References? Anything that will give us more context about the issue you are encountering + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. diff --git a/.github/ISSUE_TEMPLATE/language-server-feature.md b/.github/ISSUE_TEMPLATE/language-server-feature.md new file mode 100644 index 00000000000..6fa0779847f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/language-server-feature.md @@ -0,0 +1,30 @@ +--- +name: LSP/CLI Feature Request +about: Request a feature for the `graphql-language-service-server` and/or cli +title: '[lsp-server] <title>' +labels: [lsp-server, enhancement] +--- + +<!-- + +## Current Behavior (if applicable) + + +## Desired Behavior + + +Helpful things to include: + +- screenshots & videos where applicable + +- graphql config sample if related to the problem you are hoping to solve + +- examples of other graphql language tools that support this, if applicable + +- if the feature involves adding support for a feature already in the current spec or proposed working group spec, please include a link to the applicable section of the spec + +## PRs welcome! + +If you find a way to solve this problem by modifying the code in either the source or the distributed code, we are more than happy to accept enhancement requests as PRs! + +--> diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml new file mode 100644 index 00000000000..a06e920817f --- /dev/null +++ b/.github/workflows/canary.yml @@ -0,0 +1,63 @@ +name: Canary Release + +on: + pull_request: + paths-ignore: + - '**.md' + - 'examples' + - '!examples/monaco-graphql-webpack' + +jobs: + publish-canary: + name: Publish Canary + runs-on: ubuntu-latest + if: github.event.pull_request.head.repo.full_name == github.repository + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + - run: yarn install --frozen-lockfile --immutable + + - name: Setup NPM credentials + run: echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Release Canary + id: canary + uses: 'kamilkisiela/release-canary@master' + with: + npm-token: ${{ secrets.NPM_TOKEN }} + npm-script: 'yarn release:canary' + changesets: true + - name: Publish a message + if: steps.canary.outputs.released == 'true' + uses: 'dotansimha/pr-comment@master' + with: + commentKey: canary + message: | + The latest changes of this PR are available as canary in npm (based on the declared `changesets`): + + ``` + ${{ steps.canary.outputs.changesetsPublishedPackages}} + ``` + bot-token: ${{ secrets.GITHUB_TOKEN }} + bot: 'github-actions[bot]' + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Publish a empty message + if: steps.canary.outputs.released == 'false' + uses: 'dotansimha/pr-comment@master' + with: + commentKey: canary + message: | + The latest changes of this PR are not available as canary, since there are no linked `changesets` for this PR. + bot-token: ${{ secrets.GITHUB_TOKEN }} + bot: 'github-actions[bot]' + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/license-check.yml b/.github/workflows/license-check.yml new file mode 100644 index 00000000000..1b813771891 --- /dev/null +++ b/.github/workflows/license-check.yml @@ -0,0 +1,21 @@ +name: License Check +on: + pull_request: + paths: + - 'yarn.lock' + +jobs: + check: + name: License Check + runs-on: ubuntu-20.04 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + - run: yarn install --frozen-lockfile --immutable + + - name: License Check + run: yarn license-check diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 00000000000..69d870c153e --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,29 @@ +name: Lint & Build PR +on: + pull_request: + types: [opened, synchronize] + # TODO: ignore this after setting up a separate task to link markdown + # and to ignore eslint/prettier issues in examples + # paths-ignore: + # - '**.md' + # - 'examples' + # - '!examples/monaco-graphql-webpack' + +jobs: + lint: + name: Lint + runs-on: ubuntu-20.04 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + - run: yarn install --frozen-lockfile --immutable + + - name: TypeScript Build + run: yarn build + + - name: ESLint + run: yarn lint diff --git a/.github/workflows/pr-e2e-tests.yml b/.github/workflows/pr-e2e-tests.yml new file mode 100644 index 00000000000..513b6917e1b --- /dev/null +++ b/.github/workflows/pr-e2e-tests.yml @@ -0,0 +1,23 @@ +name: E2E +on: + pull_request: + types: [opened, synchronize] + paths-ignore: + - '**.md' + - 'examples' + - '!examples/monaco-graphql-webpack' +jobs: + e2e: + name: Cypress E2E Suite + runs-on: ubuntu-20.04 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + - run: yarn install --frozen-lockfile --immutable + + - name: Run E2E suite + run: yarn e2e diff --git a/.github/workflows/pr-graphql-compat-check.yml b/.github/workflows/pr-graphql-compat-check.yml new file mode 100644 index 00000000000..d27a2705b19 --- /dev/null +++ b/.github/workflows/pr-graphql-compat-check.yml @@ -0,0 +1,45 @@ +name: Build & Test PR w/ GraphQL Regressions +on: + push: + # only on merge to main. + # it's rare that this workflow would + # show us an error, but when it does it's important! + branches: + - main + # don't run this regression suite if we don't need to + paths-ignore: + - '**.md' + - 'examples' + - '!examples/monaco-graphql-webpack' + +# TODO: test matrix? + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + build: + name: Build & Test + runs-on: ubuntu-20.04 + strategy: + matrix: + release: ['15.5.3', '^15.8.0', '16.1.0', '16.2.0', '16.3.0'] + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + cache: 'yarn' + node-version: 16 + + - name: Force GraphQL ${{ matrix.release }} solution + run: yarn repo:resolve graphql@${{ matrix.release }} + + - run: yarn install --frozen-lockfile --immutable + + - name: Unit Tests + run: yarn test:ci + + - name: Cypress + run: yarn e2e diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml new file mode 100644 index 00000000000..322dfb313b0 --- /dev/null +++ b/.github/workflows/pr-tests.yml @@ -0,0 +1,32 @@ +name: Test PR +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize] +# paths-ignore: +# - '**.md' +# - 'examples' +# - '!examples/monaco-graphql-webpack' +jobs: + unit: + name: Unit Tests + runs-on: ubuntu-20.04 + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + - run: yarn install --frozen-lockfile --immutable + + - name: Run Unit Tests + run: yarn test:ci + + - uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage/lcov.info + fail_ci_if_error: true # optional (default = false) + verbose: true # optional (default = false) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..c428670768d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,40 @@ +name: Release + +on: + push: + branches: + - main +permissions: {} +jobs: + release: + permissions: + contents: write # for changesets/action to git push + environment: deploy + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-node@v3 + with: + node-version: 16 + cache: 'yarn' + - run: yarn install --frozen-lockfile --immutable + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + version: yarn ci:version + # This expects you to have a script called release which does a build for your packages and calls changeset publish + publish: yarn release + env: + # only use GH token here, because GITHUB_TOKEN is no longer allowed to create PRs + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # for vscode marketplace, see https://github.com/microsoft/vscode-vsce/blob/194d59b975523696362ead891dc0f3ddd277b3bd/README.md#linux + VSCE_PAT: ${{ secrets.VSCE_PAT }} + # for ovsx, see https://github.com/eclipse/openvsx/blob/master/cli/README.md#publish-extensions + OVSX_PAT: ${{ secrets.OPEN_VSX_TOKEN }} diff --git a/.gitignore b/.gitignore index 84005895cc3..b6865f3e8a3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,51 @@ *~ .*.haste_cache.* .DS_Store -npm-debug.log +.secrets +*.log node_modules/ coverage/ +.nyc_output dist/ -/graphiql.js -/graphiql.min.js -/graphiql.css +esm/ +out/ +*.vsix +*.tsbuildinfo + +yarn-1.18.0.js +*.orig +.idea/ + +# Local Netlify folder +.netlify/ + +examples/*/yarn.lock +package-lock.json +.eslintcache +.cspellcache + +vite.config.d.ts +vite.config.js + +.next/ +.turbo/ +types/ +packages/codemirror-graphql/cm6-legacy/ +packages/codemirror-graphql/results/ +packages/codemirror-graphql/utils/ +packages/codemirror-graphql/variables/ +packages/codemirror-graphql/*.js +packages/codemirror-graphql/*.d.ts +packages/codemirror-graphql/*.map +!packages/codemirror-graphql/*.config.js + +packages/graphiql/index.html +packages/graphiql/dev.html +packages/graphiql/analyzer.html +packages/graphiql/graphiql*.js +packages/graphiql/*.css +packages/graphiql/*.map +packages/graphiql/cypress/screenshots/ +packages/graphiql/typedoc/ +packages/graphiql/webpack/ diff --git a/.mailmap b/.mailmap new file mode 100644 index 00000000000..35256b28354 --- /dev/null +++ b/.mailmap @@ -0,0 +1,4 @@ +Angel Gomez Salazar <agomezs@fb.com> +Angel Gomez Salazar <agomezs@fb.com> Angel Gomez <AGS-@users.noreply.github.com> +Angel Gomez Salazar <agomezs@fb.com> angel.gomez <angelegomezsd@gmail.com> +Hyohyeon Jeong <asiandrummer@fb.com> Hyo Jeong <asiandrummer@users.noreply.github.com> diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000000..9e9db784a99 --- /dev/null +++ b/.npmignore @@ -0,0 +1,20 @@ +.* +*.swp +*~ +*.iml +.*.haste_cache.* +.DS_Store +.idea +npm-debug.log + +.babelrc +CONTRIBUTING.md +node_modules +coverage +resources +src +packages +packages/graphiql/*.html +cypress.json +babel.config.js +**/*.tsbuildinfo diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000000..d38f53c764a --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +access=public diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000000..19c7bdba7b1 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +16 \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000000..a219e18edf6 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,15 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid", + "proseWrap": "never", + "overrides": [ + { + "files": ["*.md", "*.mdx"], + "options": { + "printWidth": 80, + "proseWrap": "preserve" + } + } + ] +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3d07e6b6244..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -language: node_js - -node_js: stable - -before_install: - - npm config set spin false --global - -cache: yarn - -deploy: - - provider: npm - skip_cleanup: true - email: "lee@leebyron.com" - api_key: - secure: "aN2Hz+7Z5sCbC9V5iGdjS3oKUMl5Ix0/9WuqxrJjAngPSQMchTXMsGW+XB+5vOeYqRS/lLRe9wUGfuwWUVgl3Pu4UOeHKApPryva55ImalIkcexCAynBJDEbPkW8HB36TAQIPu/h9xVhSuyYlq9BnApb9Jxfgy/oYsF49YXmed+BUOPaln9z0xSW1jP6mzkfzzg87C0UZhDB+D7IiPW6zPAWs9oDSHm9iC4PCW5PXcJqslC6qjMH3HCHuZI1FB419JoAlsiiKEgIq+cJt5BM/qYbui2N4CHP0EhRmPmg3pM4mpf3mf1JVz94XNCYQu9z2LQr6qiLv8sX+fzkDiLHYPJqqeGlqRre3mFUvP2HcUKpV+whhw0ljDV01JjhIEvGgEmTDBMhSlhd5lYjlZJvaaZH+WD+L9xYxAM6wRl/O9IuJ/J4WJ4ojLJTAP8T5hwKFTIGzBIkzWQnILeRv3eepEe1ZYMLiQb3tqRF+khzjGdCl2bD7no5qVW0ghKEHNzp9Tb9vKW4O3UN9xMibf10BSH6OeVoRVVdrnoWSP2Qc/fRBMoSJ14WOtc2GpFTso3cE+N3WF/e1sqDsq7DhqSMK3CHIrElPVd712vOUYltaoUkGWapkAUT8qwYaYf3XvKHnc+Uo4RGq5zyvWCqwTTsWYDVXRMqIwrnenOL7wlX9t0=" - on: - tags: true - branch: master - - provider: releases - skip_cleanup: true - api_key: - secure: "HzZELTrnObaY3L/Qf+sgxtzwZM8KpNNB3CzxHjkihIkl8bt2ibUgvPrArsUisyGI5WXiW6KMzyjDEmODjyT61InCUzXwdqtH7cpcYL1hLrCXUK1MH2yobesMlqcZiu1BdXgYPfhxA9BzA8aQmSmcnIj1+8nVh2ZRYcPE4Lct3eExzLKUr4hvDoGhfStEknLcuSaiICQXqVRQbnVclR/D5fZ77Nt/XdX6yTPd5+BqtEaLXM8rO1zCmD23QNrgHl9lOzIlc7hJ87TQ2t58f1l3nqxaDQieNGctM8ahrPHfYxKX2MZsuKgNEtBmsm+ldnaI2yKoFZNiBY4nEy35beqLROHihisxMo5+kw5L6f7aQ4YpI8lrmeWXUjHBIv9lpnuo8DVO2sBwhvywNhcHqLMX5wt6akh8HSdERpUxcA9cIPVY3vi3tGIJTxidxZUqFHM83Bvh54pss5Afi9HWodTIP+0/HtB7WdKBXH0F4CefuLDhy4aVb5tMo+bi1ENhjpdjl3o+VshkEYXOP2Ge8DAoatFqgXjrc/OQS+Ca7n/75dGwZVBGsX0Gg2Bp+zaWIp5xgqImclsBuk+879/tHUitxt4ZjkWH8E07voiU7WIrmw/BX/kQR3l5T1ROjjtJg1dGuRTSryjCzg+qx3ssXmf4szsaSgOTTPdMgI6HY5cJxEQ=" - file: - - "graphiql.js" - - "graphiql.min.js" - - "graphiql.css" - on: - tags: true - branch: master diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..a6a38fcad01 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,54 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "VS Code LSP Extension: Run", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-graphql" + ], + "outFiles": [ + "${workspaceFolder}/packages/vscode-graphql/out/extension.js" + ], + "sourceMaps": true, + "preLaunchTask": "watch-vscode" + }, + { + "type": "node", + "name": "jest watch", + "request": "launch", + "program": "${workspaceFolder}/node_modules/jest/bin/jest", + "args": [ + "--config", + "jest.config.js", + "--color", + "--runInBand", + "--watch", + "${relativeFile}" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "name": "VS Code Exec Extension: Run", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/packages/vscode-graphql-execution" + ], + "outFiles": [ + "${workspaceFolder}/packages/vscode-graphql-execution/out/extension.js" + ], + "sourceMaps": true, + "preLaunchTask": "watch-vscode-exec" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..ad94bc76410 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "npm.packageManager": "yarn" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000000..fb5c26d593e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "watch-vscode", + "type": "npm", + "script": "watch-vscode", + "problemMatcher": ["$tsc-watch"], + "isBackground": true, + "presentation": { + "reveal": "always" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "watch-vscode-exec", + "type": "npm", + "script": "watch-vscode-exec", + "problemMatcher": ["$tsc-watch"], + "isBackground": true, + "presentation": { + "reveal": "always" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 00000000000..c37fc613a50 --- /dev/null +++ b/.yarnrc @@ -0,0 +1,3 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000000..615fc7e4002 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,153 @@ +> **Archived** For up to date changelogs that are automatically generated by [changesets](https://github.com/atlassian/changesets), see CHANGELOG.md files in respective workspaces. For example, the `graphiql` changelog is located at [packages/graphiql/CHANGELOG.md](./packages/graphiql/CHANGELOG.md), and the language server changelog is located at [packages/graphql-language-service-server/CHANGELOG.md](./packages/graphql-language-service-server/CHANGELOG.md) + +## GraphiQL 0.14.2 - 11 Aug, 2019 + +### Fixes + +- Fix SSR & use of window when introducing new `extraKeys` capability (#942) + +## GraphiQL 0.14.0 - 11 Aug, 2019 + +### Features + +- Add defaultVariableEditorOpen prop (#744) - @acao + +### Fixes + +- Fix formatting of subscription errors - #636, #722 - @benjie +- preserve ctrl-f key for macOS - #759 - @pd4d10 +- Fix earlier 'Mode graphql failed to advance stream' on Linux by eating an exotic whitespace character - #735 closed by #932 - @benjie +- Fix: check `this.editor` exists before `this.editor.off` in QueryEditor + +## Codemirror GraphQL - 0.9 - 11 Aug, 2019 + +### Chores + +- BREAKING: Update to gls-interface and gls-parser ^2.1 +- BREAKING: Deprecate support for GraphQL 0.11 and below +- BREAKING: introduce MIT license +- BREAKING: Support GraphQL 14 + +## GraphQL Language Service Server 2.1.0 - 11 Aug, 2019 + +### Features + +- Replace babylon with @babel/parser (#879) @ganemone +- Add support for gql template tags (#883) @ganemone @Neitsch + +### Chores + +- BREAKING: remove incompatible dependencies on graphql 0.11 and below +- BREAKING: add peer support for graphql 14.x +- BREAKING: change copyright to MIT +- update formatting for monorepo eslint/prettier rules +- update readme, badges + +## GraphQL Language Service Parser 2.1.0 - 11 Aug, 2019 + +### Fixes + +- Fix 'mode graphql failed to advance stream' error from shift-alt-space, etc - #932 - @benjie + +## GraphQL Language Service Interface 2.1.0 - 11 Aug, 2019 + +### Features + +- add \_\_typename field suggestion against object type - (#903) @yoshiakis +- Update sortText logic, so that field sort is schema driven rather than alphabetically sorted - (#884) @ganemone + +### Chores + +- BREAKING: add peer support for graphql 14.x +- MINOR BREAKING: Use MIT license +- add test case for language service hover - @divyenduz @AGS- + +## GraphQL Language Service 2.1.0 + +- BREAKING: add peer support for graphql 14.x +- BREAKING: remove incompatible dependencies on graphql 0.11 and below (b/c of gls-utils 2.x) + +## GraphQL Language Service Utils 2.1.0 - 11 Aug, 2019 + +### Chores + +- BREAKING: change copyright to MIT +- update formatting for monorepo eslint/prettier rules +- update readme, badges + +## GraphQL Language Service Types 1.3.0 - 11 Aug, 2019 + +### Chores + +- BREAKING: change copyright to MIT +- BREAKING: add peer support for graphql 14.x +- update formatting for monorepo eslint/prettier rules +- update readme, badges + +## GraphiQL 0.13.2 - 21 June, 2019 + +### Features + +- Hint/popup/etc DOM nodes use container rather than creating children of + <body> - #791 - @codestryke +- Add readOnly prop and pass to `QueryEditor` and `VariableEditor` - #718 - @TheSharpieOne +- Add operationName to introspection query - #664 - @jbblanchet +- Image Preview Functionality - #789 - @dunnbobcat @asiandrummer + +### Fixes + +- Destroy image hover tooltip when it isn't needed - #874 - @acao +- Copy non-formatted query to avoid stripping out comments - #832 - @jaebradley +- Normalizes no-break spaces - #781 - @zouxuoz +- Prevents crashing on Shift-Alt-Space - #781 - @zouxuoz +- Fix UI state change after favorite a query - #747 - @benjie + +### Chores + +- BREAKING: Upgrade to `codemirror-graphql` 0.8.3 - #773 - @jonaskello +- BREAKING: Change copyright to GraphQL Contributors, License to MIT +- Netlify deployments per PR - @orta +- Add unit test coverage +- Switch to Jest + +## Codemirror Graphql Mode 0.8.4 - 11 Aug, 2018 + +You will now be importing async methods from gls-interface 2.0.0, thus your bundler will require regenerator runtime + +## Chores + +- BREAKING - Use GLS interface/parser 2.1.0 for graphql 14 +- BREAKING - This introduces async/await + +## GraphQL Language Service Interface 2.0.0 - 11 Sep, 2018 + +### Chores + +- BREAKING: upgrade internal dependencies - gls-parser, gls-types, and gls-utils to 2.0.0 - @lostplan +- BREAKING: remove incompatible dependencies on graphql 0.11 and below - @lostplan + +## GraphQL Language Service Utils 2.0.0 - 11 Sep, 2018 + +### Chores + +- BREAKING: deprecate support for graphql-js 0.11.x and below - @lostplan [graphql/graphql-language-service#256](https://github.com/graphql/graphql-language-service/pull/256) [new ref](https://github.com/graphql/graphiql/commit/895e68537fd802b8b6ddf2578a1f76f85982c773) because of [this change](https://github.com/graphql/graphiql/commit/068c57fdb4a147be3c2fc38167e2def74d217a82#diff-696ceb17e38e4a274d4a149d24513b78) +- BREAKING: GraphQL 14.x support, peer dependency resolutions - #244 - @AGS- + +## GraphQL Language Service Utils 1.2.2 - 11 Sep, 2018 + +### Chores + +- add graphql-js 0.13 to peer deps of types package (graphql/graphql-language-service#241) + +## GraphQL Language Service Server 2.0.0 - 11 Sep, 2019 + +### Chores + +- add graphql-js 0.13 to peer dependencies (graphql/graphql-language-service#241) +- BREAKING: upgrade internal dependencies - gls-interface, gls-server and gls-utils to 2.0.0 @lostplan + +## GraphQL Language Service 2.0.0 - 11 Sep, 2018 + +### Chores + +- BREAKING: upgrade internal dependencies - gls-interface, gls-server and gls-utils to 2.0.0 @Sol diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c82cbc5cfd..ed964202e1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,16 @@ -Contributing to GraphiQL -======================== +# Contributing -We want to make contributing to this project as easy and transparent as -possible. Hopefully this document makes the process for contributing clear and -answers any questions you may have. If not, feel free to open an -[Issue](https://github.com/facebook/graphql/issues). +We welcome contributions and assistance! If you want to know where to start, +check out our +[Github Projects sorted by name](https://github.com/graphql/graphiql/projects?query=is%3Aopen+sort%3Aname-asc). + +If you want to add a new feature, note that GraphiQL is eventually going to +support its own extension system, and we are rarely adding new features, so make +sure you submit feature requests with that in mind. + +## Development + +To get setup for development, refer to [DEVELOPMENT.md](./DEVELOPMENT.md) ## Issues @@ -12,76 +18,52 @@ We use GitHub issues to track public bugs and requests. Please ensure your bug description is clear and has sufficient instructions to be able to reproduce the issue. The best way is to provide a reduced test case on jsFiddle or jsBin. -Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe -disclosure of security bugs. In those cases, please go through the process -outlined on that page and do not file a public issue. - ## Pull Requests -All active development of GraphiQL happens on GitHub. We actively welcome +All active development of this project happens on GitHub. We actively welcome your [pull requests](https://help.github.com/articles/creating-a-pull-request). -### Considered Changes - -Since GraphiQL is used both internally at Facebook and by a broad group -externally, changes which are of obvious benefit are prioritized and changes -which are specific to only some usage of GraphiQL should first consider if they -may use the existing customization hooks or if they should expose a new -customization hook. - -### Contributor License Agreement ("CLA") - -In order to accept your pull request, we need you to submit a CLA. You only need -to do this once to work on any of Facebook's open source projects. - -Complete your CLA here: <https://code.facebook.com/cla> - -### Getting Started - -1. Fork this repo by using the "Fork" button in the upper-right - -2. Check out your fork - - ```sh - git clone git@github.com:yournamehere/graphiql.git - ``` - -3. Install or Update all dependencies - - ```sh - npm install - ``` +### Type Prefixes + +[a list of type prefixes](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional#type-enum) +is available: + +```json +[ + "build", + "ci", + "chore", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test" +] +``` -4. Get coding! If you've added code, add tests. If you've changed APIs, update - any relevant documentation or tests. Ensure your work is committed within a - feature branch. +of these, `fix` and `feat` can trigger patch and minor version releases, +reflexively. the rest are useful to help track activity. -5. Ensure all tests pass +another commit message that can trigger a major version bump is this: - ```sh - npm test - ``` +``` +feat: introduce new `fooBar()` API, break `foo()` api -## Release on NPM +- list changes -*Only core contributors may release to NPM.* +BREAKING CHANGE: break `foo()` api +``` -To release a new version on NPM, first ensure you're on the `master` branch and -have recently run `git pull` and that all tests pass with `npm test`. -Use `npm version patch|minor|major` in order to increment the version in -package.json and tag and commit a release. Then `git push --follow-tags` -this change so Travis CI can deploy to NPM. *Do not run `npm publish` directly.* -Once published, add [release notes](https://github.com/graphql/graphql-js/tags). -Use [semver](http://semver.org/) to determine which version part to increment. +notice the non breaking spaces between header and footer. -Example for a patch release: +## Releasing -```sh -npm version patch -git push --follow-tags -``` +Please see [the RELEASING.md document](./RELEASING.md). ## License -By contributing to GraphiQL, you agree that your contributions will be -licensed under the LICENSE file in the project root directory. +By contributing to GraphiQL, you agree that your contributions will be licensed +under the LICENSE file in the project root directory. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000000..b4c17ab4d7b --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,107 @@ +### Getting Started + +Please note that we require a signed GraphQL Specification Membership agreement +before landing a contribution. This is checked automatically when you open a PR. +If you have not signed the membership agreement (it's free), you will be +prompted by the EasyCLA bot. For more details, please see the +[GraphQL WG repo](https://github.com/graphql/graphql-wg/tree/main/membership). + +0. First, you will need the latest `git`, `yarn` 1.16, & `node` 12 or greater. + macOS, Windows and Linux should all be supported as build environments. + +_**Note:** None of the commands below will work with `npm`. Please use `yarn` in +this repo._ + +1. Fork this repo by using the "Fork" button in the upper-right + +2. Check out your fork + + ```sh + git clone git@github.com:your-name-here/graphiql.git + ``` + +3. Install or Update all dependencies + + ```sh + yarn + ``` + +4. Build all interdependencies so the project you are working on can resolve + other packages + + First you'll need β€” + + ```sh + yarn build + ``` + + β€” or β€” + + ```sh + yarn build:watch + ``` + + If you are focused on GraphiQL development, you can run β€” + + ```sh + yarn start-graphiql + ``` + +5. Get coding! If you've added code, add tests. If you've changed APIs, update + any relevant documentation or tests. Ensure your work is committed within a + feature branch. + +6. Ensure all tests pass, and build everything + + ```sh + yarn test + ``` + +### Fix CI issues with linting + +If you have `prettier` or `eslint --fix`-able issues you see in CI, use β€” + +`yarn format` + +If you see `typescript` build issues, do a `yarn build` locally, and make sure +the whole project references tree builds. Changing interfaces can end up +breaking their implementations. + +### Run tests for GraphiQL: + +- `yarn test graphiql` will run all tests for graphiql. You can also run tests + from a workspace, but most tooling is at the root. +- `yarn test --watch` will run `jest` with `--watch` +- `yarn e2e` at the root will run the end-to-end suite +- `yarn start-monaco` will launch `webpack` dev server for the `monaco` editor + example with GitHub API from the root. This is the fastest way to test changes + to `graphql-language-service-interface`, parser, etc. + +If you want these commands to watch for changes to dependent packages in the +repo, then run `yarn build --watch` alongside either of these. + +### Developing for GraphiQL + +If you want to develop just for graphiql, you won't need to execute commands +from the package subdirectory at `packages/graphiql`. + +First, you'll need to `yarn build` all the packages from the root. + +Then, you can run these commands: + +- `yarn start-graphiql` β€” which will launch `webpack` dev server for graphiql + from the root + +> The GraphiQL UI is available at http://localhost:8080/dev.html + +### Developing Monaco GraphQL + +1. First run `yarn`. +2. run `yarn tsc --watch` to watch `monaco-graphql` and + `graphql-language-service` in one screen session/terminal tab/etc +3. in another session, run `yarn start-monaco` from anywhere in the repository + aside from an individual workspace. +4. alternatively to the webpack example, or in addition, you can run monaco or + next.js examples, though these examples are simpler. They also require their + own `yarn` or `npm install` as they are excluded from the `workspaces` + resolved on global `yarn install` diff --git a/LICENSE b/LICENSE index ad274db4ece..7bbf892a049 100644 --- a/LICENSE +++ b/LICENSE @@ -1,26 +1,21 @@ -LICENSE AGREEMENT For GraphiQL software +MIT License -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. +Copyright (c) GraphQL Contributors -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. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -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: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -Copyright (c) 2015, Facebook, Inc. All rights reserved. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 22e28616715..237dfa2f167 100644 --- a/README.md +++ b/README.md @@ -1,302 +1,274 @@ -GraphiQL -======== +<!-- @format --> -*/ˈɑrafΙ™k(Ι™)l/* A graphical interactive in-browser GraphQL IDE. [Try the live demo](http://graphql-swapi.parseapp.com/). +# GraphQL IDE Monorepo -[![Build Status](https://travis-ci.org/graphql/graphiql.svg?branch=master)](https://travis-ci.org/graphql/graphiql) +> **Security Notice:** All versions of `graphiql` < `1.4.7` are vulnerable to an +> XSS attack in cases where the GraphQL server to which the GraphiQL web app +> connects is not trusted. Learn more +> [in the graphiql `security` docs directory](docs/security) -[![](resources/graphiql.png)](http://graphql-swapi.parseapp.com/) +> **Looking for the [GraphiQL Docs?](packages/graphiql/README.md)**: This is the +> root of the monorepo! The full GraphiQL docs are located at +> [`packages/graphiql`](packages/graphiql) -### Getting started +[![Build Status](https://github.com/graphql/graphiql/workflows/Node.JS%20CI/badge.svg)](https://github.com/graphql/graphiql/actions?query=workflow%3A%22Node.JS+CI%22) +[![Discord](https://img.shields.io/discord/625400653321076807.svg)](https://discord.gg/NP5vbPeUFp) +[![Code Coverage](https://img.shields.io/codecov/c/github/graphql/graphiql)](https://codecov.io/gh/graphql/graphiql) +![GitHub top language](https://img.shields.io/github/languages/top/graphql/graphiql) +![GitHub language count](https://img.shields.io/github/languages/count/graphql/graphiql) +[![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/graphql/graphiql)](https://snyk.io/test/github/graphql/graphiql) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3887/badge)](https://bestpractices.coreinfrastructure.org/projects/3887) -Using a node.js server? Just use [`express-graphql`](https://github.com/graphql/express-graphql)! It can automatically present GraphiQL. Using another GraphQL service? GraphiQL is pretty easy to set up. With `npm`: +## Overview -``` -npm install --save graphiql -``` +GraphiQL is the reference implementation of this monorepo, GraphQL IDE, an +official project under the GraphQL Foundation. The code uses the permissive MIT +license. -Alternatively, if you are using [`yarn`](https://yarnpkg.com/): +Whether you want a simple GraphiQL IDE instance for your server, or a more +advanced web or desktop GraphQL IDE experience for your framework or plugin, or +you want to build an IDE extension or plugin, you've come to the right place! -``` -yarn add graphiql -``` +The purpose of this monorepo is to give the GraphQL Community: -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. +- a to-specification official language service (see: + [API Docs](https://graphiql-test.netlify.app/typedoc)) +- a comprehensive LSP server and CLI service for use with IDEs +- a codemirror mode +- a monaco mode (in the works) +- an example of how to use this ecosystem with GraphiQL. +- examples of how to implement or extend GraphiQL. -```js -import React from 'react'; -import ReactDOM from 'react-dom'; -import GraphiQL from 'graphiql'; -import fetch from 'isomorphic-fetch'; +## [`graphiql`](packages/graphiql#readme) -function graphQLFetcher(graphQLParams) { - return fetch(window.location.origin + '/graphql', { - method: 'post', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(graphQLParams), - }).then(response => response.json()); -} +<!-- prettier-ignore --> +> [![NPM](https://img.shields.io/npm/v/graphiql.svg)](https://npmjs.com/graphiql) +> ![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/graphiql) +> ![npm downloads](https://img.shields.io/npm/dm/graphiql?label=npm%20downloads) +> ![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/graphiql) +> ![npm bundle size (version)](https://img.shields.io/bundlephobia/min/graphiql/latest) +> ![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/graphiql/latest) -ReactDOM.render(<GraphiQL fetcher={graphQLFetcher} />, document.body); -``` +![Screenshot of GraphiQL with Doc Explorer Open](packages/graphiql/resources/graphiql.png) -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. +_/ˈɑrafΙ™k(Ι™)l/_ A graphical interactive in-browser GraphQL IDE. +[Try the live demo](https://graphql.github.io/swapi-graphql). We also have +[a demo using our latest netlify build](http://graphiql-test.netlify.com) for +the `main` branch. -Don't forget to include the CSS file on the page! If you're using npm or yarn, you can find it in `node_modules/graphiql/graphiql.css`, or you can download it from the [releases page](https://github.com/graphql/graphiql/releases). +The GraphiQL IDE, implemented in React, currently using +[GraphQL mode for CodeMirror](packages/codemirror-graphql#readme) & +[GraphQL Language Service](packages/graphql-language-service#readme). -For an example of setting up a GraphiQL, check out the [example](./example) in this repository which also includes a few useful features highlighting GraphiQL's API. +**Learn more about +[GraphiQL in `packages/graphiql/README.md`](packages/graphiql#readme)** +## [`monaco-graphql`](packages/monaco-graphql#readme) -### Features +[![NPM](https://img.shields.io/npm/v/monaco-graphql.svg)](https://npmjs.com/monaco-graphql) +![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/monaco-graphql) +![npm downloads](https://img.shields.io/npm/dm/monaco-graphql?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/monaco-graphql) -* 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. +Provides monaco editor with a powerful, schema-driven graphql language mode. +Uses the `graphql-language-service`directly. +See the [webpack example](examples/monaco-graphql-webpack#readme) for a plain +javascript demo using GitHub API -### Usage - -GraphiQL exports a single React component which is intended to encompass the entire browser viewport. This React component renders the GraphiQL editor. - -```js -import GraphiQL from 'graphiql'; - -<GraphiQL /> -``` +## [`codemirror-graphql`](packages/codemirror-graphql#readme) -GraphiQL supports customization in UI and behavior by accepting React props and children. +[![NPM](https://img.shields.io/npm/v/codemirror-graphql.svg)](https://npmjs.com/codemirror-graphql) +![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/codemirror-graphql) +![npm downloads](https://img.shields.io/npm/dm/codemirror-graphql?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/codemirror-graphql) -**Props:** +![Animated Codemirror GraphQL Completion Example](https://raw.githubusercontent.com/graphql/graphiql/main/packages/codemirror-graphql/resources/example.gif) -- `fetcher`: a function which accepts GraphQL-HTTP parameters and returns a Promise or Observable which resolves to the GraphQL parsed JSON response. +Provides CodeMirror 5 with a parser mode for GraphQL along with a live linter and +typeahead hinter powered by your GraphQL Schema. Uses the +`graphql-language-service`. -- `schema`: a GraphQLSchema instance or `null` if one is not to be used. If `undefined` is provided, GraphiQL will send an introspection query using the fetcher to produce a schema. +## [`cm6-graphql`](packages/cm6-graphql#readme) -- `query`: an optional GraphQL string to use as the initial displayed query, if `undefined` is provided, the stored query or `defaultQuery` will be used. +[![NPM](https://img.shields.io/npm/v/codemirror-graphql.svg)](https://npmjs.com/cm6-graphql) +![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/cm6-graphql) +![npm downloads](https://img.shields.io/npm/dm/cm6-graphql?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/cm6-graphql) -- `variables`: an optional GraphQL string to use as the initial displayed query variables, if `undefined` is provided, the stored variables will be used. +Provides CodeMirror 6 with a full-featured language mode for GraphQL. Uses the `graphql-language-service`. -- `operationName`: an optional name of which GraphQL operation should be executed. +## [`graphql-language-service`](packages/graphql-language-service#readme) -- `response`: an optional JSON string to use as the initial displayed response. If not provided, no response will be initially shown. You might provide this if illustrating the result of the initial query. +[![NPM](https://img.shields.io/npm/v/graphql-language-service.svg)](https://npmjs.com/graphql-language-service) +![npm downloads](https://img.shields.io/npm/dm/graphql-language-service?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/graphql-language-service) -- `storage`: an instance of [Storage][] GraphiQL will use to persist state. Only `getItem` and `setItem` are called. Default: `window.localStorage` +Provides language services for +[`graphql-language-service-server`](packages/graphql-language-service-server#readme) +[`codemirror-graphql`](packages/codemirror-graphql) and +[`monaco-graphql`](packages/monaco-graphql). Previously published separately as +the now-retired `graphql-language-service-interface`, +`graphql-language-service-parser`, `graphql-language-service-utils` and +`graphql-language-service-types`. -- `defaultQuery`: an optional GraphQL string to use when no query is provided and no stored query exists from a previous session. If `undefined` is provided, GraphiQL will use its own default query. +## [`graphql-language-service-server`](packages/graphql-language-service-server#readme) -- `onEditQuery`: an optional function which will be called when the Query editor changes. The argument to the function will be the query string. +[![NPM](https://img.shields.io/npm/v/graphql-language-service-server.svg)](https://npmjs.com/graphql-language-service-server) +![npm downloads](https://img.shields.io/npm/dm/graphql-language-service-server?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/graphql-language-service-server) -- `onEditVariables`: an optional function which will be called when the Query variable editor changes. The argument to the function will be the variables string. +Provides language services for LSP-based IDE extensions using the +`graphql-language-service` -- `onEditOperationName`: an optional function which will be called when the operation name to be executed changes. +## [`graphql.vscode-graphql`](packages/vscode-graphql#readme) -- `onToggleDocs`: an optional function which will be called when the docs will be toggled. The argument to the function will be a boolean whether the docs are now open or closed. +An example implementation of `graphql-language-service-server` for Visual Studio +Code. Available +[on the marketplace](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql). +OVSX fix is pending. -- `getDefaultFieldNames`: an optional function used to provide default fields to non-leaf fields which invalidly lack a selection set. Accepts a GraphQLType instance and returns an array of field names. If not provided, a default behavior will be used. +## [`graphql.vscode-graphql-syntax`](packages/vscode-graphql-syntax#readme) -**Children:** +A new syntax highlighting-only extension for vscode to be used by other vscode +extensions. -* `<GraphiQL.Logo>`: Replace the GraphiQL logo with your own. +## [`graphql.vscode-graphql-execution`](packages/vscode-graphql-execution#readme) -* `<GraphiQL.Toolbar>`: Add a custom toolbar above GraphiQL. +An extension for vscode-graphql that allows inline query execution. -* `<GraphiQL.ToolbarButton>`: Add a button to the toolbar above GraphiQL. +## [`graphql-language-service-cli`](packages/graphql-language-service-cli#readme) -* `<GraphiQL.Footer>`: Add a custom footer below GraphiQL Results. +[![NPM](https://img.shields.io/npm/v/graphql-language-service-cli.svg)](https://npmjs.com/graphql-language-service-cli) +![npm downloads](https://img.shields.io/npm/dm/graphql-language-service-cli?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/graphql-language-service-cli) -### Usage Examples +Provides a CLI for the language service server. -```js -class CustomGraphiQL extends React.Component { - constructor(props) { - super(props); - this.state = { - // REQUIRED: - // `fetcher` must be provided in order for GraphiQL to operate - fetcher: this.props.fetcher, +## Browser & Runtime Support - // OPTIONAL PARAMETERS - // GraphQL artifacts - query: '', - variables: '', - response: '', +Many of these packages need to work in multiple environments. - // GraphQL Schema - // If `undefined` is provided, an introspection query is executed - // using the fetcher. - schema: undefined, +By default, all typescript packages target `es6`. - // Useful to determine which operation to run - // when there are multiple of them. - operationName: null, - storage: null, - defaultQuery: null, +`graphql-language-service-server` and `graphql-language-service-cli` are made +for the node runtime, so they target `es2017` - // Custom Event Handlers - onEditQuery: null, - onEditVariables: null, - onEditOperationName: null, +`codemirror-graphql` and the `graphiql` browser bundle use the +[`.browserslistrc`](./.browserslistrc), which targets modern browsers to keep +bundle size small and keep the language services performant where async/await is +used, and especially to avoid the requirement of `regenerator-runtime` or +special babel configuration. - // GraphiQL automatically fills in leaf nodes when the query - // does not provide them. Change this if your GraphQL Definitions - // should behave differently than what's defined here: - // (https://github.com/graphql/graphiql/blob/master/src/utility/fillLeafs.js#L75) - getDefaultFieldNames: null - }; - } - - _onClickToolbarButton(event) { - alert('Clicked toolbar button!'); - } - - render() { - return ( - <GraphiQL ...this.state> - <GraphiQL.Logo> - Custom Logo - </GraphiQL.Logo> - <GraphiQL.Toolbar> - // GraphiQL.ToolbarButton usage - <GraphiQL.ToolbarButton - onClick={this._onClickToolbarButton} - title="ToolbarButton" - label="Click Me as well!" - /> - // Some other possible toolbar items - <button name="GraphiQLButton">Click Me</button> - <OtherReactComponent someProps="true" /> - </GraphiQL.Toolbar> - <GraphiQL.Footer> - // Footer works the same as Toolbar - // add items by appending child components - </GraphiQL.Footer> - </GraphiQL> - ); - } -} -``` - - -### Query Samples - -**Query** - -GraphQL queries declaratively describe what data the issuer wishes to fetch from whoever is fulfilling the GraphQL query. -``` -query FetchSomeIDQuery($someId: String!) { - human(id: $someId) { - name - } -} -``` -More examples available from: [GraphQL Queries](http://graphql.org/docs/queries/). - -**Mutation** - -Given this schema, -``` -const schema = new GraphQLSchema({ - query: new GraphQLObjectType({ - fields: { - numberHolder: { type: numberHolderType }, - }, - name: 'Query', - }), - mutation: new GraphQLObjectType({ - fields: { - immediatelyChangeTheNumber: { - type: numberHolderType, - args: { newNumber: { type: GraphQLInt } }, - resolve: (function (obj, { newNumber }) { - return obj.immediatelyChangeTheNumber(newNumber); - }) - } - }, - name: 'Mutation', - }) -}); -``` -then the following mutation queries are possible: -``` -mutation TestMutation { - first: immediatelyChangeTheNumber(newNumber: 1) { - theNumber - } -} -``` -Read more in [this mutation test in `graphql-js`](https://github.com/graphql/graphql-js/blob/master/src/execution/__tests__/mutations-test.js). - -[Relay](https://facebook.github.io/relay) has another good example using a common pattern for composing mutations. Given the following GraphQL Type Definitions, -``` -input IntroduceShipInput { - factionId: ID! - shipName: String! - clientMutationId: String! -} - -type IntroduceShipPayload { - faction: Faction - ship: Ship - clientMutationId: String! -} -``` -mutation calls are composed as such: -``` -mutation AddBWingQuery($input: IntroduceShipInput!) { - introduceShip(input: $input) { - ship { - id - name - } - faction { - name - } - clientMutationId - } -} -{ - "input": { - "shipName": "B-Wing", - "factionId": "1", - "clientMutationId": "abcde" - } -} -``` -Read more from [Relay Mutation Documentation](https://facebook.github.io/relay/docs/graphql-mutations.html). - -**Fragment** - -Fragments allow for the reuse of common repeated selections of fields, reducing duplicated text in the document. Inline Fragments can be used directly within a selection to condition upon a type condition when querying against an interface or union. Therefore, instead of the following query: -``` -{ - luke: human(id: "1000") { - name - homePlanet - } - leia: human(id: "1003") { - name - homePlanet - } -} -``` +### [`.browserslistrc`](./.browserslistrc): -using fragments, the following query is possible. ``` -{ - luke: human(id: "1000") { - ...HumanFragment - } - leia: human(id: "1003") { - ...HumanFragment - } -} - -fragment HumanFragment on Human { - name - homePlanet -} +last 2 versions +Firefox ESR +not dead +not IE 11 +not ios 10 +maintained node versions ``` -Read more from [GraphQL Fragment Specification](http://facebook.github.io/graphql/#sec-Language.Fragments). +To be clear, we do _not_ support Internet Explorer or older versions of +evergreen browsers. + +## Development + +To get setup for local development of this monorepo, refer to +[DEVELOPMENT.md](./DEVELOPMENT.md) + +# Contributing to this repo + +This is an open source project, and we welcome contributions. Please see +[CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute. + +This repository is managed by EasyCLA. Project participants must sign the free +[GraphQL Specification Membership agreement](https://preview-spec-membership.graphql.org) +before making a contribution. You only need to do this one time, and it can be +signed by +[individual contributors](http://individual-spec-membership.graphql.org/) or +their [employers](http://corporate-spec-membership.graphql.org/). + +To initiate the signature process please open a PR against this repo. The +EasyCLA bot will block the merge if we still need a membership agreement from +you. + +Please note that EasyCLA is configured to accept commits from certain GitHub +bots. These are approved on an exception basis once we are confident that any +content they create is either unlikely to consist of copyrightable content or +else was written by someone who has already signed the CLA (e.g., a project +maintainer). The bots that have currently been approved as exceptions are: + +- github-actions (exclusively for the `changesets` Action) + +You can find +[detailed information here](https://github.com/graphql/graphql-wg/tree/main/membership). +If you have issues, please email +[operations@graphql.org](mailto:operations@graphql.org). + +## Maintainers + +Maintainers of this repository regularly review PRs and issues and help advance +the GraphiQL roadmap + +### Alumni + +Without these amazing past maintainers, where would we be?! + +- [@leebyron](https://github.com/leebyron) - original author of all libraries +- [@asiandrummer](https://github.com/asiandrummer) - original creator of + GraphiQL +- [@wincent](https://github.com/wincent) - early co-author and maintainer +- [@lostplan](https://github.com/lostplan) - maintained the language service + ecosystem until about 2017 +- [@IvanGoncharov](https://github.com/ivangoncharov) - maintainer and + transitional mentor to @acao and others +- [@orta](https://github.com/orta) - has helped with so many parts of the + project over the years, and provided the original redesign! +- [@divyenduz](https://github.com/divyenduz) - the original creator of + `vscode-graphql`, and contributor to much of the ecosystem. Thanks Divy! + +### Active + +Maintainers who are currently active (to varying degrees, please contact us via +our discord channels!): + +- [@imolorhe](https://github.com/imolorhe) +- [@yoshiakis](https://github.com/yoshiakis) +- [@urigo](https://github.com/urigo) +- [@timsuchanek](https://github.com/timsuchanek) +- [@thomasheyenbrock](https://github.com/thomasheyenbrock) +- [@n1ru4l](https://github.com/n1ru4l) +- [@acao](https://github.com/acao) +- [@stonexer](https://github.com/stonexer) +- [@B2o5T](https://github.com/B2o5T) +- [@dotansimha](https://github.com/dotansimha) +- [@saihaj](https://github.com/saihaj) +- [@jonathanawesome](https://github.com/jonathanawesome) +- [@cshaver](https://github.com/cshaver) + +> Thank you graphql community for all the help & support! I did it all for you, +> and I couldn't have done it without you ❀️ - @acao + +### Fielding Proposals! + +The door is open for proposals for the new GraphiQL Plugin API, and other ideas +on how to make the rest of the IDE ecosystem more performant, scalable, +interoperable and extensible. Feel free to open a PR to create a document in the +`/proposals/` directory. Eventually we hope to move these to a repo that serves +this purpose. + +## Community + +- **Discord** + [![Discord](https://img.shields.io/discord/625400653321076807.svg)](https://discord.gg/NP5vbPeUFp) - + Most discussion outside of GitHub happens on the GraphQL + [Discord Server](https://discord.gg/NP5vbPeUFp) +- **Twitter** - [@GraphiQL](https://twitter.com/@GraphiQL) and + [#GraphiQL](https://twitter.com/hashtag/GraphiQL) +- **GitHub** - Create feature requests, discussions issues and bugs above +- **Working Group** - Yes, you're invited! Monthly planning/decision making + meetings, and working sessions every two weeks on zoom! + [Learn more.](working-group#readme) diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000000..88e2a3d9615 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,3 @@ +# Cutting New Releases + +TODO: Redo for `changesets`. See [`Changesets Readme`](./.changeset/README.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..70f301c9483 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# GraphiQL Ecosystem Security Advisories + +Security Advisories for packages in this repository will be listed here + +## GraphiQL + +### 2021 + +- [Introspection Schema XSS Attack](./docs/security/2021-introspection-schema-xss.md) diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000000..56fedcc08d2 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,42 @@ +// for ESM don't transpile modules + +const envConfig = { + modules: 'commonjs', +}; + +if (process.env.ESM) { + envConfig.modules = false; + envConfig.targets = { node: true }; + envConfig.bugfixes = true; +} + +if (process.env.CDN) { + envConfig.modules = 'umd'; + envConfig.targets = null; +} + +module.exports = { + presets: [ + [require.resolve('@babel/preset-env'), envConfig], + require.resolve('@babel/preset-react'), + require.resolve('@babel/preset-typescript'), + ], + env: { + test: { + presets: [ + [require.resolve('@babel/preset-env'), envConfig], + [require.resolve('@babel/preset-react'), { runtime: 'automatic' }], + require.resolve('@babel/preset-typescript'), + ], + plugins: [require.resolve('babel-plugin-macros')], + }, + development: { + compact: false, + }, + }, + plugins: [ + require.resolve('@babel/plugin-proposal-class-properties'), + require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), + require.resolve('@babel/plugin-proposal-optional-chaining'), + ], +}; diff --git a/cspell.json b/cspell.json new file mode 100644 index 00000000000..3ba26fe6539 --- /dev/null +++ b/cspell.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "language": "en", + "useGitignore": true, + "cache": { + "useCache": true + }, + "dictionaries": ["custom-words"], + "dictionaryDefinitions": [ + { + "name": "custom-words", + "path": "./custom-words.txt", + "addWords": true + } + ], + "ignorePaths": [ + "**/CHANGELOG.md", + "**/package.json", + "**/esbuild.js", + ".eslintrc.js" + ], + "files": ["**/*.{js,cjs,mjs,ts,jsx,tsx,md,mdx,html,json,css,toml,yaml,yml}"] +} diff --git a/css/app.css b/css/app.css deleted file mode 100644 index 758326ca3f5..00000000000 --- a/css/app.css +++ /dev/null @@ -1,492 +0,0 @@ -.graphiql-container { - color: #141823; - display: flex; - flex-direction: row; - font-family: - system, - -apple-system, - 'San Francisco', - '.SFNSDisplay-Regular', - 'Segoe UI', - Segoe, - 'Segoe WP', - 'Helvetica Neue', - helvetica, - 'Lucida Grande', - arial, - sans-serif; - font-size: 14px; - height: 100%; - margin: 0; - overflow: hidden; - width: 100%; -} - -.graphiql-container .editorWrap { - display: flex; - flex-direction: column; - flex: 1; -} - -.graphiql-container .title { - font-size: 18px; -} - -.graphiql-container .title em { - font-family: georgia; - font-size: 19px; -} - -.graphiql-container .topBarWrap { - display: flex; - flex-direction: row; -} - -.graphiql-container .topBar { - align-items: center; - background: linear-gradient(#f7f7f7, #e2e2e2); - border-bottom: 1px solid #d0d0d0; - cursor: default; - display: flex; - flex-direction: row; - flex: 1; - height: 34px; - padding: 7px 14px 6px; - user-select: none; -} - -.graphiql-container .toolbar { - overflow-x: auto; -} - -.graphiql-container .docExplorerShow { - background: linear-gradient(#f7f7f7, #e2e2e2); - border-bottom: 1px solid #d0d0d0; - border-left: 1px solid rgba(0, 0, 0, 0.2); - border-right: none; - border-top: none; - color: #3B5998; - cursor: pointer; - font-size: 14px; - margin: 0; - outline: 0; - padding: 2px 20px 0 18px; -} - -.graphiql-container .docExplorerShow:before { - border-left: 2px solid #3B5998; - border-top: 2px solid #3B5998; - content: ''; - display: inline-block; - height: 9px; - margin: 0 3px -1px 0; - position: relative; - transform: rotate(-45deg); - width: 9px; -} - -.graphiql-container .editorBar { - display: flex; - flex-direction: row; - flex: 1; -} - -.graphiql-container .queryWrap { - display: flex; - flex-direction: column; - flex: 1; -} - -.graphiql-container .resultWrap { - border-left: solid 1px #e0e0e0; - display: flex; - flex-direction: column; - flex: 1; - position: relative; -} - -.graphiql-container .docExplorerWrap { - background: white; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.15); - position: relative; - z-index: 3; -} - -.graphiql-container .docExplorerResizer { - cursor: col-resize; - height: 100%; - left: -5px; - position: absolute; - top: 0; - width: 10px; - z-index: 10; -} - -.graphiql-container .docExplorerHide { - cursor: pointer; - font-size: 18px; - margin: -7px -8px -6px 0; - padding: 18px 16px 15px 12px; -} - -.graphiql-container .query-editor { - flex: 1; - position: relative; -} - -.graphiql-container .variable-editor { - display: flex; - flex-direction: column; - height: 29px; - position: relative; -} - -.graphiql-container .variable-editor-title { - background: #eeeeee; - border-bottom: 1px solid #d6d6d6; - border-top: 1px solid #e0e0e0; - color: #777; - font-variant: small-caps; - font-weight: bold; - letter-spacing: 1px; - line-height: 14px; - padding: 6px 0 8px 43px; - text-transform: lowercase; - user-select: none; -} - -.graphiql-container .codemirrorWrap { - flex: 1; - height: 100%; - position: relative; -} - -.graphiql-container .result-window { - flex: 1; - height: 100%; - position: relative; -} - -.graphiql-container .footer { - background: #f6f7f8; - border-left: 1px solid #e0e0e0; - border-top: 1px solid #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 .toolbar-button { - background: #fdfdfd; - background: linear-gradient(#fbfbfb, #f8f8f8); - border-color: #d3d3d3 #d0d0d0 #bababa; - border-radius: 4px; - border-style: solid; - border-width: 0.5px; - box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.13), inset 0 1px #fff; - color: #444; - cursor: pointer; - display: inline-block; - margin: 0 5px 0; - padding: 2px 8px 4px; - text-decoration: none; -} - -.graphiql-container .toolbar-button:active { - background: linear-gradient(#ececec, #d8d8d8); - border-color: #cacaca #c9c9c9 #b0b0b0; - box-shadow: - 0 1px 0 #fff, - inset 0 1px rgba(255, 255, 255, 0.2), - inset 0 1px 1px rgba(0, 0, 0, 0.08); -} - -.graphiql-container .toolbar-button.error { - background: linear-gradient(#fdf3f3, #e6d6d7); - color: #b00; -} - -.graphiql-container .execute-button-wrap { - height: 34px; - margin: 0 14px 0 28px; - position: relative; -} - -.graphiql-container .execute-button { - background: linear-gradient(#fdfdfd, #d2d3d6); - border-radius: 17px; - border: 1px solid rgba(0,0,0,0.25); - box-shadow: 0 1px 0 #fff; - cursor: pointer; - fill: #444; - height: 34px; - margin: 0; - padding: 0; - width: 34px; -} - -.graphiql-container .execute-button svg { - pointer-events: none; -} - -.graphiql-container .execute-button:active { - 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 .execute-options { - background: #fff; - box-shadow: - 0 0 0 1px rgba(0,0,0,0.1), - 0 2px 4px rgba(0,0,0,0.25); - left: -1px; - margin: 0; - padding: 8px 0; - position: absolute; - top: 37px; - z-index: 100; -} - -.graphiql-container .execute-options li { - cursor: pointer; - list-style: none; - min-width: 100px; - padding: 2px 30px 4px 10px; -} - -.graphiql-container .execute-options li.selected { - background: #e10098; - color: white; -} - -.graphiql-container .CodeMirror-scroll { - overflow-scrolling: touch; -} - -.graphiql-container .CodeMirror { - color: #141823; - font-family: - 'Consolas', - 'Inconsolata', - 'Droid Sans Mono', - 'Monaco', - monospace; - font-size: 13px; - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; -} - -.graphiql-container .CodeMirror-lines { - padding: 20px 0; -} - -.CodeMirror-hint-information .content { - box-orient: vertical; - color: #141823; - display: flex; - font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Segoe UI', Segoe, 'Segoe WP', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif; - font-size: 13px; - 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; - cursor: pointer; - display: inline; - margin-right: 0.5em; -} - -.autoInsertedLeaf.cm-property { - animation-duration: 6s; - animation-name: insertionFade; - border-bottom: 2px solid rgba(255, 255, 255, 0); - border-radius: 2px; - margin: -2px -4px -1px; - padding: 2px 4px 1px; -} - -@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; - border-radius: 2px; - border: 0; - color: #141823; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - font-family: - system, - -apple-system, - 'San Francisco', - '.SFNSDisplay-Regular', - 'Segoe UI', - Segoe, - 'Segoe WP', - 'Helvetica Neue', - helvetica, - 'Lucida Grande', - arial, - sans-serif; - font-size: 13px; - line-height: 16px; - opacity: 0; - padding: 6px 10px; - 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: linear-gradient(#43A8FF, #0F83E8); - box-shadow: - 0 1px 1px rgba(0, 0, 0, 0.2), - inset 0 0 0 1px rgba(0, 0, 0, 0.1); - color: white; - font-family: arial; - font-size: 12px; - line-height: 0; - margin: 0 3px; - padding: 0px 4px 1px; - 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 deleted file mode 100644 index 870ca6b70f7..00000000000 --- a/css/codemirror.css +++ /dev/null @@ -1,352 +0,0 @@ -/* BASICS */ - -.CodeMirror { - /* Set height, width, borders, and global font properties here */ - color: black; - font-family: monospace; - height: 300px; -} - -/* 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 { - color: #999; - min-width: 20px; - padding: 0 3px 0 5px; - text-align: right; - 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 { - background: #7e7; - border: 0; - width: auto; -} -.CodeMirror.cm-fat-cursor div.CodeMirror-cursors { - z-index: 1; -} - -.cm-animate-fat-cursor { - animation: blink 1.06s steps(1) infinite; - border: 0; - width: auto; -} -@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 { - background: white; - overflow: hidden; - position: relative; -} - -.CodeMirror-scroll { - height: 100%; - /* 30px is the magic margin used to hide the element's real scrollbars */ - /* See overflow: hidden in .CodeMirror */ - margin-bottom: -30px; margin-right: -30px; - outline: none; /* Prevent dragging from highlighting the element */ - overflow: scroll !important; /* Things will break if this is overridden */ - padding-bottom: 30px; - position: relative; -} -.CodeMirror-sizer { - border-right: 30px solid transparent; - position: relative; -} - -/* The fake, visible scrollbars. Used to force redraw during scrolling - before actual scrolling happens, thus preventing shaking and - flickering artifacts. */ -.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { - display: none; - position: absolute; - z-index: 6; -} -.CodeMirror-vscrollbar { - overflow-x: hidden; - overflow-y: scroll; - right: 0; top: 0; -} -.CodeMirror-hscrollbar { - bottom: 0; left: 0; - overflow-x: scroll; - overflow-y: hidden; -} -.CodeMirror-scrollbar-filler { - right: 0; bottom: 0; -} -.CodeMirror-gutter-filler { - left: 0; bottom: 0; -} - -.CodeMirror-gutters { - min-height: 100%; - position: absolute; left: 0; top: 0; - z-index: 3; -} -.CodeMirror-gutter { - display: inline-block; - height: 100%; - margin-bottom: -30px; - vertical-align: top; - white-space: normal; - /* Hack to make IE7 behave */ - *zoom:1; - *display:inline; -} -.CodeMirror-gutter-wrapper { - background: none !important; - border: none !important; - position: absolute; - z-index: 4; -} -.CodeMirror-gutter-background { - position: absolute; - top: 0; bottom: 0; - z-index: 4; -} -.CodeMirror-gutter-elt { - cursor: default; - position: absolute; - z-index: 4; -} -.CodeMirror-gutter-wrapper { - user-select: none; -} - -.CodeMirror-lines { - cursor: text; - min-height: 1px; /* prevents collapsing before first draw */ -} -.CodeMirror pre { - -webkit-tap-highlight-color: transparent; - /* Reset some styles that the rest of the page might have set */ - background: transparent; - border-radius: 0; - border-width: 0; - color: inherit; - font-family: inherit; - font-size: inherit; - font-variant-ligatures: none; - line-height: inherit; - margin: 0; - overflow: visible; - position: relative; - white-space: pre; - word-wrap: normal; - z-index: 2; -} -.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 { - overflow: auto; - position: relative; - z-index: 2; -} - -.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 { - box-sizing: content-box; -} - -.CodeMirror-measure { - height: 0; - overflow: hidden; - position: absolute; - visibility: hidden; - width: 100%; -} - -.CodeMirror-cursor { position: absolute; } -.CodeMirror-measure pre { position: static; } - -div.CodeMirror-cursors { - position: relative; - visibility: hidden; - z-index: 3; -} -div.CodeMirror-dragcursors { - visibility: visible; -} - -.CodeMirror-focused div.CodeMirror-cursors { - visibility: visible; -} - -.CodeMirror-selected { background: #d9d9d9; } -.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } -.CodeMirror-crosshair { cursor: crosshair; } -.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } -.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-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; } - -.CodeMirror-dialog { - background: inherit; - color: inherit; - left: 0; right: 0; - overflow: hidden; - padding: .1em .8em; - position: absolute; - z-index: 15; -} - -.CodeMirror-dialog-top { - border-bottom: 1px solid #eee; - top: 0; -} - -.CodeMirror-dialog-bottom { - border-top: 1px solid #eee; - bottom: 0; -} - -.CodeMirror-dialog input { - background: transparent; - border: 1px solid #d3d6db; - color: inherit; - font-family: monospace; - outline: none; - width: 20em; -} - -.CodeMirror-dialog button { - font-size: 70%; -} diff --git a/css/doc-explorer.css b/css/doc-explorer.css deleted file mode 100644 index 51ca2233bd4..00000000000 --- a/css/doc-explorer.css +++ /dev/null @@ -1,161 +0,0 @@ -.graphiql-container .doc-explorer { - background: white; -} - -.graphiql-container .doc-explorer-title-bar { - cursor: default; - display: flex; - height: 34px; - line-height: 14px; - padding: 8px 8px 5px; - position: relative; - user-select: none; -} - -.graphiql-container .doc-explorer-title { - flex: 1; - font-weight: bold; - overflow-x: hidden; - padding: 10px 0 10px 10px; - text-align: center; - text-overflow: ellipsis; - white-space: nowrap; -} - -.graphiql-container .doc-explorer-back { - color: #3B5998; - cursor: pointer; - margin: -7px 0 -6px -8px; - overflow-x: hidden; - padding: 17px 12px 16px 16px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.doc-explorer-narrow .doc-explorer-back { - width: 0; -} - -.graphiql-container .doc-explorer-back:before { - border-left: 2px solid #3B5998; - border-top: 2px solid #3B5998; - content: ''; - display: inline-block; - height: 9px; - margin: 0 3px -1px 0; - position: relative; - transform: rotate(-45deg); - width: 9px; -} - -.graphiql-container .doc-explorer-rhs { - position: relative; -} - -.graphiql-container .doc-explorer-contents { - background-color: #ffffff; - border-top: 1px solid #d6d6d6; - bottom: 0; - left: 0; - min-width: 300px; - overflow-y: auto; - padding: 20px 15px; - position: absolute; - right: 0; - top: 47px; -} - -.graphiql-container .doc-type-description p:first-child , -.graphiql-container .doc-type-description blockquote:first-child { - margin-top: 0; -} - -.graphiql-container .doc-explorer-contents a { - cursor: pointer; - text-decoration: none; -} - -.graphiql-container .doc-explorer-contents a:hover { - text-decoration: underline; -} - -.graphiql-container .doc-value-description { - padding: 4px 0 8px 12px; -} - -.graphiql-container .doc-category { - margin: 20px 0; -} - -.graphiql-container .doc-category-title { - border-bottom: 1px solid #e0e0e0; - color: #777; - cursor: default; - font-size: 14px; - font-variant: small-caps; - font-weight: bold; - letter-spacing: 1px; - margin: 0 -15px 10px 0; - padding: 10px 0; - user-select: none; -} - -.graphiql-container .doc-category-item { - margin: 12px 0; - color: #555; -} - -.graphiql-container .keyword { - color: #B11A04; -} - -.graphiql-container .type-name { - color: #CA9800; -} - -.graphiql-container .field-name { - color: #1F61A0; -} - -.graphiql-container .value-name { - color: #0B7FC7; -} - -.graphiql-container .arg-name { - color: #8B2BB9; -} - -.graphiql-container .arg:after { - content: ', '; -} - -.graphiql-container .arg:last-child:after { - content: ''; -} - -.graphiql-container .doc-alert-text { - color: #F00F00; - font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; - font-size: 13px; -} - -.graphiql-container .search-box-outer { - border: 1px solid #d3d6db; - box-sizing: border-box; - display: inline-block; - font-size: 12px; - height: 24px; - margin-bottom: 12px; - padding: 3px 8px 5px; - vertical-align: middle; - width: 100%; -} - -.graphiql-container .search-box-input { - border: 0; - font-size: 12px; - margin: 0; - outline: 0; - padding: 0; - width: 100%; -} diff --git a/css/foldgutter.css b/css/foldgutter.css deleted file mode 100644 index 1f41e043a75..00000000000 --- a/css/foldgutter.css +++ /dev/null @@ -1,20 +0,0 @@ -.CodeMirror-foldmarker { - color: blue; - cursor: pointer; - font-family: arial; - line-height: .3; - text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; -} -.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 deleted file mode 100644 index 0bb20270a3f..00000000000 --- a/css/lint.css +++ /dev/null @@ -1,69 +0,0 @@ -/* The lint marker gutter */ -.CodeMirror-lint-markers { - width: 16px; -} - -.CodeMirror-lint-tooltip { - background-color: infobackground; - border-radius: 4px 4px 4px 4px; - border: 1px solid black; - color: infotext; - font-family: monospace; - font-size: 10pt; - max-width: 600px; - opacity: 0; - overflow: hidden; - padding: 2px 5px; - position: fixed; - transition: opacity .4s; - white-space: pre-wrap; - white-space: pre; - z-index: 100; -} - -.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; - position: relative; - vertical-align: middle; - width: 16px; -} - -.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { - background-position: top left; - background-repeat: no-repeat; - padding-left: 18px; -} - -.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-position: right bottom; - background-repeat: no-repeat; - width: 100%; height: 100%; -} diff --git a/css/loading.css b/css/loading.css deleted file mode 100644 index 930da9e7669..00000000000 --- a/css/loading.css +++ /dev/null @@ -1,28 +0,0 @@ -.graphiql-container .spinner-container { - height: 36px; - left: 50%; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - width: 36px; - z-index: 10; -} - -.graphiql-container .spinner { - animation: rotation .6s infinite linear; - border-bottom: 6px solid rgba(150, 150, 150, .15); - border-left: 6px solid rgba(150, 150, 150, .15); - border-radius: 100%; - border-right: 6px solid rgba(150, 150, 150, .15); - border-top: 6px solid rgba(150, 150, 150, .8); - display: inline-block; - height: 24px; - position: absolute; - vertical-align: middle; - width: 24px; -} - -@keyframes rotation { - from { transform: rotate(0deg); } - to { transform: rotate(359deg); } -} diff --git a/css/show-hint.css b/css/show-hint.css deleted file mode 100644 index af895fa3b36..00000000000 --- a/css/show-hint.css +++ /dev/null @@ -1,61 +0,0 @@ -.CodeMirror-hints { - background: white; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace; - font-size: 13px; - list-style: none; - margin-left: -6px; - margin: 0; - max-height: 14.5em; - overflow-y: auto; - overflow: hidden; - padding: 0; - position: absolute; - z-index: 10; -} - -.CodeMirror-hints-wrapper { - background: white; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.45); - margin-left: -6px; - position: absolute; - z-index: 10; -} - -.CodeMirror-hints-wrapper .CodeMirror-hints { - box-shadow: none; - margin-left: 0; - position: relative; - 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/custom-words.txt b/custom-words.txt new file mode 100644 index 00000000000..ee1eab2a77d --- /dev/null +++ b/custom-words.txt @@ -0,0 +1,227 @@ +// (user-)names +arthurgeron +Divyendu +Leko +LekoArts +acao +aivazis +akshat +alexey +alok +arminio +asiandrummer +aumy +benjie +bobbybobby +borggreve +bram +cshaver +dhanani +divy +divyenduz +dotan +dotansimha +gillam +goldshtein +goncharov +graphi +harshith +heyenbrock +hurrell +hyohyeon +imolorhe +jeong +jonathanawesome +kumar +leebyron +lostplan +nauroze +nishchit +nowdoc +orta +pabbati +pratap +ravikoti +rikki +rodionov +rohit +saihaj +saihajpreet +scheer +schulte +schuster +sgrove +simha +stonexer +suchanek +tanay +tanaypratap +therox +thomasheyenbrock +timsuchanek +urigo +wincent +yoshiakis + +// packages and tools +argparse +changesets +clsx +codemirror +codesandbox +combobox +delivr +dompurify +esbuild +execa +GraphiQL +headlessui +inno +intellij +jsdelivr +lezer +manypkg +meros +nullthrows +ovsx +picomatch +pnpm +snyk +sonarjs +svgr +typedoc +vite +vitest +vitejs +wonka +urql +tsup +usememo + +// identifiers used in code and configs +acmerc +binti +blockstring +browserslistrc +calar +chainable +codegen +dirpath +envrc +filesfor +flowtests +foldgutter +foldmarker +ghapi +graphqlconfig +graphqlrc +graphqls +htmling +invalidchar +languageservice +linenumber +linenumbers +linkify +listbox +listvalues +marko +matchingbracket +middlewares +modulemap +newhope +nocheck +nocursor +nonmatchingbracket +nrtbf +nulltype +nvim +objectvalues +orche +outdir +outlineable +postbuild +prebuild +quasis +ractive +resi +resizer +runmode +searchcursor +selectionset +sfc's +singleline +socker +squirrelly +streamable +subword +testid +testonly +unfocus +unnormalized +unsubscribable +vash +websockets + +// fonts +fira +menlo +roboto + +// locations +givatayim +jammu +medellΓ­n +vizag + +// companies and organizations +amfoss +colmena +firecamp +gdezerno +grafbase +graphile +novvum +pieas + +// other languages +hola +zdravo +Π—Π΄ΠΎΡ€ΠΎΠ²ΠΎ +Ψ£Ω‡Ω„Ψ§Ω‹ +Ψ³Ω„Ψ§Ω… +ΰ€Ήΰ₯‡ΰ€²ΰ₯‹ + +// phonetic notation +ˈɑrafΙ™k + +// abbreviations +// short for "developer experience": +devx +// short for "maintainers": +maint +// short for "platform as a service": +paas +// these pop up when writing "GraphQL___" +qlapi +qlid +qlide + +// cspell en-us/en-gb edgecases? +behaviour + +// other +architecting +codebases +codespaces +dedenting +exfiltrate +ooops +proto +roadmap +runtimes +typeahead +typeaheads +unparsable +randomthing +codicon +edcore diff --git a/docs/migration/graphiql-2.0.0.md b/docs/migration/graphiql-2.0.0.md new file mode 100644 index 00000000000..7244c423b56 --- /dev/null +++ b/docs/migration/graphiql-2.0.0.md @@ -0,0 +1,292 @@ +# Upgrading `graphiql` from `1.x` to `2.0.0` + +Hello GraphiQL user and thanks for upgrading! + +This migration guide walks you through all changes that come with +`graphiql@2.0.0`, in particular the breaking ones, and will show you how to +upgrade your `1.x` implementation. + +> If you encounter any issues while upgrading that are not covered in here, +> please open an issue or PR on this repo and we will extend this guide. + +## Design refresh including dark theme + +Arguably the biggest change in `graphiql@2` is the new design of the UI. It has +been reworked from scratch to look more modern while keeping its simplistic look +and feel. We also finally added a built-in dark theme. Theme selection is based +on system defaults and can be changed in the new settings dialog (available by +clicking on the gear icon at the bottom of the sidebar on the left of the +screen). + +Starting with `graphiql@2`, the only officially supported way of customizing the +CSS that make up the looks of GraphiQL is by overriding the design tokens +defined using CSS variables. In particular, changes to class names are no longer +considered breaking changes. If you use class-name based selectors to change +styles your overrides might break with minor or patch version bumps. + +A list of all CSS variables that can be customized can be found in the +[`root.css`](../../packages/graphiql-react/src/style/root.css) file of the +`@graphiql/react` package. The variables for colors use a list of values that +can be passed into the +[`hsl`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl) +function in CSS that defines colors by hue, saturation and lightness. + +## Changes to `GraphiQL` component props + +A couple of props of the `GraphiQL` have undergone breaking changes: + +- The props `defaultVariableEditorOpen` and `defaultSecondaryEditorOpen` have + been merged into one prop `defaultEditorToolsVisibility`. The default behavior + if this prop is not passed is that the editor tools are shown if at least one + of the secondary editors has contents. You can pass the following values to + the prop: + - Passing `false` hides the editor tools. + - Passing `true` shows the editor tools. + - Passing `"variables"` explicitly shows the variables editor. + - Passing `"headers"` explicitly shows the headers editor. +- The props `docExplorerOpen`, `onToggleDocs` and `onToggleHistory` have been + removed. They are replaced by the more generic props `visiblePlugin` (for + controlling which plugin is visible) and `onTogglePluginVisibility` (which is + called each time the visibility of any plugin changes). +- The `headerEditorEnabled` prop has been renamed to `isHeadersEditorEnabled`. +- The `ResultsTooltip` prop has been renamed to `responseTooltip`. + +### Tabs enabled by default + +Tabs were supported opt-in starting with `@graphiql@1.8`. With `graphiql@2` tabs +are now always enabled. The `tabs` prop (which previously toggled if tabs were +enabled or not) has therefore been replaced with a prop `onTabChange`. If you +used the `tabs` prop before to pass this function you can change your +implementation like so: + +```diff +<GraphiQL +- tabs={{ onTabChange: (tabState) => {/* do something */} }} ++ onTabChange={(tabState) => {/* do something */}} +/> +``` + +As long as only one session is open, the tab bar above the editors is hidden. A +plus icon next to the logo on the top right allows the user to open more tabs. +With at least two tabs opened, the tab bar appears above the editors. + +## Removed package exports + +All React components apart from the `GraphiQL` component have been moved to the +`@graphiql/react` package. That's why we removed most of the exports with +`graphiql@2`. Here is a list of all exported components and functions that have +been removed and where you can find them now: + +- `QueryEditor`, `VariableEditor` and `DocExplorer`: Now exported from + `@graphiql/react` under the same names + - Note that the `schema` prop of the `DocExplorer` no longer exists, the + component now uses the schema provided by the `ExplorerContext`. +- `ToolbarMenu`: Now exported from `@graphiql/react` as `ToolbarMenu` +- `ToolbarMenuItem`: Now exported from `@graphiql/react` as `ToolbarMenu.Item` +- `ToolbarSelect`: Now exported from `@graphiql/react` as `ToolbarListbox` +- `ToolbarSelectOption`: Now exported from `@graphiql/react` as + `ToolbarListbox.Option` +- `onHasCompletion`: This function is only meant to be used internally, it is no + longer being exported +- `fillLeafs`, `getSelectedOperationName` and `mergeAst`: Now exported from + `@graphiql/toolkit` under the same names +- types `Fetcher`, `FetcherOpts`, `FetcherParams`, `FetcherResult`, + `FetcherReturnType`, `Observable`, `Storage` and `SyncFetcherResult`: Exported + from `@graphiql/toolkit` under the same names (previously just re-exported by + `graphiql`) + +## `GraphiQL` is now a function component + +The `GraphiQL` component in `graphiql@1.x` was a class component. That allowed +easy access to its props, state and methods by attaching a ref to it like so: + +```tsx +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQL } from 'graphiql'; +import React from 'react'; + +const fetcher = createGraphiQLFetcher({ url: 'https://my.endpoint' }); + +class MyComponent extends React.Component { + _graphiql: GraphiQL; + + componentDidMount() { + const query = this._graphiql.getQueryEditor().getValue(); + } + + render() { + return <GraphiQL ref={r => (this._graphiql = r)} fetcher={fetcher} />; + } +} +``` + +With `graphiql@2` we refactored the codebase to more "modern" React. This also +meant replacing all class components with function components. The code above no +longer works in `graphiql@2` as attaching refs to function components is not +possible in React. + +All logic and state management now lives in multiple React contexts, provided by +the `@graphiql/react` package. The `GraphiQL` component is now basically +combining two other components, both of which are also exported by the package. + +- `GraphiQLProvider` (originally coming from `@graphiql/react`) will render all + context providers and takes care of state management +- `GraphiQLInterface` is defined in the `graphiql` package and renders the UI + +If you want to read or modify GraphiQL state from your custom implementation you +need to render both the above components separately as the hooks for consuming +the context values only work in components that are rendered inside the provider +component. + +With all that, the example above can be refactored a such: + +```jsx +import { useEditorContext } from '@graphiql/react'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQLInterface, GraphiQLProvider } from 'graphiql'; +import { useEffect } from 'react'; + +const fetcher = createGraphiQLFetcher({ url: 'https://my.endpoint' }); + +function MyComponent() { + return ( + <GraphiQLProvider fetcher={fetcher}> + <InsideContext /> + </GraphiQLProvider> + ); +} + +function InsideContext() { + // Calling this hook would not work in `MyComponent` (it would return `null`) + const { queryEditor } = useEditorContext(); + + useEffect(() => { + const query = queryEditor.getValue(); + }, [queryEditor]); + + return <GraphiQLInterface />; +} +``` + +Here is a list of all public class methods that existed in `graphiql@1` and its +replacement in `graphiql@2`. All the contexts mentioned below can be accessed +using a hook exported by `@graphiql/react`. + +- `getQueryEditor`: Use the `queryEditor` property from the `EditorContext`. +- `getVariableEditor`: Use the `variableEditor` property from the + `EditorContext`. +- `getHeaderEditor`: Use the `headerEditor` property from the `EditorContext`. +- `refresh`: Calling this method should no longer be necessary, all editors will + refresh automatically after resizing. If you really need to refresh manually + you have to call the `refresh` method on all editor instances individually. +- `autoCompleteLeafs`: Use the `useAutoCompleteLeafs` hook provided by + `@graphiql/react` that returns this function. + +There are a couple more class methods that were intended to be private and were +already removed starting in `graphiql@1.9.0`. Since they were not actually +marked with `private`, here's an extension to the above list for these methods: + +- `handleClickReference`: This was a callback method triggered when clicking on + a type or field. It would open the doc explorer for the clicked reference. If + you want to manually mimic this behavior you can use the `push` method from + the `ExplorerContext` to add an item to the navigation stack of the doc + explorer, and you can use the `setVisiblePlugin` method of the `PluginContext` + (use the `usePluginContext()` hook to access this) to show the doc explorer + plugin (by passing the `DOC_EXPLORER_PLUGIN` object provided by + `@graphiql/react`). +- `handleRunQuery`: To execute a query, use the `run` method of the + `ExecutionContext`. If you want to explicitly set an operation name, call the + `setOperationName` method of the `EditorContext` provider before that (passing + in the operation name string as argument). +- `handleEditorRunQuery`: Use the `run` method of the `ExecutionContext`. +- `handleStopQuery`: Use the `stop` method from the `ExecutionContext`. +- `handlePrettifyQuery`: Use the `usePrettifyEditors` hook provided by + `@graphiql/react` that returns this function. +- `handleMergeQuery`: Use the `useMergeQuery` hook provided by `@graphiql/react` + that returns this function. +- `handleCopyQuery`: Use the `useCopyQuery` hook provided by `@graphiql/react` + that returns this function. +- `handleToggleDocs` and `handleToggleHistory`: Use the `setVisiblePlugin` + method of the `PluginContext`. + +Some class methods were callbacks to modify state which are not intended to be +called manually. All these methods don't have a successor: `handleEditQuery`, +`handleEditVariables`, `handleEditHeaders`, `handleEditOperationName`, +`handleSelectHistoryQuery`, `handleResetResize` and +`handleHintInformationRender` + +### Static properties have been removed + +In `graphiql@1.x` the `GraphiQL` component included a bunch of static properties +that exposed utility functions and other components. Most of these have been +removed in `graphiql@2` since the components and functions have been moved to +the `@graphiql/react` and `@graphiql/toolkit` packages. + +The properties that remain on the `GraphiQL` function component are +`GraphiQL.Logo`, `GraphiQL.Toolbar` and `GraphiQL.Footer`. All three are React +components that can be passed as children to the `GraphiQL` components and +override certain parts of the UI: + +- `GraphiQL.Logo`: Overrides the "logo" at the top right of the screen. By + default, it contains the text "Graph*i*QL". +- `GraphiQL.Toolbar`: Overrides the toolbar next to the query editor. By + default, if contains buttons for prettifying the current editor contents, + merging fragment definitions into the operation definition and copying the + contents of the query editor to the clipboard. Note that the default buttons + will not be shown when passing this component as child to `GraphiQL`, instead + it will show the children you pass to `GraphiQL.Toolbar`. The execute button + will always be shown. If you want to keep the default buttons and add + additional buttons you can use the `toolbar` prop. +- `GraphiQL.Footer`: Adds a section below the response editor. By default, this + won't show up in the UI. + +Here is a list of all the static properties that have been removed and their +replacements: + +- `GraphiQL.formatResult` and `GraphiQL.formatError`: Replaced by equally named + functions from `@graphiql/toolkit` +- `GraphiQL.QueryEditor`, `GraphiQL.VariableEditor` and `GraphiQL.HeaderEditor`: + Replaced by equally named components from `@graphiql/react` +- `GraphiQL.ResultViewer`: Replaced by the `ResponseEditor` component from + `@graphiql/react` +- `GraphiQL.Button`: Replaced by the `ToolbarButton` component from + `@graphiql/react` +- `GraphiQL.ToolbarButton`: This exposed the same component as + `GraphiQL.Button`. +- `GraphiQL.Menu`: Replaced by the `ToolbarMenu` component from + `@graphiql/react` +- `GraphiQL.MenuItem`: Replaced by the `ToolbarMenu.Item` component from + `@graphiql/react` +- `GraphiQL.Group`: Grouping multiple buttons side-by-side is not provided + out-of-the box anymore in the new GraphiQL UI. If you want to implement a + similar feature in the new vertical toolbar you can do so by adding your own + styles for your custom toolbar elements. Example: + + ```jsx + import { createGraphiQLFetcher } from '@graphiql/toolkit'; + import { GraphiQL } from 'graphiql'; + + const fetcher = createGraphiQLFetcher({ url: 'https://my.endpoint' }); + + function MyComponent() { + return ( + <GraphiQL fetcher={fetcher}> + <GraphiQL.Toolbar> + {/* Add custom styles for your buttons using the given class */} + <div className="button-group"> + <button>1</button> + <button>2</button> + <button>3</button> + </div> + </GraphiQL.Toolbar> + </GraphiQL> + ); + } + ``` + +### `window.g` has been removed + +In `graphiql@1.x` the `GraphiQL` class component stored a reference to itself on +a global property named `g`. This property has been removed as refs don't exist +for function components. (Also, the property was only intended for internal use +like testing in the first place.) diff --git a/docs/security/2021-introspection-schema-xss.md b/docs/security/2021-introspection-schema-xss.md new file mode 100644 index 00000000000..2983f30c44f --- /dev/null +++ b/docs/security/2021-introspection-schema-xss.md @@ -0,0 +1,254 @@ +- [1. GraphiQL introspection schema template injection attack: Advisory Statement](#1-graphiql-introspection-schema-template-injection-attack-advisory-statement) + - [1.1. Impact](#11-impact) + - [1.2. Scope](#12-scope) + - [1.3. Patches](#13-patches) + - [1.3.1 CDN bundle implementations may be automatically patched](#131-cdn-bundle-implementations-may-be-automatically-patched) + - [1.4. Workarounds for Older Versions](#14-workarounds-for-older-versions) + - [1.5. How to Re-create the Exploit](#15-how-to-re-create-the-exploit) + - [1.6. Credit](#16-credit) + - [1.7. References](#17-references) + - [1.8. For more information](#18-for-more-information) +- [2. More Details on the Vulnerability](#2-more-details-on-the-vulnerability) +- [3. Compromised introspection Schema Example](#3-compromised-introspection-schema-example) + +## 1. GraphiQL introspection schema template injection attack: Advisory Statement + +This is a security advisory for an XSS vulnerability in `graphiql`. + +A similar vulnerability affects `graphql-playground`, a fork of `graphiql`. +There is a corresponding `graphql-playground` +[advisory](https://github.com/graphql/graphql-playground/security/advisories/GHSA-59r9-6jp6-jcm7) +and +[Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw). + +### 1.1. Impact + +All versions of `graphiql` older than +[`graphiql@1.4.7`](https://github.com/graphql/graphiql/releases/tag/v1.4.7) are +vulnerable to compromised HTTP schema introspection responses or `schema` prop +values with malicious GraphQL type names, exposing a dynamic XSS attack surface +that can allow code injection on operation autocomplete. + +In order for the attack to take place, the user must load a vulnerable schema in +`graphiql`. There are a number of ways that can occur. + +By default, the schema URL is _not_ attacker-controllable in `graphiql` or in +its suggested implementations or examples, leaving only very complex attack +vectors. + +If a custom implementation of `graphiql`'s `fetcher` allows the schema URL to be +set dynamically, such as a URL query parameter like `?endpoint=` in +`graphql-playground`, or a database provided value, then this custom `graphiql` +implementation is _vulnerable to phishing attacks_, and thus much more readily +available, low or no privilege level xss attacks. The URLs could look like any +generic looking graphql schema URL. + +Because this exposes an XSS attack surface, it would be possible for a threat +actor to exfiltrate user credentials, data, etc. using arbitrary malicious +scripts, without it being known to the user. + +### 1.2. Scope + +This advisory describes the impact on the `graphiql` package. The vulnerability +also affects other projects forked from `graphiql` such as +[`graphql-playground`](https://github.com/graphql/graphql-playground/security/advisories/GHSA-59r9-6jp6-jcm7) +and the `graphql-playground` fork distributed by Apollo Server. The impact is +more severe in the `graphql-playground` implementations; see the +[`graphql-playground` advisory](https://github.com/graphql/graphql-playground/security/advisories/GHSA-59r9-6jp6-jcm7) +and +[Apollo Server advisory](https://github.com/apollographql/apollo-server/security/advisories/GHSA-qm7x-rc44-rrqw) +for details. + +This vulnerability does not impact `codemirror-graphql`, `monaco-graphql` or +other dependents, as it exists in `onHasCompletion.ts` in `graphiql`. It does +impact all forks of `graphiql`, and every released version of `graphiql`. + +It should be noted that desktop clients such as Altair, Insomnia, Postwoman, do +not appear to be impacted by this. + +### 1.3. Patches + +`graphiql@1.4.7` addresses this issue via defense in depth. + +- **HTML-escaping text** that should be treated as text rather than HTML. In + most of the app, this happens automatically because React escapes all + interpolated text by default. However, one vulnerable component uses the + unsafe `innerHTML` API and interpolated type names directly into HTML. We now + properly escape that type name, which fixes the known vulnerability. + +- **Validates the schema** upon receiving the introspection response or schema + changes. Schemas with names that violate the GraphQL spec will no longer be + loaded. (This includes preventing the Doc Explorer from loading.) This change + is also sufficient to fix the known vulnerability. You can disable this + validation by setting `dangerouslyAssumeSchemaIsValid={true}`, which means you + are relying only on escaping values to protect you from this attack. + +- **Ensuring that user-generated HTML is safe**. Schemas can contain Markdown in + `description` and `deprecationReason` fields, and the web app renders them to + HTML using the `markdown-it` library. As part of the development of + `graphiql@1.4.7`, we verified that our use of `markdown-it` prevents the + inclusion of arbitrary HTML. We use `markdown-it` without setting + `html: true`, so we are comfortable relying on + [`markdown-it`'s HTML escaping](https://github.com/markdown-it/markdown-it/blob/master/docs/security.md) + here. We considered running a second level of sanitization over all rendered + Markdown using a library such as `dompurify` but believe that is unnecessary + as `markdown-it`'s sanitization appears to be adequate. `graphiql@1.4.7` does + update to the latest version of `markdown-it` (v12, from v10) so that any + security fixes in v11 and v12 will take effect. + +### 1.3.1 CDN bundle implementations may be automatically patched + +Note that if your implementation is depending on a CDN version of `graphiql`, +and is pointed to the `latest` tag (usually the default for most cdns if no +version is specified) then this issue is already mitigated, in case you were +vulnerable to it before. + +### 1.4. Workarounds for Older Versions + +If you cannot use `graphiql@1.4.7` or later + +- Always use a static URL to a trusted server that is serving a trusted GraphQL + schema. + +- If you have a custom implementation that allows using user-provided schema + URLs via a query parameter, database value, etc, you must either disable this + customization, or only allow trusted URLs. + +### 1.5. How to Re-create the Exploit + +You can see an example on +[codesandbox](https://codesandbox.io/s/graphiql-xss-exploit-gr22f?file=/src/App.js). +These are both fixed to the last `graphiql` release `1.4.6` which is the last +vulnerable release; however it would work with any previous release of +`graphiql`. + +Both of these examples are meant to demonstrate the phishing attack surface, so +they are customized to accept a `url` parameter. To demonstrate the phishing +attack, add `?url=https://graphql-xss-schema.netlify.app/graphql` to the +in-codesandbox browser. + +Erase the contents of the given query and type `{u`. You will see an alert +window open, showing that attacker-controlled code was executed. + +Note that when React is in development mode, a validation exception is thrown +visibly; however that exception is usually buried in the browser console in a +production build of `graphiql`. This validation exception comes from +`getDiagnostics`, which invokes `graphql` `validate()` which in turn will +`assertValidSchema()`, as `apollo-server-core` does on executing each operation. +This validation does not prevent the exploit from being successful. + +Note that something like the `url` parameter is not required for the attack to +happen if `graphiql`'s `fetcher` is configured in a different way to communicate +with a compromised GraphQL server. + +### 1.6. Credit + +This vulnerability was discovered by [@Ry0taK](https://github.com/Ry0taK), thank +you! :1st_place_medal: + +Others who contributed: + +- [@imolorhe](https://github.com/imolorhe) +- [@glasser](https://github.com/glasser) +- [@divyenduz](https://github.com/divyenduz) +- [@dotansimha](https://github.com/dotansimha) +- [@acao](https://github.com/acao) +- [@benjie](https://github.com/benjie) and many others who provided morale + support + +### 1.7. References + +**The vulnerability has always been present** + +[In the first commit](https://github.com/graphql/graphiql/commit/b9dec272d89d9c590727fd10d62e4a47e0317fc7#diff-855b77f6310b7e4fb1bcac779cd945092ed49fd759f4684ea391b45101166437R87) + +[And later moved to onHasCompletion.js in 2016](https://github.com/graphql/graphiql/commit/6701b0b626e43800e32413590a295e5c1e3d5419#diff-d45eb76aebcffd27d3a123214487116fa95e0b5a11d70a94a0ce3033ce09f879R110) +(now `.ts` after the typescript migration) + +### 1.8. For more information + +If you have any questions or comments about this advisory: + +- Open an issue in + [graphiql repo](https://github.com/graphql/graphiql/new/issues) + +## 2. More Details on the Vulnerability + +This section provides more details in addition to the advisory. + +An installation of the GraphiQL web app is vulnerable if two conditions are met: + +- The web app trusts information from its corresponding GraphQL server by + interpolating information such as GraphQL type names directly into HTML + instead of appropriately escaping or sanitizing the information. +- The victim can load the web app in a way where it speaks to a GraphQL server + controlled by the attacker. + +All versions of `graphiql` prior to 1.4.7 inappropriately trust type names +provided by the GraphQL server. They additionally rely on XSS filtering in the +`markdown-it` package to try to protect themselves from XSS attacks in GraphQL +descriptions and deprecation reasons. + +By default, `graphiql` does _not_ allow the attacker to control what GraphQL +server it speaks to. Therefore, many installations of `graphiql` are not +affected by this advisory. Installations are only affected if the `fetcher` +argument provided to GraphiQL allows arbitrary customization of the GraphQL +endpoint (eg, reading a GraphQL URL from an URL parameter), or if the attacker +has another way of affecting the introspection schema returned by the GraphQL +server. (Note that `graphql-playground`, a project which started as a fork of +`graphiql`, does this sort of URL parsing by default, so `graphql-playground` +installations _are_ affected by a corresponding vulnerability in their default +configuration.) + +One example of "another way of affecting the introspection schema" would be if +you served `graphiql` as part of a PAAS platform that allows users to define +their own GraphQL schemas. In this case, even though the `graphiql` installation +might be hard-wired to a single GraphQL endpoint, the attacker has control over +that GraphQL endpoint and could use it to inject scripts into `graphiql`. In +this case, your `graphiql` installation could be vulnerable if it responds to +introspection requests without first validating its schema. GraphQL servers can +prevent this by refusing to execute operations (including introspection +operations) on invalid schemas; any server built with `graphql-js` properly +validates its schema prior to execution. + +## 3. Compromised introspection Schema Example + +You can view the code for the exploited schema +[on codesandbox](https://codesandbox.io/s/graphql-xss-compromised-schema-3wdq7?file=/src/bad-schema.js) +or [in the repository](../../packages/graphiql/test/bad-schema.js) + +As you can see, the introspection schema must contain items with a compromised +`name` value. this could be fields, input object names, enum names, variable +names, etc any graphql +[NamedType](https://github.com/graphql/graphql-spec/blob/main/spec/Section%202%20--%20Language.md#type-references) +in the schema with it's name rendered in the autocomplete list. + +```json +{ + "kind": "OBJECT", + "name": "<img src=x onerror=alert(document.domain)>", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null +} +``` diff --git a/example/README.md b/example/README.md deleted file mode 100644 index c9f8ab048e9..00000000000 --- a/example/README.md +++ /dev/null @@ -1,13 +0,0 @@ -Example GraphiQL Install -======================== - -This example uses the version of GraphiQL found in the parent directory, rather -than depending on npm, so that it is easier to test new changes. In order to use -the compiled version of GraphiQL, first run build in the parent directory before -installing and starting the example. - -1. Run `npm install && npm run build` in the parent directory -2. Navigate to this directory (example) in Terminal -3. `npm install` -4. `npm start` -5. Open your browser to [http://localhost:8080/]() diff --git a/example/index.html b/example/index.html deleted file mode 100644 index 33ee33bd930..00000000000 --- a/example/index.html +++ /dev/null @@ -1,150 +0,0 @@ -<!-- - * Copyright (c) 2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the license found in the - * LICENSE file in the root directory of this source tree. - * ---> -<!DOCTYPE html> -<html> - <head> - <style> - body { - height: 100%; - margin: 0; - width: 100%; - overflow: hidden; - } - #graphiql { - height: 100vh; - } - </style> - - <!-- - This GraphiQL example depends on Promise and fetch, which are available in - modern browsers, but can be "polyfilled" for older browsers. - GraphiQL itself depends on React DOM. - If you do not want to rely on a CDN, you can host these files locally or - include them directly in your favored resource bunder. - --> - <script src="//cdn.jsdelivr.net/es6-promise/4.0.5/es6-promise.auto.min.js"></script> - <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script> - <script src="//cdn.jsdelivr.net/react/15.3.2/react.min.js"></script> - <script src="//cdn.jsdelivr.net/react/15.3.2/react-dom.min.js"></script> - - <!-- - These two files can be found in the npm module, however you may wish to - copy them directly into your environment, or perhaps include them in your - favored resource bundler. - --> - <link rel="stylesheet" href="./node_modules/graphiql/graphiql.css" /> - <script src="./node_modules/graphiql/graphiql.js"></script> - - </head> - <body> - <div id="graphiql">Loading...</div> - <script> - - /** - * This GraphiQL example illustrates how to use some of GraphiQL's props - * in order to enable reading and updating the URL parameters, making - * link sharing of queries a little bit easier. - * - * This is only one example of this kind of feature, GraphiQL exposes - * various React params to enable interesting integrations. - */ - - // Parse the search string to get url parameters. - var search = window.location.search; - var parameters = {}; - search.substr(1).split('&').forEach(function (entry) { - var eq = entry.indexOf('='); - if (eq >= 0) { - parameters[decodeURIComponent(entry.slice(0, eq))] = - decodeURIComponent(entry.slice(eq + 1)); - } - }); - - // if variables was provided, try to format it. - if (parameters.variables) { - try { - parameters.variables = - JSON.stringify(JSON.parse(parameters.variables), null, 2); - } catch (e) { - // Do nothing, we want to display the invalid JSON as a string, rather - // than present an error. - } - } - - // When the query and variables string is edited, update the URL bar so - // that it can be easily shared - function onEditQuery(newQuery) { - parameters.query = newQuery; - updateURL(); - } - - function onEditVariables(newVariables) { - parameters.variables = newVariables; - updateURL(); - } - - function onEditOperationName(newOperationName) { - parameters.operationName = newOperationName; - updateURL(); - } - - function updateURL() { - var newSearch = '?' + Object.keys(parameters).filter(function (key) { - return Boolean(parameters[key]); - }).map(function (key) { - return encodeURIComponent(key) + '=' + - encodeURIComponent(parameters[key]); - }).join('&'); - history.replaceState(null, null, newSearch); - } - - // Defines a GraphQL fetcher using the fetch API. You're not required to - // use fetch, and could instead implement graphQLFetcher however you like, - // as long as it returns a Promise or Observable. - function graphQLFetcher(graphQLParams) { - // This example expects a GraphQL server at the path /graphql. - // Change this to point wherever you host your GraphQL server. - return fetch('/graphql', { - method: 'post', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(graphQLParams), - credentials: 'include', - }).then(function (response) { - return response.text(); - }).then(function (responseBody) { - try { - return JSON.parse(responseBody); - } catch (error) { - return responseBody; - } - }); - } - - // Render <GraphiQL /> into the body. - // See the README in the top level of this module to learn more about - // how you can customize GraphiQL by providing different values or - // additional child elements. - ReactDOM.render( - React.createElement(GraphiQL, { - fetcher: graphQLFetcher, - query: parameters.query, - variables: parameters.variables, - operationName: parameters.operationName, - onEditQuery: onEditQuery, - onEditVariables: onEditVariables, - onEditOperationName: onEditOperationName - }), - document.getElementById('graphiql') - ); - </script> - </body> -</html> diff --git a/example/package.json b/example/package.json deleted file mode 100644 index c4a6e60495f..00000000000 --- a/example/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "description": "An example using GraphiQL", - "scripts": { - "start": "node server.js" - }, - "dependencies": { - "express": "^4.13.3", - "express-graphql": "^0.6.1", - "graphiql": "../", - "graphql": "^0.8.0" - }, - "optionalDependencies": { - "react": "^15.0.1", - "react-dom": "^15.0.1" - } -} diff --git a/example/schema.js b/example/schema.js deleted file mode 100644 index c0d011ee9e7..00000000000 --- a/example/schema.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * 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. - */ - -const { - GraphQLSchema, - GraphQLObjectType, - GraphQLUnionType, - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLInterfaceType, - GraphQLBoolean, - GraphQLInt, - GraphQLFloat, - GraphQLString, - GraphQLID, - GraphQLList, -} = require('graphql'); - -// Test Schema -const TestEnum = new GraphQLEnumType({ - name: 'TestEnum', - values: { - RED: { description: 'A rosy color' }, - GREEN: { description: 'The color of martians and slime' }, - BLUE: { description: 'A feeling you might have if you can\'t use GraphQL' }, - } -}); - -const TestInputObject = new GraphQLInputObjectType({ - name: 'TestInput', - fields: () => ({ - string: { - type: GraphQLString, - description: 'Repeats back this string' - }, - 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) }, - }) -}); - -const TestInterface = new GraphQLInterfaceType({ - name: 'TestInterface', - description: 'Test interface.', - fields: () => ({ - name: { - type: GraphQLString, - description: 'Common name string.' - } - }), - resolveType: check => { - return check ? UnionFirst : UnionSecond; - } -}); - -const UnionFirst = new GraphQLObjectType({ - name: 'First', - fields: () => ({ - name: { - type: GraphQLString, - description: 'Common name string for UnionFirst.' - }, - first: { - type: new GraphQLList(TestInterface), - resolve: () => { return true; } - } - }), - interfaces: [ TestInterface ] -}); - -const UnionSecond = new GraphQLObjectType({ - name: 'Second', - fields: () => ({ - name: { - type: GraphQLString, - description: 'Common name string for UnionFirst.' - }, - second: { - type: TestInterface, - resolve: () => { return false; } - } - }), - interfaces: [ TestInterface ] -}); - -const TestUnion = new GraphQLUnionType({ - name: 'TestUnion', - types: [ UnionFirst, UnionSecond ], - resolveType() { - return UnionFirst; - } -}); - -const TestType = new GraphQLObjectType({ - name: 'Test', - fields: () => ({ - test: { - type: TestType, - description: '`test` field from `Test` type.', - resolve: () => ({}) - }, - union: { - type: TestUnion, - description: '> union field from Test type, block-quoted.', - resolve: () => ({}) - }, - id: { - type: GraphQLID, - description: 'id field from Test type.', - resolve: () => 'abc123', - }, - isTest: { - type: GraphQLBoolean, - description: 'Is this a test schema? Sure it is.', - 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) }, - } - }, - }) -}); - -const 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 TestSubscriptionType = new GraphQLObjectType({ - name: 'SubscriptionType', - description: 'This is a simple subscription type', - fields: { - subscribeToTest: { - type: TestType, - description: 'Subscribe to the test type', - args: { - id: { type: GraphQLString } - } - } - } -}); - -const myTestSchema = new GraphQLSchema({ - query: TestType, - mutation: TestMutationType, - subscription: TestSubscriptionType -}); - -module.exports = myTestSchema; diff --git a/example/server.js b/example/server.js deleted file mode 100644 index d69e5fffd9c..00000000000 --- a/example/server.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * 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. - */ - -const express = require('express'); -const graphqlHTTP = require('express-graphql'); - -const schema = require('./schema'); - -const app = express(); -app.use(express.static(__dirname)); -app.use('/graphql', graphqlHTTP(() => ({ schema }))); - -app.listen(8080, () => console.log('Started on http://localhost:8080/')); diff --git a/examples/cm6-graphql-legacy-parcel/.gitignore b/examples/cm6-graphql-legacy-parcel/.gitignore new file mode 100644 index 00000000000..ceddaa37f12 --- /dev/null +++ b/examples/cm6-graphql-legacy-parcel/.gitignore @@ -0,0 +1 @@ +.cache/ diff --git a/examples/cm6-graphql-legacy-parcel/README.md b/examples/cm6-graphql-legacy-parcel/README.md new file mode 100644 index 00000000000..83bf1267578 --- /dev/null +++ b/examples/cm6-graphql-legacy-parcel/README.md @@ -0,0 +1,9 @@ +## Codemirror 6 Parcel Example + +This example demonstrates how to transpile your own custom ES6 Codemirror 6 +GraphQL implementation with parcel bundler. + +### Setup + +1. `yarn` and `yarn start` from this folder to start parcel dev mode. +1. `yarn build` to find production ready files. diff --git a/examples/cm6-graphql-legacy-parcel/package.json b/examples/cm6-graphql-legacy-parcel/package.json new file mode 100644 index 00000000000..d780f4d25af --- /dev/null +++ b/examples/cm6-graphql-legacy-parcel/package.json @@ -0,0 +1,35 @@ +{ + "name": "example-cm6-graphql-legacy-parcel", + "version": "1.1.10-alpha.8", + "license": "MIT", + "description": "GraphiQL Parcel Example", + "main": "index.js", + "private": true, + "scripts": { + "start": "parcel src/index.html -p 8080", + "build": "parcel build src/index.html --public-url /" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/language": "^0.20.0", + "codemirror-graphql": "^2.0.2", + "graphql": "^16.4.0" + }, + "devDependencies": { + "parcel-bundler": "^1.12.4", + "worker-loader": "^2.0.0", + "typescript": "^4.6.3" + } +} diff --git a/examples/cm6-graphql-legacy-parcel/src/index.html b/examples/cm6-graphql-legacy-parcel/src/index.html new file mode 100644 index 00000000000..5269a2c962b --- /dev/null +++ b/examples/cm6-graphql-legacy-parcel/src/index.html @@ -0,0 +1,24 @@ +<!doctype html> +<html lang="en"> + <head> + <style> + body { + padding: 0; + margin: 0; + min-height: 100vh; + } + #root { + height: 100vh; + } + </style> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <meta http-equiv="X-UA-Compatible" content="ie=edge" /> + <title>CM6 GraphQL Editor Example + + + +
+ + + diff --git a/examples/cm6-graphql-legacy-parcel/src/index.ts b/examples/cm6-graphql-legacy-parcel/src/index.ts new file mode 100644 index 00000000000..e846c84fd97 --- /dev/null +++ b/examples/cm6-graphql-legacy-parcel/src/index.ts @@ -0,0 +1,19 @@ +import { EditorState, EditorView, basicSetup } from '@codemirror/basic-setup'; +import { StreamLanguage } from '@codemirror/language'; +import { graphql } from 'codemirror-graphql/cm6-legacy/mode'; +import query from './sample-query'; + +const state = EditorState.create({ + doc: query, + extensions: [basicSetup, StreamLanguage.define(graphql)], +}); + +new EditorView({ + state, + parent: document.querySelector('#editor')!, +}); + +// Hot Module Replacement +if (module.hot) { + module.hot.accept(); +} diff --git a/examples/cm6-graphql-legacy-parcel/src/sample-query.ts b/examples/cm6-graphql-legacy-parcel/src/sample-query.ts new file mode 100644 index 00000000000..f6fe841c577 --- /dev/null +++ b/examples/cm6-graphql-legacy-parcel/src/sample-query.ts @@ -0,0 +1,57 @@ +const query = /* GraphQL */ ` + # Copyright (c) 2021 GraphQL Contributors + # 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 + ... @skip(if: false) { + id + } + ... { + id + } + } + + mutation mutationName { + setString(value: "newString") + } + + subscription subscriptionName { + subscribeToTest(id: "anId") { + ... on Test { + id + } + } + } + + fragment frag on Test { + test @include(if: true) { + union { + __typename + } + } + } +`; + +export default query; diff --git a/examples/cm6-graphql-legacy-parcel/tsconfig.json b/examples/cm6-graphql-legacy-parcel/tsconfig.json new file mode 100644 index 00000000000..b760a1ab436 --- /dev/null +++ b/examples/cm6-graphql-legacy-parcel/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "sourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src"] +} diff --git a/examples/cm6-graphql-parcel/.gitignore b/examples/cm6-graphql-parcel/.gitignore new file mode 100644 index 00000000000..5ad227de645 --- /dev/null +++ b/examples/cm6-graphql-parcel/.gitignore @@ -0,0 +1,2 @@ +.cache/ +.parcel-cache/ diff --git a/examples/cm6-graphql-parcel/README.md b/examples/cm6-graphql-parcel/README.md new file mode 100644 index 00000000000..83bf1267578 --- /dev/null +++ b/examples/cm6-graphql-parcel/README.md @@ -0,0 +1,9 @@ +## Codemirror 6 Parcel Example + +This example demonstrates how to transpile your own custom ES6 Codemirror 6 +GraphQL implementation with parcel bundler. + +### Setup + +1. `yarn` and `yarn start` from this folder to start parcel dev mode. +1. `yarn build` to find production ready files. diff --git a/examples/cm6-graphql-parcel/package.json b/examples/cm6-graphql-parcel/package.json new file mode 100644 index 00000000000..e196fd7fcda --- /dev/null +++ b/examples/cm6-graphql-parcel/package.json @@ -0,0 +1,46 @@ +{ + "name": "example-cm6-graphql-parcel", + "version": "1.1.10-alpha.8", + "license": "MIT", + "description": "GraphiQL Parcel Example", + "main": "index.js", + "private": true, + "scripts": { + "start": "parcel src/index.html -p 8080", + "build": "parcel build src/index.html --public-url /" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@codemirror/autocomplete": "6.0.0", + "@codemirror/commands": "6.0.0", + "@codemirror/language": "6.0.0", + "@codemirror/state": "6.1.0", + "@codemirror/theme-one-dark": "6.0.0", + "@codemirror/view": "6.1.2", + "cm6-graphql": "0.0.1", + "graphql": "^16.4.0" + }, + "devDependencies": { + "parcel": "^2.6.2", + "worker-loader": "^2.0.0", + "typescript": "^4.6.3" + }, + "resolutions": { + "**/@codemirror/autocomplete": "6.0.0", + "**/@codemirror/commands": "6.0.0", + "**/@codemirror/view": "6.1.2", + "**/@codemirror/state": "6.1.0", + "**/@codemirror/language": "6.0.0" + } +} diff --git a/examples/cm6-graphql-parcel/src/index.html b/examples/cm6-graphql-parcel/src/index.html new file mode 100644 index 00000000000..6573ff7f6bc --- /dev/null +++ b/examples/cm6-graphql-parcel/src/index.html @@ -0,0 +1,24 @@ + + + + + + + + Code Mirror 6 GraphQL Editor Example + + + +
+ + + diff --git a/examples/cm6-graphql-parcel/src/index.ts b/examples/cm6-graphql-parcel/src/index.ts new file mode 100644 index 00000000000..286ddeeeae0 --- /dev/null +++ b/examples/cm6-graphql-parcel/src/index.ts @@ -0,0 +1,42 @@ +import { EditorState } from '@codemirror/state'; +import { EditorView, lineNumbers } from '@codemirror/view'; +import { history } from '@codemirror/commands'; +import { autocompletion, closeBrackets } from '@codemirror/autocomplete'; +import { bracketMatching, syntaxHighlighting } from '@codemirror/language'; +import { oneDarkHighlightStyle, oneDark } from '@codemirror/theme-one-dark'; +import { graphql } from 'cm6-graphql'; +import query from './sample-query'; +import { TestSchema } from './testSchema'; + +const state = EditorState.create({ + doc: query, + extensions: [ + bracketMatching(), + closeBrackets(), + history(), + autocompletion(), + lineNumbers(), + oneDark, + syntaxHighlighting(oneDarkHighlightStyle), + graphql(TestSchema, { + onShowInDocs(field, type, parentType) { + alert( + `Showing in docs.: Field: ${field}, Type: ${type}, ParentType: ${parentType}`, + ); + }, + onFillAllFields(view, schema, _query, cursor, token) { + alert(`Filling all fields. Token: ${token}`); + }, + }), + ], +}); + +new EditorView({ + state, + parent: document.querySelector('#editor')!, +}); + +// Hot Module Replacement +if (module.hot) { + module.hot.accept(); +} diff --git a/examples/cm6-graphql-parcel/src/sample-query.ts b/examples/cm6-graphql-parcel/src/sample-query.ts new file mode 100644 index 00000000000..f6fe841c577 --- /dev/null +++ b/examples/cm6-graphql-parcel/src/sample-query.ts @@ -0,0 +1,57 @@ +const query = /* GraphQL */ ` + # Copyright (c) 2021 GraphQL Contributors + # 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 + ... @skip(if: false) { + id + } + ... { + id + } + } + + mutation mutationName { + setString(value: "newString") + } + + subscription subscriptionName { + subscribeToTest(id: "anId") { + ... on Test { + id + } + } + } + + fragment frag on Test { + test @include(if: true) { + union { + __typename + } + } + } +`; + +export default query; diff --git a/examples/cm6-graphql-parcel/src/testSchema.ts b/examples/cm6-graphql-parcel/src/testSchema.ts new file mode 100644 index 00000000000..e0ed060bb28 --- /dev/null +++ b/examples/cm6-graphql-parcel/src/testSchema.ts @@ -0,0 +1,240 @@ +/* istanbul ignore file */ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + DirectiveLocation, + GraphQLBoolean, + GraphQLDeprecatedDirective, + GraphQLDirective, + GraphQLEnumType, + GraphQLFloat, + GraphQLID, + GraphQLIncludeDirective, + GraphQLInputObjectType, + GraphQLInt, + GraphQLInterfaceType, + GraphQLList, + GraphQLObjectType, + GraphQLSchema, + GraphQLSkipDirective, + GraphQLString, + GraphQLUnionType, +} from 'graphql'; + +// Test Schema + +export const TestEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + RED: {}, + GREEN: {}, + BLUE: {}, + }, +}); + +export const TestInputObject: GraphQLInputObjectType = + 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) }, + }), + }); + +const TestInterface: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: 'TestInterface', + resolveType: () => UnionFirst, + fields: { + scalar: { + type: GraphQLString, + resolve: () => ({}), + }, + }, +}); + +const AnotherTestInterface: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: 'AnotherTestInterface', + resolveType: () => UnionFirst, + fields: { + example: { + type: GraphQLString, + resolve: () => ({}), + }, + }, +}); + +export const UnionFirst = new GraphQLObjectType({ + name: 'First', + interfaces: [TestInterface, AnotherTestInterface], + fields: () => ({ + scalar: { + type: GraphQLString, + resolve: () => ({}), + }, + first: { + type: TestType, + resolve: () => ({}), + }, + example: { + type: GraphQLString, + resolve: () => ({}), + }, + }), +}); + +export const UnionSecond = new GraphQLObjectType({ + name: 'Second', + fields: () => ({ + second: { + type: TestType, + resolve: () => ({}), + }, + }), +}); + +export const TestUnion = new GraphQLUnionType({ + name: 'TestUnion', + types: [UnionFirst, UnionSecond], + resolveType() { + return UnionFirst; + }, +}); + +export const TestType: GraphQLObjectType = new GraphQLObjectType({ + name: 'Test', + fields: () => ({ + test: { + type: TestType, + resolve: () => ({}), + }, + deprecatedTest: { + type: TestType, + deprecationReason: 'Use test instead.', + resolve: () => ({}), + }, + union: { + type: TestUnion, + resolve: () => ({}), + }, + first: { + type: UnionFirst, + 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) }, + }, + }, + }), +}); + +const 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 TestSubscriptionType = new GraphQLObjectType({ + name: 'SubscriptionType', + description: 'This is a simple subscription type', + fields: { + subscribeToTest: { + type: TestType, + description: 'Subscribe to the test type', + args: { + id: { type: GraphQLString }, + }, + }, + }, +}); + +const OnArgDirective = new GraphQLDirective({ + name: 'onArg', + locations: [DirectiveLocation.ARGUMENT_DEFINITION], +}); + +const OnAllDefsDirective = new GraphQLDirective({ + name: 'onAllDefs', + locations: [ + DirectiveLocation.SCHEMA, + DirectiveLocation.SCALAR, + DirectiveLocation.OBJECT, + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.INTERFACE, + DirectiveLocation.UNION, + DirectiveLocation.ENUM, + DirectiveLocation.ENUM_VALUE, + DirectiveLocation.INPUT_OBJECT, + DirectiveLocation.ARGUMENT_DEFINITION, + DirectiveLocation.INPUT_FIELD_DEFINITION, + ], +}); + +export const TestSchema = new GraphQLSchema({ + query: TestType, + mutation: TestMutationType, + subscription: TestSubscriptionType, + directives: [ + GraphQLIncludeDirective, + GraphQLSkipDirective, + GraphQLDeprecatedDirective, + OnArgDirective, + OnAllDefsDirective, + ], +}); diff --git a/examples/cm6-graphql-parcel/tsconfig.json b/examples/cm6-graphql-parcel/tsconfig.json new file mode 100644 index 00000000000..b760a1ab436 --- /dev/null +++ b/examples/cm6-graphql-parcel/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "sourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src"] +} diff --git a/examples/graphiql-cdn/CHANGELOG.md b/examples/graphiql-cdn/CHANGELOG.md new file mode 100644 index 00000000000..99ca010ed9c --- /dev/null +++ b/examples/graphiql-cdn/CHANGELOG.md @@ -0,0 +1,43 @@ +# Change Log + +All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.0.8](https://github.com/graphql/graphiql/compare/example-graphiql-cdn@0.0.8-alpha.6...example-graphiql-cdn@0.0.8) (2020-06-11) + +**Note:** Version bump only for package example-graphiql-cdn + +## [0.0.8-alpha.6](https://github.com/graphql/graphiql/compare/example-graphiql-cdn@0.0.8-alpha.5...example-graphiql-cdn@0.0.8-alpha.6) (2020-06-04) + +**Note:** Version bump only for package example-graphiql-cdn + +## [0.0.8-alpha.5](https://github.com/graphql/graphiql/compare/example-graphiql-cdn@0.0.8-alpha.4...example-graphiql-cdn@0.0.8-alpha.5) (2020-04-10) + +**Note:** Version bump only for package example-graphiql-cdn + +## [0.0.8-alpha.4](https://github.com/graphql/graphiql/compare/example-graphiql-cdn@0.0.8-alpha.3...example-graphiql-cdn@0.0.8-alpha.4) (2020-04-10) + +**Note:** Version bump only for package example-graphiql-cdn + +## [0.0.8-alpha.3](https://github.com/graphql/graphiql/compare/example-graphiql-cdn@0.0.8-alpha.2...example-graphiql-cdn@0.0.8-alpha.3) (2020-03-20) + +**Note:** Version bump only for package example-graphiql-cdn + +## [0.0.8-alpha.2](https://github.com/graphql/graphiql/compare/example-graphiql-cdn@0.0.8-alpha.0...example-graphiql-cdn@0.0.8-alpha.2) (2020-03-20) + +**Note:** Version bump only for package example-graphiql-cdn + +**Note:** Version bump only for package example-graphiql-cdn + +## 0.0.8-alpha.1 (2020-01-18) + +## [0.0.7](https://github.com/graphql/graphiql/compare/graphiql-example-cdn@0.0.6...graphiql-example-cdn@0.0.7) (2019-12-03) + +**Note:** Version bump only for package graphiql-example-cdn + +## [0.0.6](https://github.com/graphql/graphiql/compare/graphiql-example-cdn@0.0.5...graphiql-example-cdn@0.0.6) (2019-11-26) + +**Note:** Version bump only for package graphiql-example-cdn + +## [0.0.5](https://github.com/graphql/graphiql/compare/graphiql-example-cdn@0.0.4...graphiql-example-cdn@0.0.5) (2019-10-19) + +**Note:** Version bump only for package graphiql-example-cdn diff --git a/examples/graphiql-cdn/README.md b/examples/graphiql-cdn/README.md new file mode 100644 index 00000000000..222718f896c --- /dev/null +++ b/examples/graphiql-cdn/README.md @@ -0,0 +1,10 @@ +# GraphiQL CDN Example + +This example uses the CDN bundles to show the most simple example possible. It +uses the latest version published on npm, via unpkg + +### Setup + +none required, just open the index.html! + +`open index.html` in osx `firefox index.html` or `chromium index.html` in linux diff --git a/examples/graphiql-cdn/index.html b/examples/graphiql-cdn/index.html new file mode 100644 index 00000000000..8ca063e8564 --- /dev/null +++ b/examples/graphiql-cdn/index.html @@ -0,0 +1,67 @@ + + + + + GraphiQL + + + + + + + + + + + +
Loading...
+ + + + diff --git a/examples/graphiql-cdn/package.json b/examples/graphiql-cdn/package.json new file mode 100644 index 00000000000..fb2b68a60d1 --- /dev/null +++ b/examples/graphiql-cdn/package.json @@ -0,0 +1,10 @@ +{ + "name": "example-graphiql-cdn", + "version": "0.0.8", + "private": true, + "license": "MIT", + "description": "An example using GraphiQL", + "scripts": { + "build-demo": "copy index.html ../../packages/graphiql/cdn/" + } +} diff --git a/examples/graphiql-create-react-app/CHANGELOG.md b/examples/graphiql-create-react-app/CHANGELOG.md new file mode 100644 index 00000000000..2f39755401f --- /dev/null +++ b/examples/graphiql-create-react-app/CHANGELOG.md @@ -0,0 +1,68 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.11-alpha.8](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.7...example-graphiql-create-react-app@0.1.11-alpha.8) (2021-01-07) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.7](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.6...example-graphiql-create-react-app@0.1.11-alpha.7) (2021-01-07) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.6](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.5...example-graphiql-create-react-app@0.1.11-alpha.6) (2021-01-07) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.5](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.4...example-graphiql-create-react-app@0.1.11-alpha.5) (2021-01-03) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.4](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.3...example-graphiql-create-react-app@0.1.11-alpha.4) (2020-12-28) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.3](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.2...example-graphiql-create-react-app@0.1.11-alpha.3) (2020-08-26) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.2](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.1...example-graphiql-create-react-app@0.1.11-alpha.2) (2020-08-22) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.1](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.11-alpha.0...example-graphiql-create-react-app@0.1.11-alpha.1) (2020-08-12) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.11-alpha.0](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.10...example-graphiql-create-react-app@0.1.11-alpha.0) (2020-08-10) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.10](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.6...example-graphiql-create-react-app@0.1.10) (2020-08-06) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.6](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.5...example-graphiql-create-react-app@0.1.6) (2020-06-11) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.5](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.4...example-graphiql-create-react-app@0.1.5) (2020-06-04) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.4](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.3...example-graphiql-create-react-app@0.1.4) (2020-06-04) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.3](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.2...example-graphiql-create-react-app@0.1.3) (2020-05-28) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## [0.1.2](https://github.com/graphql/graphiql/compare/example-graphiql-create-react-app@0.1.1...example-graphiql-create-react-app@0.1.2) (2020-05-19) + +**Note:** Version bump only for package example-graphiql-create-react-app + +## 0.1.1 (2020-05-17) + +**Note:** Version bump only for package example-graphiql-create-react-app diff --git a/examples/graphiql-create-react-app/README.md b/examples/graphiql-create-react-app/README.md new file mode 100644 index 00000000000..ed1d4301f9c --- /dev/null +++ b/examples/graphiql-create-react-app/README.md @@ -0,0 +1,8 @@ +# GraphiQL `create-react-app` Example + +This example demonstrates how to transpile your own custom ES6 and typescript GraphiQL implementation bootstrapped with `create-react-app`, no config needed. + +## Setup + +1. `yarn` and `yarn start` from this folder to start `react-scripts` dev server. +1. `yarn build` from this folder to build production ready transpiled files using `react-scripts`. Find the output in `build` folder. diff --git a/examples/graphiql-create-react-app/package.json b/examples/graphiql-create-react-app/package.json new file mode 100644 index 00000000000..de4b96f132c --- /dev/null +++ b/examples/graphiql-create-react-app/package.json @@ -0,0 +1,32 @@ +{ + "name": "example-graphiql-create-react-app", + "version": "0.1.11-alpha.8", + "private": true, + "dependencies": { + "graphiql": "^2.2.0", + "graphql": "^16.4.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "5.0.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "eslint-config-react-app": "^5.2.1", + "worker-loader": "^2.0.0" + } +} diff --git a/examples/graphiql-create-react-app/public/index.html b/examples/graphiql-create-react-app/public/index.html new file mode 100644 index 00000000000..701a1dbef87 --- /dev/null +++ b/examples/graphiql-create-react-app/public/index.html @@ -0,0 +1,31 @@ + + + + + + + + + GraphiQL create-react-app Example + + + +
+ + + diff --git a/examples/graphiql-create-react-app/src/App.tsx b/examples/graphiql-create-react-app/src/App.tsx new file mode 100644 index 00000000000..022260f5c67 --- /dev/null +++ b/examples/graphiql-create-react-app/src/App.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { GraphiQL } from 'graphiql'; +import type { Fetcher } from '@graphiql/toolkit'; +import 'graphiql/graphiql.min.css'; + +const fetcher: Fetcher = async graphQLParams => { + const data = await fetch( + 'https://swapi-graphql.netlify.app/.netlify/functions/index', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(graphQLParams), + credentials: 'same-origin', + }, + ); + return data.json().catch(() => data.text()); +}; + +const App = () => ; + +export default App; diff --git a/examples/graphiql-create-react-app/src/index.css b/examples/graphiql-create-react-app/src/index.css new file mode 100644 index 00000000000..d95d5ede303 --- /dev/null +++ b/examples/graphiql-create-react-app/src/index.css @@ -0,0 +1,8 @@ +body { + padding: 0; + margin: 0; + min-height: 100vh; +} +#root { + height: 100vh; +} diff --git a/examples/graphiql-create-react-app/src/index.tsx b/examples/graphiql-create-react-app/src/index.tsx new file mode 100644 index 00000000000..ac32f49914d --- /dev/null +++ b/examples/graphiql-create-react-app/src/index.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './index.css'; + +const root = createRoot(document.getElementById('root')!); +root.render(); diff --git a/examples/graphiql-create-react-app/src/react-app-env.d.ts b/examples/graphiql-create-react-app/src/react-app-env.d.ts new file mode 100644 index 00000000000..6431bc5fc6b --- /dev/null +++ b/examples/graphiql-create-react-app/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/graphiql-create-react-app/tsconfig.json b/examples/graphiql-create-react-app/tsconfig.json new file mode 100644 index 00000000000..af10394b4c3 --- /dev/null +++ b/examples/graphiql-create-react-app/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src"] +} diff --git a/examples/graphiql-parcel/.gitignore b/examples/graphiql-parcel/.gitignore new file mode 100644 index 00000000000..f78ec92852f --- /dev/null +++ b/examples/graphiql-parcel/.gitignore @@ -0,0 +1 @@ +.parcel-cache diff --git a/examples/graphiql-parcel/CHANGELOG.md b/examples/graphiql-parcel/CHANGELOG.md new file mode 100644 index 00000000000..62177beb7ed --- /dev/null +++ b/examples/graphiql-parcel/CHANGELOG.md @@ -0,0 +1,69 @@ +# Change Log + +All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.10-alpha.8](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.7...graphiql-parcel-example@1.1.10-alpha.8) (2021-01-07) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.7](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.6...graphiql-parcel-example@1.1.10-alpha.7) (2021-01-07) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.6](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.5...graphiql-parcel-example@1.1.10-alpha.6) (2021-01-07) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.5](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.4...graphiql-parcel-example@1.1.10-alpha.5) (2021-01-03) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.4](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.3...graphiql-parcel-example@1.1.10-alpha.4) (2020-12-28) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.3](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.2...graphiql-parcel-example@1.1.10-alpha.3) (2020-08-26) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.2](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.1...graphiql-parcel-example@1.1.10-alpha.2) (2020-08-22) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.1](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.10-alpha.0...graphiql-parcel-example@1.1.10-alpha.1) (2020-08-12) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.10-alpha.0](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.9...graphiql-parcel-example@1.1.10-alpha.0) (2020-08-10) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.9](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.5...graphiql-parcel-example@1.1.9) (2020-08-06) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.5](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.4...graphiql-parcel-example@1.1.5) (2020-06-11) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.4](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.3...graphiql-parcel-example@1.1.4) (2020-06-04) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.3](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.2...graphiql-parcel-example@1.1.3) (2020-06-04) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.2](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.1...graphiql-parcel-example@1.1.2) (2020-05-28) + +**Note:** Version bump only for package graphiql-parcel-example + +## [1.1.1](https://github.com/graphql/graphiql/compare/graphiql-parcel-example@1.1.0...graphiql-parcel-example@1.1.1) (2020-05-19) + +**Note:** Version bump only for package graphiql-parcel-example + +# 1.1.0 (2020-05-17) + +### Features + +- Add Parcel Example for GraphiQL ([#1511](https://github.com/graphql/graphiql/issues/1511)) ([fe4b811](https://github.com/graphql/graphiql/commit/fe4b811876838cabdf545a6034ad12bc33e044b2)) diff --git a/examples/graphiql-parcel/README.md b/examples/graphiql-parcel/README.md new file mode 100644 index 00000000000..edbeb13bb7b --- /dev/null +++ b/examples/graphiql-parcel/README.md @@ -0,0 +1,9 @@ +## GraphiQL Parcel Example + +This example demonstrates how to transpile your own custom ES6 GraphiQL +implementation with parcel bundler. + +### Setup + +1. `yarn` and `yarn start` from this folder to start parcel dev mode. +1. `yarn build` to find production ready files. diff --git a/examples/graphiql-parcel/package.json b/examples/graphiql-parcel/package.json new file mode 100644 index 00000000000..4a4f576e8c7 --- /dev/null +++ b/examples/graphiql-parcel/package.json @@ -0,0 +1,35 @@ +{ + "name": "example-graphiql-parcel", + "version": "1.1.10-alpha.8", + "license": "MIT", + "description": "GraphiQL Parcel Example", + "main": "index.js", + "private": true, + "scripts": { + "start": "parcel src/index.html -p 8080", + "build": "parcel build src/index.html --public-url /" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "graphiql": "^2.2.0", + "graphql": "^16.4.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "parcel": "^2.5.0", + "worker-loader": "^2.0.0", + "typescript": "^4.6.3" + } +} diff --git a/examples/graphiql-parcel/src/index.html b/examples/graphiql-parcel/src/index.html new file mode 100644 index 00000000000..936537cbb0d --- /dev/null +++ b/examples/graphiql-parcel/src/index.html @@ -0,0 +1,26 @@ + + + + + + + + Parcel React Example + + + + + +
+ + + diff --git a/examples/graphiql-parcel/src/index.tsx b/examples/graphiql-parcel/src/index.tsx new file mode 100644 index 00000000000..d9116626d5b --- /dev/null +++ b/examples/graphiql-parcel/src/index.tsx @@ -0,0 +1,30 @@ +import { createRoot } from 'react-dom/client'; +import { GraphiQL } from 'graphiql'; +import type { Fetcher } from '@graphiql/toolkit'; +import { CSSProperties } from 'react'; + +const fetcher: Fetcher = async graphQLParams => { + const data = await fetch( + 'https://swapi-graphql.netlify.app/.netlify/functions/index', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(graphQLParams), + credentials: 'same-origin', + }, + ); + return data.json().catch(() => data.text()); +}; + +const style: CSSProperties = { height: '100vh' }; + +const App = () => ; + +const root = createRoot(document.getElementById('root')); +root.render(); + +// Hot Module Replacement +module.hot?.accept(); diff --git a/examples/graphiql-parcel/tsconfig.json b/examples/graphiql-parcel/tsconfig.json new file mode 100644 index 00000000000..e33621994a7 --- /dev/null +++ b/examples/graphiql-parcel/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "sourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/graphiql-webpack/CHANGELOG.md b/examples/graphiql-webpack/CHANGELOG.md new file mode 100644 index 00000000000..06296e8d05d --- /dev/null +++ b/examples/graphiql-webpack/CHANGELOG.md @@ -0,0 +1,141 @@ +# Change Log + +All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.1-alpha.8](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.7...example-graphiql-webpack@1.1.1-alpha.8) (2021-01-07) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.7](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.6...example-graphiql-webpack@1.1.1-alpha.7) (2021-01-07) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.6](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.5...example-graphiql-webpack@1.1.1-alpha.6) (2021-01-07) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.5](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.4...example-graphiql-webpack@1.1.1-alpha.5) (2021-01-03) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.4](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.3...example-graphiql-webpack@1.1.1-alpha.4) (2020-12-28) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.3](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.2...example-graphiql-webpack@1.1.1-alpha.3) (2020-08-26) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.2](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.1...example-graphiql-webpack@1.1.1-alpha.2) (2020-08-22) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.1](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.1-alpha.0...example-graphiql-webpack@1.1.1-alpha.1) (2020-08-12) + +**Note:** Version bump only for package example-graphiql-webpack + +## [1.1.1-alpha.0](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.1.0...example-graphiql-webpack@1.1.1-alpha.0) (2020-08-10) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.1.0](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0...example-graphiql-webpack@1.1.0) (2020-08-06) + +### Features + +- [RFC] GraphiQL rewrite for monaco editor, react context and redesign, i18n ([#1523](https://github.com/graphql/graphiql/issues/1523)) ([ad730cd](https://github.com/graphql/graphiql/commit/ad730cdc2e3cb7216d821a01725c60475989ee20)) + +# [1.0.0](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.13...example-graphiql-webpack@1.0.0) (2020-06-11) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.13](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.12...example-graphiql-webpack@1.0.0-alpha.13) (2020-06-04) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.12](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.11...example-graphiql-webpack@1.0.0-alpha.12) (2020-06-04) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.11](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.10...example-graphiql-webpack@1.0.0-alpha.11) (2020-05-28) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.10](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.9...example-graphiql-webpack@1.0.0-alpha.10) (2020-05-19) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.9](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.8...example-graphiql-webpack@1.0.0-alpha.9) (2020-05-17) + +### Features + +- introduce proper vscode completion kinds ([#1488](https://github.com/graphql/graphiql/issues/1488)) ([f19aa0d](https://github.com/graphql/graphiql/commit/f19aa0ddde6109526c101c8a487f43bbb8238394)) + +# [1.0.0-alpha.8](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.7...example-graphiql-webpack@1.0.0-alpha.8) (2020-04-10) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.7](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.6...example-graphiql-webpack@1.0.0-alpha.7) (2020-04-10) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.6](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.5...example-graphiql-webpack@1.0.0-alpha.6) (2020-04-10) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.5](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.4...example-graphiql-webpack@1.0.0-alpha.5) (2020-04-06) + +### Features + +- upgrade to graphql@15.0.0 for [#1191](https://github.com/graphql/graphiql/issues/1191) ([#1204](https://github.com/graphql/graphiql/issues/1204)) ([f13c8e9](https://github.com/graphql/graphiql/commit/f13c8e9d0e66df4b051b332c7d02f4bb83e07ffd)) + +# [1.0.0-alpha.4](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.3...example-graphiql-webpack@1.0.0-alpha.4) (2020-04-03) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.3](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.2...example-graphiql-webpack@1.0.0-alpha.3) (2020-03-20) + +**Note:** Version bump only for package example-graphiql-webpack + +# [1.0.0-alpha.2](https://github.com/graphql/graphiql/compare/example-graphiql-webpack@1.0.0-alpha.0...example-graphiql-webpack@1.0.0-alpha.2) (2020-03-20) + +**Note:** Version bump only for package example-graphiql-webpack + +# 1.0.0-alpha.1 (2020-01-18) + +### Features + +- deprecate support for 15, support react 16 features ([#1107](https://github.com/graphql/graphiql/issues/1107)) ([bc4b6fc](https://github.com/graphql/graphiql/commit/bc4b6fc)) + +### BREAKING CHANGES + +- Deprecate support for React 15. Please use React 16.8 or greater for hooks support. Co-authored-by: @ryan-m-walker, @acao Reviewed-by: @benjie + +## [0.0.10](https://github.com/graphql/graphiql/compare/graphiql-example-webpack@0.0.9...graphiql-example-webpack@0.0.10) (2019-12-09) + +**Note:** Version bump only for package graphiql-example-webpack + +## [0.0.9](https://github.com/graphql/graphiql/compare/graphiql-example-webpack@0.0.8...graphiql-example-webpack@0.0.9) (2019-12-09) + +**Note:** Version bump only for package graphiql-example-webpack + +## [0.0.8](https://github.com/graphql/graphiql/compare/graphiql-example-webpack@0.0.7...graphiql-example-webpack@0.0.8) (2019-12-09) + +**Note:** Version bump only for package graphiql-example-webpack + +## [0.0.7](https://github.com/graphql/graphiql/compare/graphiql-example-webpack@0.0.6...graphiql-example-webpack@0.0.7) (2019-12-03) + +### Bug Fixes + +- ensure css files move with babel dist ([ca95547](https://github.com/graphql/graphiql/commit/ca95547)) + +## [0.0.6](https://github.com/graphql/graphiql/compare/graphiql-example-webpack@0.0.5...graphiql-example-webpack@0.0.6) (2019-12-03) + +### Bug Fixes + +- convert browserify build to webpack, fixes [#976](https://github.com/graphql/graphiql/issues/976) ([#1001](https://github.com/graphql/graphiql/issues/1001)) ([3caf041](https://github.com/graphql/graphiql/commit/3caf041)) + +## 0.0.5 (2019-11-26) + +### Bug Fixes + +- webpack resolutions for [#882](https://github.com/graphql/graphiql/issues/882), add webpack example ([ea9df3e](https://github.com/graphql/graphiql/commit/ea9df3e)) diff --git a/examples/graphiql-webpack/README.md b/examples/graphiql-webpack/README.md new file mode 100644 index 00000000000..62a4444e2bb --- /dev/null +++ b/examples/graphiql-webpack/README.md @@ -0,0 +1,14 @@ +## GraphiQL Webpack Example + +This example demonstrates how to transpile your own custom ES6 GraphiQL +implementation with webpack and babel configuration. + +There is also a no-config example with `create-react-app`: + +[![Edit graphiql-example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/graphiql-example-nhzvc) + +It appears `create-react-app` supports all the language features we require. + +### Setup + +1. `yarn` and `yarn start` from this folder to start webpack dev server diff --git a/examples/graphiql-webpack/babel.config.js b/examples/graphiql-webpack/babel.config.js new file mode 100644 index 00000000000..405e4ff7b24 --- /dev/null +++ b/examples/graphiql-webpack/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../resources/babel.config'); diff --git a/examples/graphiql-webpack/package.json b/examples/graphiql-webpack/package.json new file mode 100644 index 00000000000..8cdf89a4b56 --- /dev/null +++ b/examples/graphiql-webpack/package.json @@ -0,0 +1,37 @@ +{ + "name": "example-graphiql-webpack", + "version": "1.1.1-alpha.8", + "private": true, + "license": "MIT", + "description": "A GraphiQL example with webpack and typescript", + "scripts": { + "build-demo": "webpack-cli && mkdirp ../../packages/graphiql/webpack && cp -r dist/** ../../packages/graphiql/webpack", + "start": "NODE_ENV=development webpack-dev-server" + }, + "dependencies": { + "@graphiql/plugin-code-exporter": "^1.0.0", + "@graphiql/plugin-explorer": "^1.0.0", + "@graphiql/toolkit": "^0.8.4", + "graphiql": "^3.0.1", + "graphql": "^16.4.0", + "graphql-ws": "^5.5.5", + "react": "^18.2.0", + "regenerator-runtime": "^0.13.9" + }, + "devDependencies": { + "@babel/plugin-proposal-class-properties": "^7.8.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/preset-env": "^7.9.5", + "@babel/preset-react": "^7.9.4", + "babel-loader": "^9.1.2", + "cross-env": "^7.0.2", + "css-loader": "^6.7.3", + "html-webpack-plugin": "^4.2.0", + "react-dom": "^18.2.0", + "style-loader": "^3.3.1", + "webpack": "5.75.0", + "webpack-cli": "^5.0.1", + "webpack-dev-server": "^4.11.1", + "worker-loader": "^2.0.0" + } +} diff --git a/examples/graphiql-webpack/src/index.jsx b/examples/graphiql-webpack/src/index.jsx new file mode 100644 index 00000000000..6701dc35c17 --- /dev/null +++ b/examples/graphiql-webpack/src/index.jsx @@ -0,0 +1,55 @@ +import 'regenerator-runtime/runtime.js'; +import * as React from 'react'; +import { createRoot } from 'react-dom/client'; +import { GraphiQL } from 'graphiql'; +import { explorerPlugin } from '@graphiql/plugin-explorer'; +import { snippets } from './snippets'; +import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; +import 'graphiql/graphiql.css'; +import '@graphiql/plugin-explorer/dist/style.css'; +import '@graphiql/plugin-code-exporter/dist/style.css'; + +/** + * A manual fetcher implementation, you should probably + * just use `createGraphiQLFetcher` from `@graphiql/toolkit + * @returns + */ +const fetcher = async (graphQLParams, options) => { + const data = await fetch( + 'https://swapi-graphql.netlify.app/.netlify/functions/index', + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...options.headers, + }, + body: JSON.stringify(graphQLParams), + credentials: 'same-origin', + }, + ); + return data.json().catch(() => data.text()); +}; + +const style = { height: '100vh' }; +/** + * instantiate outside of the component lifecycle + * unless you need to pass it dynamic values from your react app, + * then use the `useMemo` hook + */ +const explorer = explorerPlugin(); +const exporter = codeExporterPlugin({ snippets }); + +const App = () => { + return ( + + ); +}; + +const root = createRoot(document.getElementById('root')); +root.render(); diff --git a/examples/graphiql-webpack/src/snippets.js b/examples/graphiql-webpack/src/snippets.js new file mode 100644 index 00000000000..f63468a3d1e --- /dev/null +++ b/examples/graphiql-webpack/src/snippets.js @@ -0,0 +1,41 @@ +const removeQueryName = query => + query.replace( + /^[^{(]+([{(])/, + (_match, openingCurlyBracketsOrParenthesis) => + `query ${openingCurlyBracketsOrParenthesis}`, + ); + +const getQuery = (arg, spaceCount) => { + const { operationDataList } = arg; + const { query } = operationDataList[0]; + const anonymousQuery = removeQueryName(query); + return ( + ' '.repeat(spaceCount) + + anonymousQuery.replaceAll('\n', '\n' + ' '.repeat(spaceCount)) + ); +}; + +const exampleSnippetOne = { + name: 'Example One', + language: 'JavaScript', + codeMirrorMode: 'jsx', + options: [], + generate: arg => `export const query = graphql\` + ${getQuery(arg, 2)} + \` + `, +}; + +const exampleSnippetTwo = { + name: 'Example Two', + language: 'JavaScript', + codeMirrorMode: 'jsx', + options: [], + generate: arg => `import { graphql } from 'graphql' + export const query = graphql\` + ${getQuery(arg, 2)} + \` + `, +}; + +export const snippets = [exampleSnippetOne, exampleSnippetTwo]; diff --git a/examples/graphiql-webpack/webpack.config.js b/examples/graphiql-webpack/webpack.config.js new file mode 100644 index 00000000000..a4bee98f5ac --- /dev/null +++ b/examples/graphiql-webpack/webpack.config.js @@ -0,0 +1 @@ +module.exports = require('../../resources/webpack.config'); diff --git a/examples/monaco-graphql-nextjs/.gitignore b/examples/monaco-graphql-nextjs/.gitignore new file mode 100644 index 00000000000..536d88c8a6d --- /dev/null +++ b/examples/monaco-graphql-nextjs/.gitignore @@ -0,0 +1 @@ +.next/ diff --git a/examples/monaco-graphql-nextjs/README.md b/examples/monaco-graphql-nextjs/README.md new file mode 100644 index 00000000000..d5901e71bac --- /dev/null +++ b/examples/monaco-graphql-nextjs/README.md @@ -0,0 +1,24 @@ +# Monaco GraphQL Next.js Example + +## Getting Started + +This is a working example of `monaco-editor` and `monaco-graphql` using +`next.js` 13 + +It shows how to use the latest monaco-editor with next.js and a custom +webworker, without using `@monaco/react` or `monaco-editor-react`'s approach of +cdn (AMD) bundles. These approaches avoid using ESM `monaco-editor` or web +workers, which prevents introducing custom web workers like with +`monaco-graphql`. + +For universal loading, we use `@next/loadable` with `{ssr: false}`, but any +similar client-side-only loading (with or without dynamic import) should be +fine. For more information on loading `monaco-editor` in esm contexts, you can +[read their docs](https://github.com/microsoft/monaco-editor/blob/main/docs/integrate-esm.md) + +This work was sponsored by [Grafbase](https://grafbase.com)! + +## Setup + +1. In monorepo root directory run `yarn` and `yarn build`. +1. In this directory run `yarn dev`. diff --git a/examples/monaco-graphql-nextjs/next-env.d.ts b/examples/monaco-graphql-nextjs/next-env.d.ts new file mode 100644 index 00000000000..4f11a03dc6c --- /dev/null +++ b/examples/monaco-graphql-nextjs/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/monaco-graphql-nextjs/next.config.js b/examples/monaco-graphql-nextjs/next.config.js new file mode 100644 index 00000000000..2ae3c4371ad --- /dev/null +++ b/examples/monaco-graphql-nextjs/next.config.js @@ -0,0 +1,53 @@ +import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; +import { patchWebpackConfig } from 'next-global-css'; + +/** @type {import('next').NextConfig} */ +const nextConfig = { + eslint: { + ignoreDuringBuilds: true, + }, + trailingSlash: true, + webpack(config, options) { + // this fixes some issues with loading web workers + config.output.publicPath = '/_next/'; + // because next.js doesn't like node_modules that import css files + // this solves the issue for monaco-editor, which relies on importing css files + patchWebpackConfig(config, options); + config.resolve.alias = { + ...config.resolve.alias, + // this solves a bug with more recent `monaco-editor` versions in next.js, + // where vscode contains a version of `marked` with modules pre-transpiled, which seems to break the build. + // + // (the error mentions that exports.Lexer is a const that can't be re-declared) + '../common/marked/marked.js': 'marked', + }; + if (!options.isServer) { + config.plugins.push( + // if you find yourself needing to override + // MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker, + // you probably just need to tweak configuration here. + new MonacoWebpackPlugin({ + // you can add other languages here as needed + languages: ['json', 'graphql'], + filename: 'static/[name].worker.js', + // this is not in the plugin readme, but saves us having to override + // MonacoEnvironment.getWorkerUrl or similar. + customLanguages: [ + { + label: 'graphql', + worker: { + id: 'graphql', + entry: 'monaco-graphql/esm/graphql.worker.js', + }, + }, + ], + }), + ); + } + // load monaco-editor provided ttf fonts + config.module.rules.push({ test: /\.ttf$/, type: 'asset/resource' }); + return config; + }, +}; + +export default nextConfig; diff --git a/examples/monaco-graphql-nextjs/package.json b/examples/monaco-graphql-nextjs/package.json new file mode 100644 index 00000000000..247e27ae154 --- /dev/null +++ b/examples/monaco-graphql-nextjs/package.json @@ -0,0 +1,31 @@ +{ + "name": "example-monaco-graphql-nextjs", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "next", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "prettier": "3.0.0-alpha.12", + "@graphiql/toolkit": "^0.8.4", + "graphql": "^16.6.0", + "graphql-ws": "^5.5.5", + "jsonc-parser": "^3.2.0", + "marked": "^4.2.12", + "monaco-editor": "^0.39.0", + "monaco-editor-webpack-plugin": "^7.0.1", + "monaco-graphql": "^1.3.0", + "next": "13.4.7", + "react": "18.2.0", + "react-dom": "18.2.0" + }, + "devDependencies": { + "@types/node": "18.16.18", + "@types/react": "18.2.14", + "next-global-css": "1.3.1", + "typescript": "5.1.3" + } +} diff --git a/examples/monaco-graphql-nextjs/public/favicon.ico b/examples/monaco-graphql-nextjs/public/favicon.ico new file mode 100644 index 00000000000..718d6fea483 Binary files /dev/null and b/examples/monaco-graphql-nextjs/public/favicon.ico differ diff --git a/examples/monaco-graphql-nextjs/src/constants.ts b/examples/monaco-graphql-nextjs/src/constants.ts new file mode 100644 index 00000000000..e6964c76988 --- /dev/null +++ b/examples/monaco-graphql-nextjs/src/constants.ts @@ -0,0 +1,84 @@ +import { editor, Uri } from 'monaco-graphql/esm/monaco-editor'; +import { initializeMode } from 'monaco-graphql/esm/initializeMode'; + +type ModelType = 'operations' | 'variables' | 'response'; + +export const GRAPHQL_URL = 'https://countries.trevorblades.com'; + +export const DEFAULT_EDITOR_OPTIONS: editor.IStandaloneEditorConstructionOptions = + { + theme: 'vs-dark', + minimap: { + enabled: false, + }, + }; + +export const STORAGE_KEY = { + operations: 'operations', + variables: 'variables', +}; + +export const DEFAULT_VALUE: Record = { + operations: + localStorage.getItem(STORAGE_KEY.operations) ?? + `# CMD/CTRL + Return/Enter will execute the operation, +# same in the variables editor below +# also available via context menu & F1 command palette + +query($code: ID!) { + country(code: $code) { + awsRegion + native + phone + } +}`, + variables: + localStorage.getItem(STORAGE_KEY.variables) ?? + `{ + "code": "UA" +}`, + response: '', +}; + +export const FILE_SYSTEM_PATH: Record< + ModelType, + `${string}.${'graphql' | 'json'}` +> = { + operations: 'operations.graphql', + variables: 'variables.json', + response: 'response.json', +}; + +export const MONACO_GRAPHQL_API = initializeMode({ + diagnosticSettings: { + validateVariablesJSON: { + [Uri.file(FILE_SYSTEM_PATH.operations).toString()]: [ + Uri.file(FILE_SYSTEM_PATH.variables).toString(), + ], + }, + jsonDiagnosticSettings: { + validate: true, + schemaValidation: 'error', + // set these again, because we are entirely re-setting them here + allowComments: true, + trailingCommas: 'ignore', + }, + }, +}); + +export const MODEL: Record = { + operations: getOrCreateModel('operations'), + variables: getOrCreateModel('variables'), + response: getOrCreateModel('response'), +}; + +function getOrCreateModel( + type: 'operations' | 'variables' | 'response', +): editor.ITextModel { + const uri = Uri.file(FILE_SYSTEM_PATH[type]); + const defaultValue = DEFAULT_VALUE[type]; + const language = uri.path.split('.').pop(); + return ( + editor.getModel(uri) ?? editor.createModel(defaultValue, language, uri) + ); +} diff --git a/examples/monaco-graphql-nextjs/src/editor.tsx b/examples/monaco-graphql-nextjs/src/editor.tsx new file mode 100644 index 00000000000..ea861a67230 --- /dev/null +++ b/examples/monaco-graphql-nextjs/src/editor.tsx @@ -0,0 +1,162 @@ +import { ReactElement, useEffect, useRef, useState } from 'react'; +import { getIntrospectionQuery, IntrospectionQuery } from 'graphql'; +import { + editor, + KeyMod, + KeyCode, + languages, +} from 'monaco-graphql/esm/monaco-editor'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import * as JSONC from 'jsonc-parser'; +import { + DEFAULT_EDITOR_OPTIONS, + MONACO_GRAPHQL_API, + STORAGE_KEY, + GRAPHQL_URL, + MODEL, +} from './constants'; + +const fetcher = createGraphiQLFetcher({ url: GRAPHQL_URL }); + +async function getSchema(): Promise { + const data = await fetcher({ + query: getIntrospectionQuery(), + operationName: 'IntrospectionQuery', + }); + const introspectionJSON = + 'data' in data && (data.data as unknown as IntrospectionQuery); + + if (!introspectionJSON) { + throw new Error( + 'this demo does not support subscriptions or http multipart yet', + ); + } + return introspectionJSON; +} + +function debounce any>(duration: number, fn: F) { + let timeout = 0; + return (...args: Parameters) => { + if (timeout) { + window.clearTimeout(timeout); + } + timeout = window.setTimeout(() => { + timeout = 0; + fn(args); + }, duration); + }; +} + +const queryAction: editor.IActionDescriptor = { + id: 'graphql-run', + label: 'Run Operation', + contextMenuOrder: 0, + contextMenuGroupId: 'graphql', + keybindings: [ + // eslint-disable-next-line no-bitwise + KeyMod.CtrlCmd | KeyCode.Enter, + ], + async run() { + const result = await fetcher({ + query: MODEL.operations.getValue(), + variables: JSONC.parse(MODEL.variables.getValue()), + }); + // TODO: this demo only supports a single iteration for http GET/POST, + // no multipart or subscriptions yet. + // @ts-expect-error + const data = await result.next(); + MODEL.response.setValue(JSON.stringify(data.value, null, 2)); + }, +}; +// set these early on so that initial variables with comments don't flash an error +languages.json.jsonDefaults.setDiagnosticsOptions({ + allowComments: true, + trailingCommas: 'ignore', +}); + +export default function Editor(): ReactElement { + const operationsRef = useRef(null); + const variablesRef = useRef(null); + const responseRef = useRef(null); + const [operationsEditor, setOperationsEditor] = + useState(); + const [variablesEditor, setVariablesEditor] = + useState(); + const [responseEditor, setResponseEditor] = + useState(); + const [schema, setSchema] = useState(); + const [loading, setLoading] = useState(false); + /** + * Create the models & editors + */ + useEffect(() => { + if (!operationsEditor) { + const codeEditor = editor.create(operationsRef.current!, { + model: MODEL.operations, + ...DEFAULT_EDITOR_OPTIONS, + }); + codeEditor.addAction(queryAction); + MODEL.operations.onDidChangeContent( + debounce(300, () => { + localStorage.setItem( + STORAGE_KEY.operations, + MODEL.operations.getValue(), + ); + }), + ); + setOperationsEditor(codeEditor); + } + if (!variablesEditor) { + const codeEditor = editor.create(variablesRef.current!, { + model: MODEL.variables, + ...DEFAULT_EDITOR_OPTIONS, + }); + codeEditor.addAction(queryAction); + MODEL.variables.onDidChangeContent( + debounce(300, () => { + localStorage.setItem( + STORAGE_KEY.variables, + MODEL.variables.getValue(), + ); + }), + ); + setVariablesEditor(codeEditor); + } + if (!responseEditor) { + setResponseEditor( + editor.create(responseRef.current!, { + model: MODEL.response, + ...DEFAULT_EDITOR_OPTIONS, + readOnly: true, + smoothScrolling: true, + }), + ); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps -- only run once on mount + /** + * Handle the initial schema load + */ + useEffect(() => { + if (schema || loading) { + return; + } + setLoading(true); + void getSchema().then(async introspectionJSON => { + MONACO_GRAPHQL_API.setSchemaConfig([ + { introspectionJSON, uri: 'my-schema.graphql' }, + ]); + setSchema(introspectionJSON); + setLoading(false); + }); + }, [schema, loading]); + + return ( + <> +
+
+
+
+
+ + ); +} diff --git a/examples/monaco-graphql-nextjs/src/pages/_app.tsx b/examples/monaco-graphql-nextjs/src/pages/_app.tsx new file mode 100644 index 00000000000..70321d1ef2f --- /dev/null +++ b/examples/monaco-graphql-nextjs/src/pages/_app.tsx @@ -0,0 +1,6 @@ +import type { AppProps } from 'next/app'; +import '../style.css'; + +export default function App({ Component, pageProps }: AppProps) { + return ; +} diff --git a/examples/monaco-graphql-nextjs/src/pages/index.tsx b/examples/monaco-graphql-nextjs/src/pages/index.tsx new file mode 100644 index 00000000000..f6541d67d6a --- /dev/null +++ b/examples/monaco-graphql-nextjs/src/pages/index.tsx @@ -0,0 +1,18 @@ +import Head from 'next/head'; +import dynamic from 'next/dynamic'; + +const DynamicEditor = dynamic(() => import('../editor'), { ssr: false }); + +export default function Home() { + return ( + <> + + Monaco Next.js Example + + + + + + + ); +} diff --git a/examples/monaco-graphql-nextjs/src/style.css b/examples/monaco-graphql-nextjs/src/style.css new file mode 100644 index 00000000000..12b19fc2642 --- /dev/null +++ b/examples/monaco-graphql-nextjs/src/style.css @@ -0,0 +1,17 @@ +body { + margin: 0; + height: 100vh; +} + +#__next { + display: flex; + height: inherit; +} + +.pane { + width: 50%; +} + +.left-editor { + height: 50%; +} diff --git a/examples/monaco-graphql-nextjs/tsconfig.json b/examples/monaco-graphql-nextjs/tsconfig.json new file mode 100644 index 00000000000..2159bf45c40 --- /dev/null +++ b/examples/monaco-graphql-nextjs/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/examples/monaco-graphql-react-vite/README.md b/examples/monaco-graphql-react-vite/README.md new file mode 100644 index 00000000000..17be8deb661 --- /dev/null +++ b/examples/monaco-graphql-react-vite/README.md @@ -0,0 +1,16 @@ +# Monaco-GraphQL React Vite Example + +## Getting Started + +This is an extremely naive & minimalist implementation of `monaco-graphql` with +`react` using `vite` as a bundler. + +This workspace could be used to help us prototype components & hooks for +`@graphiql/react` + +[Here is a StackBlitz demo of this example](https://stackblitz.com/edit/monaco-graphql-react-vite?file=src/App.tsx) + +## Setup + +1. In monorepo root directory run `yarn` and `yarn build`. +1. In this directory run `yarn dev`. diff --git a/examples/monaco-graphql-react-vite/index.html b/examples/monaco-graphql-react-vite/index.html new file mode 100644 index 00000000000..faadf86d987 --- /dev/null +++ b/examples/monaco-graphql-react-vite/index.html @@ -0,0 +1,13 @@ + + + + + + + Monaco React Vite Example + + +
Loading...
+ + + diff --git a/examples/monaco-graphql-react-vite/package.json b/examples/monaco-graphql-react-vite/package.json new file mode 100644 index 00000000000..b79b76ccdad --- /dev/null +++ b/examples/monaco-graphql-react-vite/package.json @@ -0,0 +1,25 @@ +{ + "name": "example-monaco-graphql-react-vite", + "private": true, + "version": "0.0.0", + "dependencies": { + "prettier": "3.0.0-alpha.12", + "@graphiql/toolkit": "^0.8.4", + "graphql-language-service": "^5.1.7", + "monaco-graphql": "^1.3.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "graphql": "^16.6.0", + "jsonc-parser": "^3.2.0", + "monaco-editor": "^0.39.0" + }, + "devDependencies": { + "vite": "^4.3.9", + "@vitejs/plugin-react": "^4.0.1", + "vite-plugin-monaco-editor": "^1.1.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build" + } +} diff --git a/examples/monaco-graphql-react-vite/src/constants.ts b/examples/monaco-graphql-react-vite/src/constants.ts new file mode 120000 index 00000000000..5e6ab6e6068 --- /dev/null +++ b/examples/monaco-graphql-react-vite/src/constants.ts @@ -0,0 +1 @@ +../../monaco-graphql-nextjs/src/constants.ts \ No newline at end of file diff --git a/examples/monaco-graphql-react-vite/src/editor.tsx b/examples/monaco-graphql-react-vite/src/editor.tsx new file mode 120000 index 00000000000..b49aef8cc69 --- /dev/null +++ b/examples/monaco-graphql-react-vite/src/editor.tsx @@ -0,0 +1 @@ +../../monaco-graphql-nextjs/src/editor.tsx \ No newline at end of file diff --git a/examples/monaco-graphql-react-vite/src/index.tsx b/examples/monaco-graphql-react-vite/src/index.tsx new file mode 100644 index 00000000000..83f9e302e91 --- /dev/null +++ b/examples/monaco-graphql-react-vite/src/index.tsx @@ -0,0 +1,5 @@ +import { createRoot } from 'react-dom/client'; +import Editor from './editor'; + +const root = createRoot(document.getElementById('__next')!); +root.render(); diff --git a/examples/monaco-graphql-react-vite/src/style.css b/examples/monaco-graphql-react-vite/src/style.css new file mode 120000 index 00000000000..32a7938addd --- /dev/null +++ b/examples/monaco-graphql-react-vite/src/style.css @@ -0,0 +1 @@ +../../monaco-graphql-nextjs/src/style.css \ No newline at end of file diff --git a/examples/monaco-graphql-react-vite/tsconfig.json b/examples/monaco-graphql-react-vite/tsconfig.json new file mode 100644 index 00000000000..621557798a2 --- /dev/null +++ b/examples/monaco-graphql-react-vite/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/monaco-graphql-react-vite/vite.config.ts b/examples/monaco-graphql-react-vite/vite.config.ts new file mode 100644 index 00000000000..e0bcfff2fec --- /dev/null +++ b/examples/monaco-graphql-react-vite/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import monacoEditorPlugin from 'vite-plugin-monaco-editor'; + +export default defineConfig({ + plugins: [ + react(), + monacoEditorPlugin({ + publicPath: 'workers', + languageWorkers: ['json', 'editorWorkerService'], + customWorkers: [ + { + label: 'graphql', + entry: 'monaco-graphql/esm/graphql.worker', + }, + ], + }), + ], +}); diff --git a/examples/monaco-graphql-webpack/CHANGELOG.md b/examples/monaco-graphql-webpack/CHANGELOG.md new file mode 100644 index 00000000000..f2c7debeffc --- /dev/null +++ b/examples/monaco-graphql-webpack/CHANGELOG.md @@ -0,0 +1,80 @@ +# Change Log + +## 1.1.1 + +### Patch Changes + +- Updated dependencies [[`e68cb8bc`](https://github.com/graphql/graphiql/commit/e68cb8bcaf9baddf6fca747abab871ecd1bc7a4c), [`f788e65a`](https://github.com/graphql/graphiql/commit/f788e65aff267ec873237034831d1fd936222a9b), [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4), [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d), [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba)]: + - graphql-language-service@5.1.2 + - monaco-graphql@1.1.8 + +All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.1.1-alpha.7](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.1-alpha.6...example-monaco-graphql-webpack@1.1.1-alpha.7) (2021-01-07) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +## [1.1.1-alpha.6](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.1-alpha.5...example-monaco-graphql-webpack@1.1.1-alpha.6) (2021-01-07) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +## [1.1.1-alpha.5](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.1-alpha.4...example-monaco-graphql-webpack@1.1.1-alpha.5) (2021-01-07) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +## [1.1.1-alpha.4](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.1-alpha.3...example-monaco-graphql-webpack@1.1.1-alpha.4) (2021-01-03) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +## [1.1.1-alpha.3](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.1-alpha.2...example-monaco-graphql-webpack@1.1.1-alpha.3) (2020-12-28) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +## [1.1.1-alpha.2](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.1-alpha.1...example-monaco-graphql-webpack@1.1.1-alpha.2) (2020-08-22) + +### Bug Fixes + +- improve setSchema & schema loading, allow primitive schema ([#1648](https://github.com/graphql/graphiql/issues/1648)) ([975f29e](https://github.com/graphql/graphiql/commit/975f29ed6e21c7354c42ed778dfd1b52287f70c6)) + +## [1.1.1-alpha.1](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.1-alpha.0...example-monaco-graphql-webpack@1.1.1-alpha.1) (2020-08-12) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +## [1.1.1-alpha.0](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.1.0...example-monaco-graphql-webpack@1.1.1-alpha.0) (2020-08-10) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +# [1.1.0](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.0.0...example-monaco-graphql-webpack@1.1.0) (2020-08-06) + +### Features + +- [RFC] GraphiQL rewrite for monaco editor, react context and redesign, i18n ([#1523](https://github.com/graphql/graphiql/issues/1523)) ([ad730cd](https://github.com/graphql/graphiql/commit/ad730cdc2e3cb7216d821a01725c60475989ee20)) + +# [1.0.0](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.0.0-alpha.8...example-monaco-graphql-webpack@1.0.0) (2020-06-11) + +### Features + +- standalone monaco API ([#1575](https://github.com/graphql/graphiql/issues/1575)) ([954aa3d](https://github.com/graphql/graphiql/commit/954aa3d7159fd26bba9650824e0f668e417ca64f)) + +# [1.0.0-alpha.8](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.0.0-alpha.7...example-monaco-graphql-webpack@1.0.0-alpha.8) (2020-06-04) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +# [1.0.0-alpha.7](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.0.0-alpha.6...example-monaco-graphql-webpack@1.0.0-alpha.7) (2020-06-04) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +# [1.0.0-alpha.6](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.0.0-alpha.5...example-monaco-graphql-webpack@1.0.0-alpha.6) (2020-05-28) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +# [1.0.0-alpha.5](https://github.com/graphql/graphiql/compare/example-monaco-graphql-webpack@1.0.0-alpha.4...example-monaco-graphql-webpack@1.0.0-alpha.5) (2020-05-19) + +**Note:** Version bump only for package example-monaco-graphql-webpack + +# 1.0.0-alpha.4 (2020-05-17) + +### Features + +- Monaco Mode - Phase 2 - Mode & Worker ([#1459](https://github.com/graphql/graphiql/issues/1459)) ([bc95fb4](https://github.com/graphql/graphiql/commit/bc95fb46459a4437ff9471ff43c98e1c5c50f51e)) +- monaco-graphql docs, api, improvements ([#1521](https://github.com/graphql/graphiql/issues/1521)) ([c79158c](https://github.com/graphql/graphiql/commit/c79158c72e976ab286e7ec3fded7f3e2d24e50d0)) diff --git a/examples/monaco-graphql-webpack/README.md b/examples/monaco-graphql-webpack/README.md new file mode 100644 index 00000000000..3106e9a6ce7 --- /dev/null +++ b/examples/monaco-graphql-webpack/README.md @@ -0,0 +1,17 @@ +A simple example of `monaco-graphql` using webpack 4 + +[live demo](https://monaco-graphql.netlify.com) of the monaco webpack example + +### Setup + +`yarn` and `yarn start` from this folder to start webpack dev server + +### JS only + +If you want to learn how to bundle `monaco-graphql` using webpack without +typescript, these steps will help: + +1. rename .ts files to .js +1. rename .ts to .js in webpack.config.js +1. remove fork ts checker plugin from webpack.config.js +1. remove typescript annotations from the renamed files diff --git a/examples/monaco-graphql-webpack/babel.config.js b/examples/monaco-graphql-webpack/babel.config.js new file mode 100644 index 00000000000..796fefb7b62 --- /dev/null +++ b/examples/monaco-graphql-webpack/babel.config.js @@ -0,0 +1,11 @@ +module.exports = { + presets: [ + require.resolve('@babel/preset-env'), + require.resolve('@babel/preset-typescript'), + require.resolve('@babel/preset-react'), + ], + plugins: [ + require.resolve('@babel/plugin-proposal-class-properties'), + require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), + ], +}; diff --git a/examples/monaco-graphql-webpack/package.json b/examples/monaco-graphql-webpack/package.json new file mode 100644 index 00000000000..35933015397 --- /dev/null +++ b/examples/monaco-graphql-webpack/package.json @@ -0,0 +1,40 @@ +{ + "name": "example-monaco-graphql-webpack", + "version": "1.1.1", + "private": true, + "license": "MIT", + "description": "A simple monaco example with webpack and typescript", + "scripts": { + "build": "cross-env NODE_ENV=production webpack-cli", + "start": "cross-env NODE_ENV=development webpack-cli serve" + }, + "dependencies": { + "graphql": "^16.6.0", + "graphql-language-service": "^5.1.7", + "json-schema": "^0.4.0", + "jsonc-parser": "3.2.0", + "monaco-editor": "^0.36.0", + "monaco-graphql": "^1.3.0", + "prettier": "3.0.0-alpha.12" + }, + "devDependencies": { + "@babel/core": "^7.21.0", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "7.8.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@webpack-cli/serve": "^2.0.1", + "babel-loader": "^9.1.2", + "cross-env": "^7.0.2", + "css-loader": "^6.7.3", + "file-loader": "6.2.0", + "html-webpack-plugin": "^5.5.0", + "monaco-editor-webpack-plugin": "^7.0.1", + "style-loader": "^3.3.1", + "typescript": "^4.6.3", + "webpack": "5.76.0", + "webpack-cli": "^5.0.1", + "webpack-dev-server": "^4.11.1" + } +} diff --git a/examples/monaco-graphql-webpack/src/editors.ts b/examples/monaco-graphql-webpack/src/editors.ts new file mode 100644 index 00000000000..f9fa905c666 --- /dev/null +++ b/examples/monaco-graphql-webpack/src/editors.ts @@ -0,0 +1,110 @@ +import { editor, Uri } from 'monaco-graphql/esm/monaco-editor'; + +const GRAPHQL_LANGUAGE_ID = 'graphql'; + +const operationString = + localStorage.getItem('operations') ?? + ` +# right click to view context menu +# F1 for command palette +# enjoy prettier formatting, autocompletion, +# validation, hinting and more for GraphQL SDL and operations! +query Example( + $owner: String! + $name: String! + $reviewEvent: PullRequestReviewEvent! + $user: FollowUserInput! +) { + repository(owner: $owner, name: $name) { + stargazerCount + } +} +`; + +const variablesString = + localStorage.getItem('variables') ?? + `{ + "reviewEvent": "graphql", + "name": true +}`; + +const resultsString = '{}'; + +const schemaSdlString = localStorage.getItem('schema-sdl') ?? ''; + +const THEME = 'vs-dark'; + +export function createEditors() { + const variablesModel = editor.createModel( + variablesString, + 'json', + Uri.file('/1/variables.json'), + ); + + const variablesEditor = editor.create(document.getElementById('variables'), { + model: variablesModel, + language: 'json', + formatOnPaste: true, + formatOnType: true, + theme: THEME, + comments: { + insertSpace: true, + ignoreEmptyLines: true, + }, + }); + + const operationModel = editor.createModel( + operationString, + GRAPHQL_LANGUAGE_ID, + Uri.file('/1/operation.graphql'), + ); + + const operationEditor = editor.create(document.getElementById('operation'), { + model: operationModel, + formatOnPaste: true, + formatOnType: true, + folding: true, + theme: THEME, + language: GRAPHQL_LANGUAGE_ID, + }); + + const schemaModel = editor.createModel( + schemaSdlString, + GRAPHQL_LANGUAGE_ID, + Uri.file('/1/schema.graphqls'), + ); + + const schemaEditor = editor.create(document.getElementById('schema-sdl'), { + model: schemaModel, + formatOnPaste: true, + formatOnType: true, + folding: true, + theme: THEME, + language: GRAPHQL_LANGUAGE_ID, + }); + + const resultsModel = editor.createModel( + resultsString, + 'json', + Uri.file('/1/results.json'), + ); + + const resultsEditor = editor.create(document.getElementById('results'), { + model: resultsModel, + language: 'json', + theme: THEME, + wordWrap: 'on', + readOnly: true, + showFoldingControls: 'always', + }); + + return { + operationEditor, + variablesEditor, + resultsEditor, + schemaEditor, + operationModel, + variablesModel, + schemaModel, + }; +} diff --git a/examples/monaco-graphql-webpack/src/index.html.ejs b/examples/monaco-graphql-webpack/src/index.html.ejs new file mode 100644 index 00000000000..9e2d246284a --- /dev/null +++ b/examples/monaco-graphql-webpack/src/index.html.ejs @@ -0,0 +1,33 @@ + + + + + + Monaco Example! + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/examples/monaco-graphql-webpack/src/index.ts b/examples/monaco-graphql-webpack/src/index.ts new file mode 100644 index 00000000000..3b7c47fc821 --- /dev/null +++ b/examples/monaco-graphql-webpack/src/index.ts @@ -0,0 +1,320 @@ +/* global netlify */ + +import { editor, KeyMod, KeyCode } from 'monaco-graphql/esm/monaco-editor'; +import * as JSONC from 'jsonc-parser'; +import { initializeMode } from 'monaco-graphql/esm/initializeMode'; + +import { createEditors } from './editors'; +import { schemaFetcher, schemaOptions } from './schema'; +import './style.css'; +import type { MonacoGraphQLAPI } from 'monaco-graphql'; + +const SITE_ID = '46a6b3c8-992f-4623-9a76-f1bd5d40505c'; + +let monacoGraphQLAPI: MonacoGraphQLAPI | null = null; + +void render(); + +async function render() { + if (!schemaFetcher.token) { + renderGithubLoginButton(); + + return; + } + monacoGraphQLAPI ||= initializeMode({ + formattingOptions: { + prettierConfig: { + printWidth: 120, + }, + }, + }); + + document.getElementById('github-login-wrapper')?.remove(); + document + .getElementById('session-editor') + ?.setAttribute('style', 'display: flex'); + document + .getElementById('toolbar') + ?.setAttribute('style', 'display: inline-flex'); + + const toolbar = document.getElementById('toolbar')!; + const editors = createEditors(); + const { + operationModel, + operationEditor, + variablesEditor, + schemaEditor, + resultsEditor, + variablesModel, + schemaModel, + } = editors; + const { schemaReloadButton, executeOpButton, schemaPicker } = + renderToolbar(toolbar); + + renderGithubLoginButton(); + + const operationUri = operationModel.uri.toString(); + + const schema = await schemaFetcher.loadSchema(); + if (schema) { + console.log('loaded schema', schema); + monacoGraphQLAPI.setSchemaConfig([ + { ...schema, fileMatch: [operationUri, schemaModel.uri.toString()] }, + ]); + + schemaEditor.setValue(schema.documentString || ''); + } + + monacoGraphQLAPI.setDiagnosticSettings({ + validateVariablesJSON: { + [operationUri]: [variablesModel.uri.toString()], + }, + jsonDiagnosticSettings: { + // jsonc tip! + allowComments: true, + schemaValidation: 'error', + // this is nice too + trailingCommas: 'warning', + }, + }); + operationModel.onDidChangeContent(() => { + setTimeout(() => { + localStorage.setItem('operations', operationModel.getValue()); + }, 200); + }); + variablesModel.onDidChangeContent(() => { + setTimeout(() => { + localStorage.setItem('variables', variablesModel.getValue()); + }, 200); + }); + schemaModel.onDidChangeContent(() => { + setTimeout(async () => { + const value = schemaModel.getValue(); + localStorage.setItem('schema-sdl', value); + + const nextSchema = await schemaFetcher.overrideSchema(value); + + if (nextSchema) { + monacoGraphQLAPI?.setSchemaConfig([ + { + ...nextSchema, + fileMatch: [operationUri, schemaModel.uri.toString()], + }, + ]); + } + }, 200); + }); + + /** + * Choosing a new schema + */ + schemaPicker.addEventListener( + 'input', + async function SchemaSelectionHandler(_ev: Event) { + if (schemaPicker.value === schemaFetcher.currentSchema.value) { + return; + } + + const schemaResult = await schemaFetcher.changeSchema(schemaPicker.value); + if (schemaResult && monacoGraphQLAPI) { + monacoGraphQLAPI.setSchemaConfig([ + { + ...schemaResult, + fileMatch: [operationModel.uri.toString()], + }, + ]); + schemaEditor.setValue(schemaResult.documentString || ''); + } + }, + ); + + /** + * Reloading your schema + */ + schemaReloadButton.addEventListener('click', async () => { + const schemaResult = await schemaFetcher.loadSchema(); + if (schemaResult) { + schemaEditor.setValue(schemaResult.documentString || ''); + } + }); + + /** + * Execute GraphQL operations, for reference! + * monaco-graphql itself doesn't do anything with handling operations yet, but it may soon! + */ + + const getOperationHandler = () => async () => { + try { + const operation = operationEditor.getValue(); + const variables = variablesEditor.getValue(); + const body: { variables?: string; query: string } = { + query: operation, + }; + // parse the variables with JSONC, so we can have comments! + const parsedVariables = JSONC.parse(variables); + if (parsedVariables && Object.keys(parsedVariables).length) { + body.variables = JSON.stringify(parsedVariables, null, 2); + } + const result = await fetch(schemaFetcher.currentSchema.value, { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...schemaFetcher.currentSchema?.headers, + }, + body: JSON.stringify(body, null, 2), + }); + + const resultText = await result.text(); + resultsEditor.setValue(JSON.stringify(JSON.parse(resultText), null, 2)); + } catch (err) { + if (err instanceof Error) { + resultsEditor.setValue(err.toString()); + } + } + }; + + const operationHandler = getOperationHandler(); + + executeOpButton.addEventListener('click', operationHandler); + executeOpButton.addEventListener('touchend', operationHandler); + + /** + * Add an editor operation to the command palette & keyboard shortcuts + */ + const opAction: editor.IActionDescriptor = { + id: 'graphql-run', + label: 'Run Operation', + contextMenuOrder: 0, + contextMenuGroupId: 'graphql', + keybindings: [ + // eslint-disable-next-line no-bitwise + KeyMod.CtrlCmd | KeyCode.Enter, + ], + run: operationHandler, + }; + + /** + * Add a reload operation to the command palette & keyboard shortcuts + */ + const reloadAction: editor.IActionDescriptor = { + id: 'graphql-reload', + label: 'Reload Schema', + contextMenuOrder: 0, + contextMenuGroupId: 'graphql', + keybindings: [ + KeyMod.CtrlCmd | KeyCode?.KeyR, // eslint-disable-line no-bitwise + ], + async run() { + await schemaFetcher.loadSchema(); + }, + }; + + operationEditor.addAction(opAction); + variablesEditor.addAction(opAction); + resultsEditor.addAction(opAction); + operationEditor.addAction(reloadAction); +} + +function renderToolbar(toolbar: HTMLElement) { + toolbar.innerHTML = ''; + + const schemaStatus = document.createElement('div'); + const schemaReloadButton = document.createElement('button'); + const executeOpButton = document.createElement('button'); + const schemaPicker = getSchemaPicker(); + const executionTray = document.createElement('div'); + + executionTray.id = 'execution-tray'; + executionTray.append(executeOpButton); + executionTray.classList.add('align-right'); + + executeOpButton.id = 'execute-op'; + executeOpButton.textContent = 'Run Operation ➀'; + executeOpButton.title = 'Execute the active GraphQL Operation'; + + schemaReloadButton.classList.add('reload-button'); + schemaReloadButton.innerHTML = 'πŸ”„'; + schemaReloadButton.title = 'Reload the graphql schema'; + + schemaStatus.id = 'schema-status'; + schemaStatus.innerHTML = 'Schema Empty'; + + toolbar.append( + schemaPicker, + schemaReloadButton, + schemaStatus, + executeOpButton, + ); + return { schemaReloadButton, executeOpButton, schemaStatus, schemaPicker }; +} + +function getSchemaPicker(): HTMLSelectElement { + const schemaPicker = document.createElement('select'); + schemaPicker.id = 'schema-picker'; + + for (const option of schemaOptions) { + const optEl = document.createElement('option'); + optEl.value = option.value; + optEl.label = option.label; + if (option.default) { + optEl.selected = true; + } + schemaPicker.append(optEl); + } + + return schemaPicker; +} + +/** + * login using the provided netlify API for oauth + */ +export function renderGithubLoginButton() { + const githubLoginWrapper = document.createElement('div'); + githubLoginWrapper.id = 'github-login-wrapper'; + githubLoginWrapper.innerHTML = + "

Using Netlify's OAuth client to retrieve your token, you'll see a simple GitHub graphql monaco-graphql Demo.

"; + const githubButton = document.createElement('button'); + + const logoutButton = document.createElement('button'); + + logoutButton.innerHTML = 'Logout'; + + logoutButton.onclick = async e => { + e.preventDefault(); + schemaFetcher.logout(); + await render(); + document + .getElementById('session-editor') + ?.setAttribute('style', 'display: none'); + document.getElementById('toolbar')?.setAttribute('style', 'display: none'); + }; + + if (schemaFetcher.token) { + document.getElementById('github-login-wrapper')?.remove(); + const toolbar = document.getElementById('toolbar'); + toolbar?.append(logoutButton); + } else { + githubLoginWrapper.append(githubButton); + document.getElementById('flex-wrapper')?.prepend(githubLoginWrapper); + } + + githubButton.id = 'login'; + githubButton.innerHTML = 'GitHub Login'; + + githubButton.onclick = e => { + e.preventDefault(); + // @ts-expect-error + const authenticator = new netlify.default({ site_id: SITE_ID }); + authenticator.authenticate( + { provider: 'github', scope: ['user'] }, + async (err: Error, data: { token: string }) => { + if (err) { + console.error('Error authenticating with GitHub:', err); + } else { + await schemaFetcher.setApiToken(data.token); + await render(); + } + }, + ); + }; +} diff --git a/examples/monaco-graphql-webpack/src/schema.ts b/examples/monaco-graphql-webpack/src/schema.ts new file mode 100644 index 00000000000..b3c2b677203 --- /dev/null +++ b/examples/monaco-graphql-webpack/src/schema.ts @@ -0,0 +1,147 @@ +import { + buildClientSchema, + getIntrospectionQuery, + printSchema, + parse, + buildASTSchema, +} from 'graphql'; +import type { SchemaConfig } from 'monaco-graphql'; +import { Uri } from 'monaco-graphql/esm/monaco-editor'; + +const SCHEMA_URL = 'https://api.github.com/graphql'; +const API_TOKEN = localStorage.getItem('ghapi') || null; + +const localStorageKey = 'ghapi'; + +export const schemaOptions = [ + { + value: SCHEMA_URL, + label: 'Github API', + default: true, + headers: Object.create(null), + }, + { + value: 'https://api.spacex.land/graphql', + label: 'SpaceX GraphQL API', + headers: Object.create(null), + }, +]; + +const setSchemaStatus = (message: string) => { + const schemaStatus = document.getElementById('schema-status'); + if (schemaStatus) { + const html = message; + schemaStatus.innerHTML = html; + } +}; + +class MySchemaFetcher { + private _options: typeof schemaOptions; + private _currentSchema: (typeof schemaOptions)[0]; + private _schemaCache = new Map(); + private _schemaOverride = new Map(); + + constructor(options = schemaOptions) { + this._options = options; + this._currentSchema = schemaOptions[0]; + if (API_TOKEN) { + this._currentSchema.headers.authorization = `Bearer ${API_TOKEN}`; + } + } + public get currentSchema() { + return this._currentSchema; + } + public get token() { + return this._currentSchema.headers.authorization; + } + async getSchema() { + const cacheItem = this._schemaCache.get(this._currentSchema.value); + if (cacheItem) { + return { + ...cacheItem, + documentString: this.getOverride() || cacheItem.documentString, + }; + } + return this.loadSchema(); + } + async setApiToken(token: string) { + this._currentSchema.headers.authorization = `Bearer ${token}`; + localStorage.setItem(localStorageKey, token); + } + logout() { + this._currentSchema.headers.authorization = undefined; + localStorage.removeItem(localStorageKey); + } + async loadSchema() { + try { + setSchemaStatus('Schema Loading...'); + const url = this._currentSchema.value; + + const headers = { + 'content-type': 'application/json', + }; + const result = await fetch(url, { + method: 'POST', + headers: { + ...headers, + ...this._currentSchema.headers, + }, + body: JSON.stringify( + { + query: getIntrospectionQuery(), + operationName: 'IntrospectionQuery', + }, + null, + 2, + ), + }); + const introspectionJSON = (await result.json()).data; + const documentString = printSchema(buildClientSchema(introspectionJSON)); + this._schemaCache.set(url, { + introspectionJSON, + documentString, + uri: Uri.parse(url).toString(), + }); + + this.clearOverride(); + + setSchemaStatus('Schema Loaded'); + } catch { + setSchemaStatus('Schema error'); + } + + return this._schemaCache.get(this._currentSchema.value); + } + async changeSchema(uri: string) { + this._currentSchema = this._options.find(opt => opt.value === uri)!; + this.clearOverride(); + return this.getSchema(); + } + + getOverride() { + return this._schemaOverride.get(this._currentSchema.value); + } + + clearOverride() { + this._schemaOverride.delete(this._currentSchema.value); + } + + async overrideSchema(sdl: string) { + if (isValid(sdl)) { + this._schemaOverride.set(this._currentSchema.value, sdl); + return this.getSchema(); + } + } +} + +function isValid(sdl: string) { + try { + const ast = parse(sdl); + buildASTSchema(ast); + return true; + } catch { + return false; + } +} + +export const schemaFetcher = new MySchemaFetcher(schemaOptions); diff --git a/examples/monaco-graphql-webpack/src/style.css b/examples/monaco-graphql-webpack/src/style.css new file mode 100644 index 00000000000..51479b9a19d --- /dev/null +++ b/examples/monaco-graphql-webpack/src/style.css @@ -0,0 +1,114 @@ +body { + background-color: #1e1e1e; + margin: 0; + padding: 0; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} +.div { + margin: 0; + padding: 0; +} + +.full-height { + height: 100vh; +} + +.half-width { + width: 50%; +} +.column { + display: flex; + flex-direction: column; + align-items: stretch; +} +.row { + display: flex; + flex-direction: row; +} + +.align-right { + /* align-self: stretch; */ + margin-left: auto; + align-self: stretch; +} + +/* Editors */ + +#flex-wrapper { + display: flex; + align-items: stretch; +} +#operation { + height: 60vh; + min-height: 260px; +} +#variables { + height: 30vh; + align-items: stretch; +} +#results { + align-items: stretch; + height: 45vh; +} +#schema-sdl { + align-items: stretch; + height: 45vh; +} + +/* Toolbar */ + +#toolbar { + min-height: 40px; + background-color: #1e1e1e; + display: inline-flex; + align-items: stretch; +} + +#toolbar > button, +#toolbar > select, +#toolbar > div, +button#execute-op { + margin: 0px 4px; + padding: 4px; +} + +#toolbar button, +#toolbar select { + background-color: #1e1e1e; + color: #eee; + border: 1px solid #eee; + border-radius: 4px; +} + +#toolbar button:hover, +select:hover, +button:focus, +select:focus { + background-color: darkslategrey; +} + +#execution-tray { + align-items: flex-end; +} + +#toolbar #schema-status { + color: #eee; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + padding: 11px 4px; + font-size: small; +} + +#toolbar button.reload-button { + border: 0 none; + padding: 4px; + width: 30px; + text-align: center; +} + +#github-login-wrapper { + text-align: center; + color: white; + flex-direction: row; + width: 100%; + height: 200px; +} diff --git a/examples/monaco-graphql-webpack/tsconfig.json b/examples/monaco-graphql-webpack/tsconfig.json new file mode 100644 index 00000000000..a125eadcf94 --- /dev/null +++ b/examples/monaco-graphql-webpack/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "rootDir": "./src", + "outDir": "./dist", + "composite": true, + "baseUrl": ".", + "strictPropertyInitialization": false, + "types": ["node", "jest"], + "typeRoots": ["../../node_modules/@types", "node_modules/@types"], + "lib": ["dom", "ESNext"], + "moduleResolution": "node" + }, + "references": [{ "path": "../../packages/monaco-graphql" }], + "include": ["src"], + "exclude": ["**/__tests__/**", "**/build/**.*", "../../node_modules"] +} diff --git a/examples/monaco-graphql-webpack/webpack.config.js b/examples/monaco-graphql-webpack/webpack.config.js new file mode 100644 index 00000000000..577f2b1f5c2 --- /dev/null +++ b/examples/monaco-graphql-webpack/webpack.config.js @@ -0,0 +1,87 @@ +const path = require('node:path'); + +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); +const isDev = process.env.NODE_ENV === 'development'; + +const relPath = (...args) => path.resolve(__dirname, ...args); +const rootPath = (...args) => relPath(...args); + +const resultConfig = { + mode: process.env.NODE_ENV, + entry: './index.ts', + context: rootPath('src'), + output: { + path: rootPath('dist'), + filename: '[name].js', + }, + module: { + rules: [ + // you can also use ts-loader of course + // i prefer to use babel-loader & @babel/plugin-typescript + // so we can experiment with how changing browserslistrc targets impacts + // monaco-graphql bundling + { + test: /\.(js|jsx|ts|tsx)$/, + use: [{ loader: 'babel-loader' }], + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.svg$/, + use: [{ loader: 'svg-inline-loader' }], + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + type: 'asset/resource', + }, + ], + }, + resolve: { + extensions: ['.ts', '.js'], + }, + plugins: [ + // in order to prevent async modules for CDN builds + // until we can guarantee it will work with the CDN properly + // and so that graphiql.min.js can retain parity + new HtmlWebpackPlugin({ + template: relPath('src/index.html.ejs'), + filename: 'index.html', + }), + // critical! make sure that webpack can consume the exported modules and types + new ForkTsCheckerWebpackPlugin({ + async: isDev, + typescript: { configFile: rootPath('tsconfig.json') }, + }), + + new MonacoWebpackPlugin({ + languages: ['json', 'graphql'], + publicPath: '/', + customLanguages: [ + { + label: 'graphql', + worker: { + id: 'graphql', + entry: require.resolve('monaco-graphql/esm/graphql.worker.js'), + }, + }, + ], + }), + ], +}; + +if (process.env.ANALYZE) { + resultConfig.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + openAnalyzer: false, + reportFilename: rootPath('build/analyzer.html'), + }), + ); +} + +module.exports = resultConfig; diff --git a/functions/schema-demo.js b/functions/schema-demo.js new file mode 100644 index 00000000000..87455852796 --- /dev/null +++ b/functions/schema-demo.js @@ -0,0 +1,50 @@ +/* example using https://github.com/awslabs/aws-serverless-express */ +const express = require('express'); +// eslint-disable-next-line import/no-extraneous-dependencies +const { createHandler } = require('graphql-http/lib/use/express'); +const awsServerlessExpress = require('aws-serverless-express'); +const schema = require('../packages/graphiql/test/schema'); +const cors = require('cors'); + +const binaryMimeTypes = [ + 'application/javascript', + 'application/json', + 'font/eot', + 'font/opentype', + 'font/otf', + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'text/css', + 'text/html', + 'text/javascript', + 'multipart/mixed', +]; + +const app = express(); + +app.use(cors({ origin: '*' })); + +// Requests to /graphql redirect to / +app.all('/graphql', (req, res) => res.redirect('/')); + +// Finally, serve up the GraphQL Schema itself +app.use( + '/', + createHandler(() => ({ schema })), +); + +const server = awsServerlessExpress.createServer(app, null, binaryMimeTypes); + +exports.handler = (event, context) => + awsServerlessExpress.proxy(server, event, context); + +// // Server +// app.post('/graphql', graphqlHTTP({ schema })); + +// app.get('/graphql', graphQLMiddleware); +// // Export Lambda handler + +// exports.handler = serverless(app, { +// httpMethod: 'POST' +// }) diff --git a/jest.config.base.js b/jest.config.base.js new file mode 100644 index 00000000000..8765a91bad9 --- /dev/null +++ b/jest.config.base.js @@ -0,0 +1,56 @@ +const path = require('node:path'); + +module.exports = (dir, env = 'jsdom') => { + const package = require(`${dir}/package.json`); + const setupFilesAfterEnv = []; + if (env === 'jsdom') { + setupFilesAfterEnv.push(path.join(__dirname, '/resources/test.config.js')); + } + return { + globals: { + 'ts-jest': { + tsConfig: `${__dirname}/resources/tsconfig.base.esm.json`, + }, + }, + clearMocks: true, + collectCoverage: true, + coverageDirectory: `${__dirname}/coverage/jest`, + setupFilesAfterEnv, + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + 'identity-obj-proxy', + '\\.(css|less)$': 'identity-obj-proxy', + '^graphql-language-service-([^/]+)': `${__dirname}/packages/graphql-language-service/src/$1`, + '^graphql-language-([^/]+)': `${__dirname}/packages/graphql-language-$1/src`, + '^@graphiql\\/([^/]+)': `${__dirname}/packages/graphiql-$1/src`, + '^@graphiql-plugins\\/([^/]+)': `${__dirname}/plugins/$1/src`, + '^codemirror-graphql\\/esm([^]+)': `${__dirname}/packages/codemirror-graphql/src/$1`, + '^codemirror-graphql\\/cjs([^]+)': `${__dirname}/packages/codemirror-graphql/src/$1`, + // relies on compilation + '^cm6-graphql\\/src\\/([^]+)': `${__dirname}/packages/cm6-graphql/dist/$1`, + '^example-([^/]+)': `${__dirname}/examples/$1/src`, + '^-!svg-react-loader.*$': '/resources/jest/svgImportMock.js', + }, + testMatch: ['**/*[-.](spec|test).[jt]s?(x)', '!**/cypress/**'], + testEnvironment: env, + testPathIgnorePatterns: ['node_modules', 'dist', 'cypress'], + collectCoverageFrom: ['**/src/**/*.{js,jsx,ts,tsx}'], + coveragePathIgnorePatterns: [ + 'dist', + 'esm', + 'node_modules', + '__tests__', + 'resources', + 'test', + 'examples', + '.d.ts', + 'types.ts', + ], + + roots: [''], + + rootDir: dir, + name: package.name, + displayName: package.name, + }; +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000000..3ef34f68be1 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + projects: ['/packages/*/jest.config.js'], +}; diff --git a/js-green-licenses.json b/js-green-licenses.json new file mode 100644 index 00000000000..562c1c2f883 --- /dev/null +++ b/js-green-licenses.json @@ -0,0 +1,7 @@ +{ + "packageAllowlist": [ + // MIT, just lacking SPDX in manifest + "valid-url", + "argparse" + ] +} diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 00000000000..e1fa1e1ef5d --- /dev/null +++ b/netlify.toml @@ -0,0 +1,8 @@ +# Settings in the [build] context are global and are applied to +# all contexts unless otherwise overridden by more specific contexts. +[build] + publish = "packages/graphiql" + + # Default build command. + command = "yarn build && yarn build-bundles && yarn build-docs && yarn build-demo" + environment = { YARN_FLAGS = "--frozen-lockfile --immutable"} diff --git a/package.json b/package.json index 2a6161a60a3..ca9c1a73465 100644 --- a/package.json +++ b/package.json @@ -1,85 +1,146 @@ { - "name": "graphiql", - "version": "0.8.1", - "description": "An graphical interactive in-browser GraphQL IDE.", - "contributors": [ - "Hyohyeon Jeong ", - "Lee Byron (http://leebyron.com/)" - ], - "homepage": "https://github.com/graphql/graphiql", - "bugs": { - "url": "https://github.com/graphql/graphiql/issues" + "name": "graphiql-monorepo", + "version": "0.0.0", + "private": true, + "license": "MIT", + "workspaces": { + "packages": [ + "packages/*", + "examples/monaco-graphql-webpack", + "examples/monaco-graphql-nextjs", + "examples/monaco-graphql-react-vite", + "examples/graphiql-webpack" + ] }, - "repository": { - "type": "git", - "url": "http://github.com/graphql/graphiql.git" + "lint-staged": { + "*.{js,ts,jsx,tsx}": [ + "eslint --fix", + "prettier --write --ignore-path .eslintignore", + "jest --passWithNoTests", + "yarn lint-cspell" + ], + "*.{md,html,json,css}": [ + "prettier --write --ignore-path .eslintignore", + "yarn lint-cspell" + ] }, - "license": "SEE LICENSE IN LICENSE", - "main": "dist/index.js", - "files": [ - "dist", - "graphiql.js", - "graphiql.min.js", - "graphiql.css", - "README.md", - "LICENSE" - ], - "browserify-shim": { - "react": "global:React", - "react-dom": "global:ReactDOM" + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } }, - "options": { - "mocha": "--full-trace --require resources/mocha-bootload src/**/*-test.js" + "engines": { + "npm": "please_use_yarn_instead" }, "scripts": { - "build": ". ./resources/build.sh", - "check": "flow check", - "dev": "babel-node test/server.js", - "lint": "eslint src", - "prepublish": ". ./resources/prepublish.sh", - "preversion": ". ./resources/checkgit.sh && npm test", - "test": "npm run lint && npm run check && npm run testonly", - "testonly": "babel-node ./node_modules/.bin/_mocha $npm_package_options_mocha" - }, - "dependencies": { - "codemirror": "^5.15.2", - "codemirror-graphql": "^0.5.9", - "marked": "^0.3.5" - }, - "peerDependencies": { - "graphql": "^0.6.0 || ^0.7.0 || ^0.8.0-b", - "react": ">=0.14.8", - "react-dom": ">=0.14.8" + "build": "yarn build:clean && yarn build:cm6-graphql && yarn build:packages && yarn build:graphiql-react && yarn build:graphiql-plugin-explorer && yarn build:graphiql-plugin-code-exporter && yarn build:graphiql", + "build-bundles": "yarn prebuild-bundles && wsrun -p -m -s build-bundles", + "build-bundles-clean": "rimraf '{packages,examples,plugins}/**/{bundle,cdn,webpack}' && yarn workspace graphiql build-bundles-clean", + "build-clean": "wsrun -m build-clean", + "build-docs": "rimraf packages/graphiql/typedoc && typedoc packages", + "build:clean": "yarn tsc --clean && yarn tsc --clean resources/tsconfig.graphiql.json", + "build:cm6-graphql": "yarn workspace cm6-graphql build", + "build:graphiql": "yarn tsc resources/tsconfig.graphiql.json", + "build:graphiql-plugin-explorer": "yarn workspace @graphiql/plugin-explorer build", + "build:graphiql-plugin-code-exporter": "yarn workspace @graphiql/plugin-code-exporter build", + "build:graphiql-react": "yarn workspace @graphiql/react build", + "build:packages": "yarn tsc", + "build:watch": "yarn tsc --watch", + "build-demo": "wsrun -m build-demo", + "watch": "yarn build:watch", + "watch-vscode": "yarn workspace vscode-graphql compile", + "watch-vscode-exec": "yarn workspace vscode-graphql-execution compile", + "check": "yarn tsc --noEmit", + "cypress-open": "yarn workspace graphiql cypress-open", + "dev-graphiql": "yarn workspace graphiql dev", + "e2e": "yarn run e2e:build && yarn workspace graphiql e2e", + "e2e:build": "yarn build && yarn workspace graphiql build-bundles", + "eslint": "NODE_OPTIONS=--max-old-space-size=4096 eslint --max-warnings=0 --ignore-path .gitignore --cache .", + "format": "yarn eslint --fix && yarn pretty", + "jest": "jest --testPathIgnorePatterns cm6-graphql", + "license-check": "jsgl --local ./", + "lint": "yarn eslint && yarn pretty-check && yarn lint-cspell", + "lint-cspell": "cspell --unique --no-progress --no-must-find-files", + "lint-fix": "yarn eslint --fix", + "postbuild": "yarn workspace codemirror-graphql postbuild && yarn workspace monaco-graphql postbuild", + "prebuild-bundles": "yarn build-bundles-clean", + "prepublishOnly": "./scripts/prepublish.sh", + "pretty": "yarn pretty-check --write", + "pretty-check": "prettier --cache --check --ignore-path .gitignore --ignore-path .eslintignore .", + "ci:version": "yarn changeset version && yarn build && yarn format", + "release": "yarn build && yarn build-bundles && (wsrun release --exclude-missing --serial --recursive --changedSince main -- || true) && yarn changeset publish", + "release:canary": "(node scripts/canary-release.js && yarn build && yarn build-bundles && yarn changeset publish --tag canary) || echo Skipping Canary...", + "repo:lint": "manypkg check", + "repo:fix": "manypkg fix", + "repo:resolve": "node scripts/set-resolution.js", + "start-graphiql": "yarn workspace graphiql dev", + "start-monaco": "yarn workspace example-monaco-graphql-webpack start", + "t": "yarn test", + "test": "yarn jest", + "test:ci": "yarn build && jest --coverage && yarn workspace monaco-graphql test", + "test:coverage": "yarn jest --coverage", + "test:watch": "yarn jest --watch", + "tsc": "tsc --build" }, "devDependencies": { - "autoprefixer": "^6.5.4", - "babel-cli": "6.18.0", - "babel-eslint": "7.1.1", - "babel-plugin-syntax-async-functions": "6.13.0", - "babel-plugin-transform-class-properties": "6.19.0", - "babel-plugin-transform-object-rest-spread": "^6.20.2", - "babel-preset-es2015": "6.18.0", - "babel-preset-react": "6.16.0", - "babelify": "7.3.0", - "browserify": "13.1.1", - "browserify-shim": "3.8.12", - "chai": "3.5.0", - "chai-subset": "1.4.0", - "eslint": "^3.12.0", - "eslint-plugin-babel": "4.0.0", - "eslint-plugin-react": "6.8.0", - "express": "4.14.0", - "express-graphql": "0.6.1", - "flow-bin": "0.37.4", - "graphql": "0.8.2", - "jsdom": "9.9.1", - "mocha": "3.2.0", - "postcss-cli": "^2.6.0", - "react": "15.4.1", - "react-dom": "15.4.1", - "react-test-renderer": "15.4.1", - "uglify-js": "2.7.5", - "uglifyify": "3.0.4", - "watchify": "3.8.0" + "@arthurgeron/eslint-plugin-react-usememo": "^1.1.4", + "@babel/cli": "^7.21.0", + "@babel/core": "^7.21.0", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/polyfill": "^7.12.1", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@babel/register": "^7.21.0", + "@changesets/changelog-github": "^0.4.7", + "@changesets/cli": "^2.25.2", + "@manypkg/get-packages": "^1.1.3", + "@shopify/eslint-plugin": "^42.1.0", + "@strictsoftware/typedoc-plugin-monorepo": "^0.3.1", + "@testing-library/jest-dom": "5.16.5", + "@types/aws-serverless-express": "^3.3.3", + "@types/codemirror": "^0.0.90", + "@types/express": "^4.17.11", + "@types/fetch-mock": "^7.3.2", + "@types/jest": "^29.5.2", + "@types/node": "^16.18.4", + "@types/ws": "^7.4.0", + "@typescript-eslint/eslint-plugin": "6.0.0-alpha.159", + "@typescript-eslint/parser": "6.0.0-alpha.159", + "aws-serverless-express": "^3.4.0", + "babel-jest": "^29.4.3", + "concurrently": "^7.0.0", + "copy": "^0.3.2", + "cors": "^2.8.5", + "cspell": "^5.15.2", + "eslint": "^8.43.0", + "eslint-config-prettier": "^8.8.0", + "eslint-import-resolver-typescript": "^3.5.5", + "eslint-plugin-cypress": "^2.13.3", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.2.2", + "eslint-plugin-mdx": "^2.1.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-sonar": "^0.12.0", + "eslint-plugin-sonarjs": "^0.19.0", + "eslint-plugin-unicorn": "^47.0.0", + "execa": "^6.0.0", + "express": "^4.18.2", + "fetch-mock": "6.5.2", + "husky": "^4.2.3", + "jest": "^27.5.1", + "js-green-licenses": "3.0.0", + "lint-staged": "^10.1.2", + "mkdirp": "^1.0.4", + "prettier": "3.0.0-alpha.12", + "rimraf": "^3.0.2", + "ts-jest": "^27.1.5", + "typedoc": "^0.19.2", + "typescript": "^4.6.3", + "vitest": "^0.32.2", + "wsrun": "^5.2.4" } } diff --git a/packages/cm6-graphql/.gitignore b/packages/cm6-graphql/.gitignore new file mode 100644 index 00000000000..610ddf870c9 --- /dev/null +++ b/packages/cm6-graphql/.gitignore @@ -0,0 +1,6 @@ +/node_modules +package-lock.json +/dist +/src/*.js +/src/*.d.ts +!syntax.grammar.d.ts diff --git a/packages/cm6-graphql/.npmignore b/packages/cm6-graphql/.npmignore new file mode 100644 index 00000000000..9bd97602d94 --- /dev/null +++ b/packages/cm6-graphql/.npmignore @@ -0,0 +1,5 @@ +/src +/test +/node_modules +rollup.config.js +tsconfig.json diff --git a/packages/cm6-graphql/CHANGELOG.md b/packages/cm6-graphql/CHANGELOG.md new file mode 100644 index 00000000000..80aae074266 --- /dev/null +++ b/packages/cm6-graphql/CHANGELOG.md @@ -0,0 +1,89 @@ +# cm6-graphql + +## 0.0.9 + +### Patch Changes + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5)]: + - graphql-language-service@5.1.7 + +## 0.0.9-alpha.0 + +### Patch Changes + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5)]: + - graphql-language-service@5.1.7-alpha.0 + +## 0.0.8 + +### Patch Changes + +- [#3118](https://github.com/graphql/graphiql/pull/3118) [`431b7fe1`](https://github.com/graphql/graphiql/commit/431b7fe1efefa4867f0ea617adc436b1117052e8) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer `.textContent` over `.innerText` + +## 0.0.7 + +### Patch Changes + +- Updated dependencies [[`06007498`](https://github.com/graphql/graphiql/commit/06007498880528ed75dd4d705dcbcd7c9e775939)]: + - graphql-language-service@5.1.6 + +## 0.0.6 + +### Patch Changes + +- Updated dependencies [[`4d33b221`](https://github.com/graphql/graphiql/commit/4d33b2214e941f171385a1b72a1fa995714bb284)]: + - graphql-language-service@5.1.5 + +## 0.0.5 + +### Patch Changes + +- [#3127](https://github.com/graphql/graphiql/pull/3127) [`0d2bb2bc`](https://github.com/graphql/graphiql/commit/0d2bb2bcc6522e156e2d70f3be553bd4b60c8ee1) Thanks [@imolorhe](https://github.com/imolorhe)! - Updated cm6-graphql package README + +- Updated dependencies [[`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9)]: + - graphql-language-service@5.1.4 + +## 0.0.4 + +### Patch Changes + +- [#3075](https://github.com/graphql/graphiql/pull/3075) [`9c1a02db`](https://github.com/graphql/graphiql/commit/9c1a02dbff4a39fe999873912daec7dcd1d39b5c) Thanks [@acao](https://github.com/acao)! - another manual release attempt to trigger versioning + +- [#3074](https://github.com/graphql/graphiql/pull/3074) [`7cb2a2f1`](https://github.com/graphql/graphiql/commit/7cb2a2f156d918fd57b7d3757ee1ecc0f4dab4ce) Thanks [@acao](https://github.com/acao)! - Fix release bug, trigger changeset release action + +- [#3069](https://github.com/graphql/graphiql/pull/3069) [`d922e930`](https://github.com/graphql/graphiql/commit/d922e930f77dff879212ad39191ad6a1b8f7dd8a) Thanks [@sergeichestakov](https://github.com/sergeichestakov)! - Added graphql-language-service as a direct dep of cm6-graphql and update peer dependencies + +- Updated dependencies [[`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d), [`881a2024`](https://github.com/graphql/graphiql/commit/881a202497d5a58eb5260a5aa54c0c88930d69a0)]: + - graphql-language-service@5.1.3 + +## 0.0.3 + +### Patch Changes + +- [#2995](https://github.com/graphql/graphiql/pull/2995) [`5f276c41`](https://github.com/graphql/graphiql/commit/5f276c415ad93350382fec873025ffecc9a29d9d) Thanks [@imolorhe](https://github.com/imolorhe)! - fix(cm6-graphql): Fix query token used as field name + +- [#2962](https://github.com/graphql/graphiql/pull/2962) [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d) Thanks [@B2o5T](https://github.com/B2o5T)! - clean all ESLint warnings, add `--max-warnings=0` and `--cache` flags + +- [#2940](https://github.com/graphql/graphiql/pull/2940) [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-node-protocol` rule + +## 0.0.2 + +### Patch Changes + +- [#2931](https://github.com/graphql/graphiql/pull/2931) [`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-negated-condition` and `no-else-return` rules + +- [#2922](https://github.com/graphql/graphiql/pull/2922) [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b) Thanks [@B2o5T](https://github.com/B2o5T)! - extends `plugin:import/recommended` and fix warnings + +- [#2992](https://github.com/graphql/graphiql/pull/2992) [`cc245246`](https://github.com/graphql/graphiql/commit/cc2452467688f3cdcd7a196dddf47e3b81367d62) Thanks [@acao](https://github.com/acao)! - fix tsconfig reference, new netlify deploy + +## 0.0.1 + +### Patch Changes + +- [#2867](https://github.com/graphql/graphiql/pull/2867) [`9fd12838`](https://github.com/graphql/graphiql/commit/9fd128381a86220a7c658f21d72baa8eea45a8af) Thanks [@imolorhe](https://github.com/imolorhe)! - fix: fixed "Mark decorations may not be empty" error + +## 0.0.0 + +### Patch Changes + +- [#2852](https://github.com/graphql/graphiql/pull/2852) [`20869583`](https://github.com/graphql/graphiql/commit/20869583eff563f5d6494e93302a835f0e034f4b) Thanks [@acao](https://github.com/acao)! - First release of a modern codemirror 6 mode for graphql by @imolorhe! diff --git a/packages/cm6-graphql/LICENSE b/packages/cm6-graphql/LICENSE new file mode 100644 index 00000000000..3541ee754ca --- /dev/null +++ b/packages/cm6-graphql/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) 2021 by GraphQL Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/cm6-graphql/README.md b/packages/cm6-graphql/README.md new file mode 100644 index 00000000000..a23f718dbff --- /dev/null +++ b/packages/cm6-graphql/README.md @@ -0,0 +1,52 @@ +# CodeMirror 6 GraphQL Language extension + +[![NPM](https://img.shields.io/npm/v/cm6-graphql.svg?style=flat-square)](https://npmjs.com/cm6-graphql) +![npm downloads](https://img.shields.io/npm/dm/cm6-graphql?label=npm%20downloads) +[![License](https://img.shields.io/npm/l/cm6-graphql.svg?style=flat-square)](LICENSE) +[Discord Channel](https://discord.gg/cffZwk8NJW) + +Provides CodeMirror 6 extension with a parser mode for GraphQL along with +autocomplete and linting powered by your GraphQL Schema. + +### Getting Started + +```sh +npm install --save cm6-graphql +``` + +[CodeMirror 6](https://codemirror.net/) customization is done through +[extensions](https://codemirror.net/docs/guide/#extension). This package is +an extension that customizes CodeMirror 6 for GraphQL. + +```js +import { basicSetup, EditorView } from 'codemirror'; +import { graphql } from 'cm6-graphql'; + +const view = new EditorView({ + doc: `mutation mutationName { + setString(value: "newString") + }`, + extensions: [basicSetup, graphql(myGraphQLSchema)], + parent: document.body, +}); +``` + +_**Note:** You have to provide a theme to CodeMirror 6 for the styling you want. You +can take a look at +[this example](https://github.com/graphql/graphiql/blob/main/examples/cm6-graphql-parcel/src/index.ts) +or see the CodeMirror 6 +[documentation examples](https://codemirror.net/examples/styling/) for more +details._ + +### Updating schema + +If you need to dynamically update the GraphQL schema used in the editor, you can +call `updateSchema` with the CodeMirror `EditorView` instance and the new schema + +```js +import { updateSchema } from 'cm6-graphql'; + +const onNewSchema = schema => { + updateSchema(view, schema); +}; +``` diff --git a/packages/cm6-graphql/__tests__/cases.txt b/packages/cm6-graphql/__tests__/cases.txt new file mode 100644 index 00000000000..c6a8db77228 --- /dev/null +++ b/packages/cm6-graphql/__tests__/cases.txt @@ -0,0 +1,115 @@ +# Simple query + +{ hello } + +==> + +Document(OperationDefinition(SelectionSet("{",Selection(Field(FieldName)),"}"))) + +# Named query + +query a { hello } + +==> + +Document(OperationDefinition(OperationType,Name,SelectionSet("{",Selection(Field(FieldName)),"}"))) + +# Nested query + +query { + node { + id + } +} + +==> + +Document(OperationDefinition(OperationType,SelectionSet("{",Selection(Field(FieldName,SelectionSet("{",Selection(Field(FieldName)),"}"))),"}"))) + +# Query with argument + +{ + node(id: 4) { + id, + name + } +} + +==> + +Document( + OperationDefinition( + SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,IntValue),")"),SelectionSet("{",Selection(Field(FieldName)),Selection(Field(FieldName)),"}"))),"}") + ) +) + +# Multiple query nesting + +{ + categories(id: "1") { + name + products { + name + vendor { + products { + name + } + } + } + } +} + +==> + +Document(OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,StringValue),")"),SelectionSet("{",Selection(Field(FieldName)),Selection(Field(FieldName,SelectionSet("{",Selection(Field(FieldName)),Selection(Field(FieldName,SelectionSet("{",Selection(Field(FieldName,SelectionSet("{",Selection(Field(FieldName)),"}"))),"}"))),"}"))),"}"))),"}"))) + +# Query of fields with arguments + +{ + vendors(productname: "Coconut") { + name + } + products(price:9.99) { + id + } + categories(id: ALL) { + parent + } +} + +==> + +Document(OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,StringValue),")"),SelectionSet("{",Selection(Field(FieldName)),"}"))),Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,FloatValue),")"),SelectionSet("{",Selection(Field(FieldName)),"}"))),Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,EnumValue),")"),SelectionSet("{",Selection(Field(FieldName)),"}"))),"}"))) + +# Multiple named queries + +query a { hello } +query b { bye } +mutation c($val: String!) { + addAnother(value: $val) { + name + } +} + +==> + +Document( + OperationDefinition(OperationType,Name,SelectionSet("{",Selection(Field(FieldName)),"}")), + OperationDefinition(OperationType,Name,SelectionSet("{",Selection(Field(FieldName)),"}")), + OperationDefinition(OperationType,Name,VariableDefinitions("(",VariableDefinition(Variable,NonNullType(NamedType(Name))),")"),SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,Variable),")"),SelectionSet("{",Selection(Field(FieldName)),"}"))),"}")) +) + +# Query with a `query` field + +{ + other + query { + inner + } +} + +==> + +Document( + OperationDefinition(SelectionSet("{",Selection(Field(FieldName)),Selection(Field(FieldName,SelectionSet("{",Selection(Field(FieldName)),"}"))),"}")) +) diff --git a/packages/cm6-graphql/__tests__/test.spec.ts b/packages/cm6-graphql/__tests__/test.spec.ts new file mode 100644 index 00000000000..a16d1ce446b --- /dev/null +++ b/packages/cm6-graphql/__tests__/test.spec.ts @@ -0,0 +1,34 @@ +/* eslint-disable jest/expect-expect */ +import { graphqlLanguage } from '../dist/index.js'; +import { fileTests } from '@lezer/generator/dist/test'; + +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +// because of the babel transformations, __dirname is the package root (cm6-graphql) +const caseDir = path.resolve(path.dirname(__dirname), '__tests__'); + +describe('codemirror 6 language', () => { + for (const file of fs.readdirSync(caseDir)) { + if (!/\.txt$/.test(file)) { + continue; + } + + const describeName = /^[^.]*/.exec(file)![0]; + describe(`${describeName}`, () => { + for (const { name, run } of fileTests( + fs.readFileSync(path.join(caseDir, file), 'utf8'), + file, + )) { + it(`${name}`, () => { + try { + run(graphqlLanguage.parser); + } catch (err) { + require('node:console').log(name, err); + throw err; + } + }); + } + }); + } +}); diff --git a/packages/cm6-graphql/__tests__/types.txt b/packages/cm6-graphql/__tests__/types.txt new file mode 100644 index 00000000000..b9d07d74075 --- /dev/null +++ b/packages/cm6-graphql/__tests__/types.txt @@ -0,0 +1,43 @@ +# String + +{ test(v1: "abc") } + +==> + +Document(OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,StringValue),")"))),"}"))) + +# Enum + +{ test(v1: ABC) } + +==> + +Document(OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,EnumValue),")"))),"}"))) + +# Numbers + +{ test(v1: 123) } +{ test(v1: 123.01) } +{ test(v1: -1.35384e+3) } + +==> + +Document( + OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,IntValue),")"))),"}")), + OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,FloatValue),")"))),"}")), + OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,FloatValue),")"))),"}")) +) + +# List + +{ test(v1: []) } +{ test(v1: ["abc", "def"]) } +{ test(v1: ["abc", ABC, 123, 213.01, true, null]) } + +==> + +Document( + OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,ListValue("[","]")),")"))),"}")), + OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,ListValue("[",StringValue,StringValue,"]")),")"))),"}")), + OperationDefinition(SelectionSet("{",Selection(Field(FieldName,Arguments("(",Argument(ArgumentAttributeName,ListValue("[",StringValue,EnumValue,IntValue,FloatValue,BooleanValue,NullValue,"]")),")"))),"}")) +) diff --git a/packages/cm6-graphql/jest.config.js b/packages/cm6-graphql/jest.config.js new file mode 100644 index 00000000000..2e9d6c82a6f --- /dev/null +++ b/packages/cm6-graphql/jest.config.js @@ -0,0 +1,9 @@ +const base = require('../../jest.config.base')(__dirname); + +// remove the ignore line for cm6-graphql +base.testPathIgnorePatterns.pop(); + +module.exports = { + ...base, + transformIgnorePatterns: ['/node_modules/(?!@lezer)'], +}; diff --git a/packages/cm6-graphql/package.json b/packages/cm6-graphql/package.json new file mode 100644 index 00000000000..87cb0df8683 --- /dev/null +++ b/packages/cm6-graphql/package.json @@ -0,0 +1,49 @@ +{ + "name": "cm6-graphql", + "version": "0.0.9", + "description": "GraphQL language support for CodeMirror 6", + "scripts": { + "build": "cm-buildhelper src/index.ts", + "prepare": "yarn build" + }, + "main": "dist/index.cjs", + "module": "dist/index.js", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "types": "dist/index.d.ts", + "sideEffects": false, + "dependencies": { + "graphql-language-service": "^5.1.7" + }, + "devDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/buildhelper": "^0.1.16", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.1.0", + "@codemirror/view": "^6.1.2", + "@lezer/common": "^1.0.0", + "@lezer/generator": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0", + "esbuild": "0.18.10", + "graphql": "^16.5.0", + "rollup": "^2.60.2", + "rollup-plugin-dts": "^4.0.1", + "rollup-plugin-esbuild": "^4.9.1", + "rollup-plugin-ts": "^2.0.4", + "typescript": "^4.3.4" + }, + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.1.0", + "@codemirror/view": "^6.1.2", + "@lezer/highlight": "^1.0.0", + "graphql": "^16.5.0" + }, + "license": "MIT" +} diff --git a/packages/cm6-graphql/src/commands.ts b/packages/cm6-graphql/src/commands.ts new file mode 100644 index 00000000000..d78c93e9317 --- /dev/null +++ b/packages/cm6-graphql/src/commands.ts @@ -0,0 +1,40 @@ +import { EditorView } from '@codemirror/view'; +import { getTokenAtPosition, getTypeInfo } from 'graphql-language-service'; +import { offsetToPos } from './helpers'; +import { getOpts, getSchema } from './state'; + +export const fillAllFieldsCommands = (view: EditorView) => { + const schema = getSchema(view.state); + if (!schema) { + return true; + } + const opts = getOpts(view.state); + const currentPosition = view.state.selection.main.head; + const pos = offsetToPos(view.state.doc, currentPosition); + const token = getTokenAtPosition(view.state.doc.toString(), pos); + + if (schema && opts?.onFillAllFields) { + opts.onFillAllFields(view, schema, view.state.doc.toString(), pos, token); + } + + return true; +}; +export const showInDocsCommand = (view: EditorView) => { + const schema = getSchema(view.state); + if (!schema) { + return true; + } + const opts = getOpts(view.state); + const currentPosition = view.state.selection.main.head; + const pos = offsetToPos(view.state.doc, currentPosition); + const token = getTokenAtPosition(view.state.doc.toString(), pos); + if (schema && opts?.onShowInDocs) { + const tInfo = getTypeInfo(schema, token.state); + opts.onShowInDocs( + tInfo.fieldDef?.name, + tInfo.type?.toString(), + tInfo.parentType?.toString(), + ); + } + return true; +}; diff --git a/packages/cm6-graphql/src/completions.ts b/packages/cm6-graphql/src/completions.ts new file mode 100644 index 00000000000..27131cf6975 --- /dev/null +++ b/packages/cm6-graphql/src/completions.ts @@ -0,0 +1,59 @@ +import { Completion, CompletionContext } from '@codemirror/autocomplete'; +import { getAutocompleteSuggestions } from 'graphql-language-service'; +import { getOpts, getSchema } from './state'; +import { offsetToPos } from './helpers'; +import { graphqlLanguage } from './language'; + +const AUTOCOMPLETE_CHARS = /^[a-zA-Z0-9_@(]$/; + +export const completion = graphqlLanguage.data.of({ + autocomplete(ctx: CompletionContext) { + const schema = getSchema(ctx.state); + const opts = getOpts(ctx.state); + if (!schema) { + return null; + } + + const word = ctx.matchBefore(/\w*/); + + if (!word) { + return null; + } + + const lastWordChar = word.text.split('').pop()!; + if (!AUTOCOMPLETE_CHARS.test(lastWordChar) && !ctx.explicit) { + return null; + } + const val = ctx.state.doc.toString(); + const pos = offsetToPos(ctx.state.doc, ctx.pos); + const results = getAutocompleteSuggestions(schema, val, pos); + + if (results.length === 0) { + return null; + } + + return { + from: word.from, + options: results.map(item => { + return { + label: item.label, + detail: item.detail || '', + info(completionData: Completion) { + if (opts?.onCompletionInfoRender) { + return opts.onCompletionInfoRender(item, ctx, completionData); + } + if ( + item.documentation || + (item.isDeprecated && item.deprecationReason) + ) { + const el = document.createElement('div'); + el.textContent = + item.documentation || item.deprecationReason || ''; + return el; + } + }, + }; + }), + }; + }, +}); diff --git a/packages/cm6-graphql/src/graphql.ts b/packages/cm6-graphql/src/graphql.ts new file mode 100644 index 00000000000..3b0cec36781 --- /dev/null +++ b/packages/cm6-graphql/src/graphql.ts @@ -0,0 +1,21 @@ +import { Extension } from '@codemirror/state'; +import { GraphQLSchema } from 'graphql'; +import { completion } from './completions'; +import { GqlExtensionsOptions } from './interfaces'; +import { jump } from './jump'; +import { graphqlLanguageSupport } from './language'; +import { lint } from './lint'; +import { stateExtensions } from './state'; + +export function graphql( + schema?: GraphQLSchema, + opts?: GqlExtensionsOptions, +): Extension[] { + return [ + graphqlLanguageSupport(), + completion, + lint, + jump, + stateExtensions(schema, opts), + ]; +} diff --git a/packages/cm6-graphql/src/helpers.ts b/packages/cm6-graphql/src/helpers.ts new file mode 100644 index 00000000000..6248db87f96 --- /dev/null +++ b/packages/cm6-graphql/src/helpers.ts @@ -0,0 +1,43 @@ +import { Text } from '@codemirror/state'; + +export function posToOffset(doc: Text, pos: IPosition) { + return doc.line(pos.line + 1).from + pos.character; +} +export function offsetToPos(doc: Text, offset: number): Position { + const line = doc.lineAt(offset); + return new Position(line.number - 1, offset - line.from); +} + +export interface IPosition { + line: number; + character: number; + setLine(line: number): void; + setCharacter(character: number): void; + lessThanOrEqualTo(position: IPosition): boolean; +} + +export class Position implements IPosition { + constructor( + public line: number, + public character: number, + ) {} + + setLine(line: number) { + this.line = line; + } + + setCharacter(character: number) { + this.character = character; + } + + lessThanOrEqualTo(position: IPosition) { + return ( + this.line < position.line || + (this.line === position.line && this.character <= position.character) + ); + } +} + +const isMac = () => /mac/i.test(navigator.platform); +export const isMetaKeyPressed = (e: MouseEvent) => + isMac() ? e.metaKey : e.ctrlKey; diff --git a/packages/cm6-graphql/src/index.ts b/packages/cm6-graphql/src/index.ts new file mode 100644 index 00000000000..993ad7f3a92 --- /dev/null +++ b/packages/cm6-graphql/src/index.ts @@ -0,0 +1,8 @@ +export * from './commands'; +export * from './completions'; +export * from './graphql'; +export * from './helpers'; +export * from './jump'; +export * from './language'; +export * from './lint'; +export * from './state'; diff --git a/packages/cm6-graphql/src/interfaces.ts b/packages/cm6-graphql/src/interfaces.ts new file mode 100644 index 00000000000..0bf59cf6936 --- /dev/null +++ b/packages/cm6-graphql/src/interfaces.ts @@ -0,0 +1,20 @@ +import { Completion, CompletionContext } from '@codemirror/autocomplete'; +import { EditorView } from '@codemirror/view'; +import { GraphQLSchema } from 'graphql'; +import { ContextToken, CompletionItem } from 'graphql-language-service'; +import { Position } from './helpers'; +export interface GqlExtensionsOptions { + onShowInDocs?: (field?: string, type?: string, parentType?: string) => void; + onFillAllFields?: ( + view: EditorView, + schema: GraphQLSchema, + query: string, + cursor: Position, + token: ContextToken, + ) => void; + onCompletionInfoRender?: ( + gqlCompletionItem: CompletionItem, + ctx: CompletionContext, + item: Completion, + ) => Node | Promise | null; +} diff --git a/packages/cm6-graphql/src/jump.ts b/packages/cm6-graphql/src/jump.ts new file mode 100644 index 00000000000..159159a0a94 --- /dev/null +++ b/packages/cm6-graphql/src/jump.ts @@ -0,0 +1,28 @@ +import { EditorView } from '@codemirror/view'; +import { getTokenAtPosition, getTypeInfo } from 'graphql-language-service'; +import { isMetaKeyPressed, offsetToPos } from './helpers'; +import { getOpts, getSchema } from './state'; + +export const jump = EditorView.domEventHandlers({ + click(evt, view) { + const schema = getSchema(view.state); + if (!schema) { + return; + } + // TODO: Set class on cm-editor when mod key is pressed, to style cursor and tokens + const currentPosition = view.state.selection.main.head; + const pos = offsetToPos(view.state.doc, currentPosition); + const token = getTokenAtPosition(view.state.doc.toString(), pos); + const tInfo = getTypeInfo(schema, token.state); + + const opts = getOpts(view.state); + + if (opts?.onShowInDocs && isMetaKeyPressed(evt)) { + opts.onShowInDocs( + tInfo.fieldDef?.name, + tInfo.type?.toString(), + tInfo.parentType?.toString(), + ); + } + }, +}); diff --git a/packages/cm6-graphql/src/language.ts b/packages/cm6-graphql/src/language.ts new file mode 100644 index 00000000000..5a38e994d3b --- /dev/null +++ b/packages/cm6-graphql/src/language.ts @@ -0,0 +1,58 @@ +import { parser } from './syntax.grammar'; +import { + LRLanguage, + LanguageSupport, + indentNodeProp, + foldNodeProp, + foldInside, + delimitedIndent, +} from '@codemirror/language'; +import { styleTags, tags as t } from '@lezer/highlight'; + +const nodesWithBraces = + 'RootTypeDefinition InputFieldsDefinition EnumValuesDefinition FieldsDefinition SelectionSet { }'; +const keywords = + 'scalar type interface union enum input implements fragment extend schema directive on repeatable'; +const punctuations = '( ) { } : [ ]'; +export const graphqlLanguage = LRLanguage.define({ + parser: parser.configure({ + props: [ + styleTags({ + Variable: t.variableName, + BooleanValue: t.bool, + StringValue: t.string, + Comment: t.lineComment, + IntValue: t.integer, + FloatValue: t.float, + EnumValue: t.special(t.name), + NullValue: t.null, + DirectiveName: t.modifier, + [keywords]: t.keyword, + OperationType: t.definitionKeyword, + FieldName: t.propertyName, + Field: t.propertyName, + ArgumentAttributeName: t.attributeName, + Name: t.atom, + '( )': t.paren, + '{ }': t.brace, + ',': t.separator, + [punctuations]: t.punctuation, + }), + // https://codemirror.net/docs/ref/#language.indentNodeProp + indentNodeProp.add({ + [nodesWithBraces]: delimitedIndent({ closing: '}', align: true }), + }), + foldNodeProp.add({ + [nodesWithBraces]: foldInside, + }), + ], + }), + languageData: { + commentTokens: { line: '#' }, + indentOnInput: /^\s*(\{|\})$/, + }, +}); + +export function graphqlLanguageSupport() { + return new LanguageSupport(graphqlLanguage); +} diff --git a/packages/cm6-graphql/src/lint.ts b/packages/cm6-graphql/src/lint.ts new file mode 100644 index 00000000000..2d572b4ba8a --- /dev/null +++ b/packages/cm6-graphql/src/lint.ts @@ -0,0 +1,44 @@ +import { Diagnostic, linter } from '@codemirror/lint'; +import { getDiagnostics } from 'graphql-language-service'; +import { Position, posToOffset } from './helpers'; +import { getSchema } from './state'; + +const SEVERITY = ['error', 'warning', 'info'] as const; + +export const lint = linter(view => { + const schema = getSchema(view.state); + if (!schema) { + return []; + } + const results = getDiagnostics(view.state.doc.toString(), schema); + + return results + .map((item): Diagnostic | null => { + if (!item.severity || !item.source) { + return null; + } + + const calculatedFrom = posToOffset( + view.state.doc, + new Position(item.range.start.line, item.range.start.character), + ); + const from = Math.max(0, Math.min(calculatedFrom, view.state.doc.length)); + const calculatedRo = posToOffset( + view.state.doc, + new Position(item.range.end.line, item.range.end.character - 1), + ); + const to = Math.min( + Math.max(from + 1, calculatedRo), + view.state.doc.length, + ); + return { + from, + to: from === to ? to + 1 : to, + severity: SEVERITY[item.severity - 1], + // source: item.source, // TODO: + message: item.message, + actions: [], // TODO: + }; + }) + .filter((_): _ is Diagnostic => Boolean(_)); +}); diff --git a/packages/cm6-graphql/src/state.ts b/packages/cm6-graphql/src/state.ts new file mode 100644 index 00000000000..5851715454b --- /dev/null +++ b/packages/cm6-graphql/src/state.ts @@ -0,0 +1,53 @@ +import { EditorState, StateField, StateEffect } from '@codemirror/state'; +import { EditorView } from '@codemirror/view'; +import { GraphQLSchema } from 'graphql'; +import { GqlExtensionsOptions } from './interfaces'; + +const schemaEffect = StateEffect.define(); +const schemaStateField = StateField.define({ + create() {}, + update(schema, tr) { + for (const e of tr.effects) { + if (e.is(schemaEffect)) { + return e.value; + } + } + + return schema; + }, +}); + +const optionsEffect = StateEffect.define(); +const optionsStateField = StateField.define({ + create() {}, + update(opts, tr) { + for (const e of tr.effects) { + if (e.is(optionsEffect)) { + return e.value; + } + } + + return opts; + }, +}); +export const updateSchema = (view: EditorView, schema?: GraphQLSchema) => { + view.dispatch({ + effects: schemaEffect.of(schema), + }); +}; +export const updateOpts = (view: EditorView, opts?: GqlExtensionsOptions) => { + view.dispatch({ + effects: optionsEffect.of(opts), + }); +}; +export const getSchema = (state: EditorState) => { + return state.field(schemaStateField); +}; +export const getOpts = (state: EditorState) => { + return state.field(optionsStateField); +}; + +export const stateExtensions = ( + schema?: GraphQLSchema, + opts?: GqlExtensionsOptions, +) => [schemaStateField.init(() => schema), optionsStateField.init(() => opts)]; diff --git a/packages/cm6-graphql/src/syntax.grammar b/packages/cm6-graphql/src/syntax.grammar new file mode 100644 index 00000000000..355ef280dd3 --- /dev/null +++ b/packages/cm6-graphql/src/syntax.grammar @@ -0,0 +1,452 @@ + +// https://spec.graphql.org/October2021/#sec-Document-Syntax + +// https://github.com/antlr/grammars-v4/blob/49469cb6906f4514be3c04ac0c61c78bc5a6e35a/graphql/GraphQL.g4 + +@precedence { + vDef +} + +@skip { whitespace | Comment } + +// https://spec.graphql.org/October2021/#Document +@top Document { definition+ } + +// https://spec.graphql.org/October2021/#Definition +definition { executableDefinition | typeSystemDefinitionOrExtension } + +// https://spec.graphql.org/October2021/#TypeSystemDefinitionOrExtension +typeSystemDefinitionOrExtension { TypeSystemDefinition | TypeSystemExtension } + +// https://spec.graphql.org/October2021/#TypeSystemExtension +TypeSystemExtension { + SchemaExtension | TypeExtension +} + +// https://spec.graphql.org/October2021/#SchemaExtension +SchemaExtension { + ExtendKeyword SchemaKeyword Directives? RootTypeDefinition | + ExtendKeyword SchemaKeyword Directives +} + +// https://spec.graphql.org/October2021/#TypeExtension +TypeExtension { + ScalarTypeExtension | + ObjectTypeExtension | + InterfaceTypeExtension | + UnionTypeExtension | + EnumTypeExtension | + InputObjectTypeExtension +} + +// https://spec.graphql.org/October2021/#ScalarTypeExtension +ScalarTypeExtension { + ExtendKeyword ScalarKeyword Name Directives +} + +// https://spec.graphql.org/October2021/#ObjectTypeExtension +ObjectTypeExtension { + ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives? FieldsDefinition | + ExtendKeyword TypeKeyword Name ImplementsInterfaces? Directives | + ExtendKeyword TypeKeyword Name ImplementsInterfaces +} + +// https://spec.graphql.org/October2021/#InterfaceTypeExtension +InterfaceTypeExtension { + ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition | + ExtendKeyword InterfaceKeyword Name ImplementsInterfaces? Directives | + ExtendKeyword InterfaceKeyword Name ImplementsInterfaces +} + +// https://spec.graphql.org/October2021/#UnionTypeExtension +UnionTypeExtension { + ExtendKeyword UnionKeyword Name Directives? UnionMemberTypes | + ExtendKeyword UnionKeyword Name Directives +} + +// https://spec.graphql.org/October2021/#EnumTypeExtension +EnumTypeExtension { + ExtendKeyword EnumKeyword Name Directives? EnumValuesDefinition | + ExtendKeyword EnumKeyword Name Directives +} + +// https://spec.graphql.org/October2021/#InputObjectTypeExtension +InputObjectTypeExtension { + ExtendKeyword InputKeyword Name Directives? InputFieldsDefinition | + ExtendKeyword InputKeyword Name Directives +} + +// https://spec.graphql.org/October2021/#ExecutableDefinition +executableDefinition { OperationDefinition | FragmentDefinition } + +// https://spec.graphql.org/October2021/#OperationDefinition +OperationDefinition { + OperationType Name? VariableDefinitions? Directives? SelectionSet | + SelectionSet +} + +// https://spec.graphql.org/October2021/#TypeSystemDefinition +TypeSystemDefinition { + SchemaDefinition | + TypeDefinition | + DirectiveDefinition +} + +// https://spec.graphql.org/October2021/#SchemaDefinition +SchemaDefinition { + Description? SchemaKeyword Directives? RootTypeDefinition +} + +RootTypeDefinition { scopedBraces } + +// https://spec.graphql.org/October2021/#Description +Description { StringValue } + +// https://spec.graphql.org/October2021/#RootOperationTypeDefinition +RootOperationTypeDefinition { OperationType ":" NamedType } + +// https://spec.graphql.org/October2021/#TypeDefinition +TypeDefinition { + ScalarTypeDefinition | + ObjectTypeDefinition | + InterfaceTypeDefinition | + UnionTypeDefinition | + EnumTypeDefinition | + InputObjectTypeDefinition +} + +// https://spec.graphql.org/October2021/#DirectiveDefinition +DirectiveDefinition { + Description? DirectiveKeyword DirectiveName ArgumentsDefinition? RepeatableKeyword? OnKeyword DirectiveLocations +} + +// https://spec.graphql.org/October2021/#DirectiveLocations +DirectiveLocations { + DirectiveLocations "|" DirectiveLocation | + "|"? DirectiveLocation +} + +// https://spec.graphql.org/October2021/#DirectiveLocation +DirectiveLocation { + ExecutableDirectiveLocation | + TypeSystemDirectiveLocation +} + +ExecutableDirectiveLocation { + "QUERY" | + "MUTATION" | + "SUBSCRIPTION" | + "FIELD" | + "FRAGMENT_DEFINITION" | + "FRAGMENT_SPREAD" | + "INLINE_FRAGMENT" | + "VARIABLE_DEFINITION" +} + +TypeSystemDirectiveLocation { + "SCHEMA" | + "SCALAR" | + "OBJECT" | + "FIELD_DEFINITION" | + "ARGUMENT_DEFINITION" | + "INTERFACE" | + "UNION" | + "ENUM" | + "ENUM_VALUE" | + "INPUT_OBJECT" | + "INPUT_FIELD_DEFINITION" + +} + +// https://spec.graphql.org/October2021/#ScalarTypeDefinition +ScalarTypeDefinition { + Description ScalarKeyword Name Directives? +} + +// https://spec.graphql.org/October2021/#ObjectTypeDefinition +ObjectTypeDefinition { + Description? TypeKeyword Name ImplementsInterfaces? Directives? FieldsDefinition? +} + +// https://spec.graphql.org/October2021/#InterfaceTypeDefinition +InterfaceTypeDefinition { + Description? InterfaceKeyword Name ImplementsInterfaces? Directives? FieldsDefinition? +} + +// https://spec.graphql.org/October2021/#UnionTypeDefinition +UnionTypeDefinition { + Description? UnionKeyword Name Directives? UnionMemberTypes? +} + +// https://spec.graphql.org/October2021/#EnumTypeDefinition +EnumTypeDefinition { + Description? EnumKeyword Name Directives? EnumValuesDefinition? +} + +// https://spec.graphql.org/October2021/#InputObjectTypeDefinition +InputObjectTypeDefinition { + Description? InputKeyword Name Directives? InputFieldsDefinition? +} + +// https://spec.graphql.org/October2021/#InputFieldsDefinition +InputFieldsDefinition { + scopedBraces +} + +// https://spec.graphql.org/October2021/#EnumValuesDefinition +EnumValuesDefinition { + scopedBraces +} + +// https://spec.graphql.org/October2021/#EnumValueDefinition +EnumValueDefinition { + Description? EnumValue Directives? +} + +// https://spec.graphql.org/October2021/#UnionMemberTypes +UnionMemberTypes { + UnionMemberTypes "|" NamedType | + "=" "|"? NamedType +} + +// https://spec.graphql.org/October2021/#ImplementsInterfaces +ImplementsInterfaces { + ImplementsInterfaces "&" NamedType | + ImplementsKeyword "&"? NamedType +} + +// https://spec.graphql.org/October2021/#FieldsDefinition +FieldsDefinition { scopedBraces } + +// https://spec.graphql.org/October2021/#FieldDefinition +FieldDefinition { + Description? Name ArgumentsDefinition? ":" type Directives? +} + +// https://spec.graphql.org/October2021/#ArgumentsDefinition +ArgumentsDefinition { InputValueDefinition+ } + +// https://spec.graphql.org/October2021/#InputValueDefinition +InputValueDefinition { + Description? Name ":" type DefaultValue? Directives? +} + +// https://spec.graphql.org/October2021/#FragmentDefinition +FragmentDefinition { + FragmentKeyword FragmentName TypeCondition Directives? SelectionSet +} + +// https://spec.graphql.org/October2021/#FragmentSpread +FragmentSpread { + "..." FragmentName Directives? +} + +// https://spec.graphql.org/October2021/#FragmentName +FragmentName { + Name // TODO: not `on` +} + +// https://spec.graphql.org/October2021/#InlineFragment +InlineFragment { + "..." TypeCondition? Directives? SelectionSet +} + +// https://spec.graphql.org/October2021/#TypeCondition +TypeCondition { OnKeyword NamedType } + +// https://spec.graphql.org/October2021/#OperationType +OperationType { operation<"query"> | operation<"mutation"> | operation<"subscription"> } + +// https://spec.graphql.org/October2021/#VariableDefinitions +VariableDefinitions { "(" VariableDefinition+ ")" } + +// TODO: Directives[Const] +// https://spec.graphql.org/October2021/#VariableDefinition +VariableDefinition { + Variable ":" type DefaultValue? Directives? comma? +} + +// https://spec.graphql.org/October2021/#Type +type { NamedType | ListType | NonNullType } + +// https://spec.graphql.org/October2021/#NamedType +NamedType { Name } + +// https://spec.graphql.org/October2021/#ListType +ListType { "[" type "]" } + +// https://spec.graphql.org/October2021/#NonNullType +NonNullType { + NamedType "!" | + ListType "!" +} + +// https://spec.graphql.org/October2021/#Directives +Directives { Directive+ } + +// https://spec.graphql.org/October2021/#Directive +Directive { DirectiveName Arguments? } + +// https://spec.graphql.org/October2021/#Arguments +Arguments { "(" Argument+ ")"} + +// https://spec.graphql.org/October2021/#Argument +Argument { ArgumentAttributeName ":" value } + +ArgumentAttributeName { name } + +// https://spec.graphql.org/October2021/#SelectionSet +SelectionSet { "{" Selection+ "}" } + +// https://spec.graphql.org/October2021/#Selection +Selection { + (Field | FragmentSpread | InlineFragment) comma? +} + +// https://spec.graphql.org/October2021/#Field +Field { + Alias? FieldName Arguments? Directives? SelectionSet? +} + +FieldName { name } + +// https://spec.graphql.org/October2021/#Alias +Alias { Name ":" } + +// TODO: Value[Const] +// https://spec.graphql.org/October2021/#DefaultValue +DefaultValue { "=" value } + +// https://spec.graphql.org/October2021/#Value +value { + Variable | // TODO: [if not Const] + IntValue | + FloatValue | + StringValue | + BooleanValue | + NullValue | + EnumValue | + ListValue | // TODO: [?Const] + ObjectValue // TODO: [?Const] +} + +// https://spec.graphql.org/October2021/#ListValue +ListValue { "[" (value comma?)* "]" } + +// TODO: ObjectField[Const] +// https://spec.graphql.org/October2021/#ObjectValue +ObjectValue { + "{" objectField* "}" +} + +// https://spec.graphql.org/October2021/#ObjectField +objectField { + Name ":" value comma? +} + +Name { name } + +@tokens { + + @precedence { FloatValue, IntValue } + @precedence { NullValue, EnumValue } + @precedence { BooleanValue, EnumValue } + + DirectiveName { "@" name } + + // https://spec.graphql.org/October2021/#Variable + Variable { "$" name } + + // https://spec.graphql.org/October2021/#IntValue + IntValue { + "-"? @digit+ + } + + // https://spec.graphql.org/October2021/#FloatValue + FloatValue { + IntValue fractionalPart exponentPart | + IntValue fractionalPart | + IntValue exponentPart + } + + fractionalPart { + "." @digit+ + } + + exponentPart { + $[eE] $[+\-]? @digit+ + } + + // https://spec.graphql.org/October2021/#StringValue + StringValue { + '"' stringCharacter* '"' | + '"""' blockStringCharacter* '"""' + } + + // https://spec.graphql.org/October2021/#StringCharacter + stringCharacter { + !["\\\r\n] | + "\\u" escapedUnicode | + "\\" escapedCharacter + } + + // https://spec.graphql.org/October2021/#BlockStringCharacter + blockStringCharacter { + '\\"""' | + '"' '"'? !["] + } + + // https://spec.graphql.org/October2021/#EscapedUnicode + escapedUnicode { + $[0-9A-Fa-f] $[0-9A-Fa-f] $[0-9A-Fa-f] $[0-9A-Fa-f] + } + + // https://spec.graphql.org/October2021/#EscapedCharacter + escapedCharacter { "\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t"} + + // https://spec.graphql.org/October2021/#BooleanValue + BooleanValue { "true" | "false" } + + // https://spec.graphql.org/October2021/#NullValue + NullValue { "null" } + + // TODO: Value but not true or false or null + // https://spec.graphql.org/October2021/#EnumValue + EnumValue { name } + + // https://spec.graphql.org/October2021/#Name + name { $[_A-Za-z] $[_0-9A-Za-z]* } + + // Name { name } + + Comment { "#" ![\n]* } + + "{" "}" "(" ")" "[" "]" + + comma { "," } + + whitespace { @whitespace+ } +} + +scopedBraces { !vDef "{" expr "}" } + +// keywords +kw { @specialize[@name={term}] } + +operation { @extend } + +ScalarKeyword { kw<"scalar"> } +TypeKeyword { kw<"type"> } +InterfaceKeyword { kw<"interface"> } +UnionKeyword { kw<"union"> } +EnumKeyword { kw<"enum"> } +InputKeyword { kw<"input"> } +ImplementsKeyword { kw<"implements"> } +FragmentKeyword { kw<"fragment"> } +ExtendKeyword { kw<"extend"> } +SchemaKeyword { kw<"schema"> } +DirectiveKeyword { kw<"directive"> } +OnKeyword { kw<"on"> } +RepeatableKeyword { kw<"repeatable"> } + +@detectDelim diff --git a/packages/cm6-graphql/src/syntax.grammar.d.ts b/packages/cm6-graphql/src/syntax.grammar.d.ts new file mode 100644 index 00000000000..1d7864093b2 --- /dev/null +++ b/packages/cm6-graphql/src/syntax.grammar.d.ts @@ -0,0 +1,3 @@ +import { LRParser } from '@lezer/lr'; + +export declare const parser: LRParser; diff --git a/packages/cm6-graphql/tsconfig.esm.json b/packages/cm6-graphql/tsconfig.esm.json new file mode 100644 index 00000000000..876c041c963 --- /dev/null +++ b/packages/cm6-graphql/tsconfig.esm.json @@ -0,0 +1,14 @@ +{ + "extends": "../../resources/tsconfig.base.esm.json", + "compilerOptions": { + "strict": true, + "newLine": "lf", + "outDir": "dist" + }, + "include": ["src/*.ts"], + "references": [ + { + "path": "../graphql-language-service" + } + ] +} diff --git a/packages/cm6-graphql/tsconfig.json b/packages/cm6-graphql/tsconfig.json new file mode 100644 index 00000000000..6c9ca9a48fb --- /dev/null +++ b/packages/cm6-graphql/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../resources/tsconfig.base.cjs.json", + "compilerOptions": { + "strict": true, + "target": "es6", + "module": "es2020", + "newLine": "lf", + "declaration": true, + "moduleResolution": "node", + "outDir": "dist" + }, + "include": ["src", "__tests__"], + "exclude": ["**/__tests__/**", "**/dist/**.*"], + "references": [ + { + "path": "../graphql-language-service" + } + ] +} diff --git a/packages/codemirror-graphql/.npmignore b/packages/codemirror-graphql/.npmignore new file mode 100644 index 00000000000..76d2c8e5aab --- /dev/null +++ b/packages/codemirror-graphql/.npmignore @@ -0,0 +1,4 @@ +babel.config.js +.gitignore +resources +src diff --git a/packages/codemirror-graphql/CHANGELOG.md b/packages/codemirror-graphql/CHANGELOG.md new file mode 100644 index 00000000000..307dc93b337 --- /dev/null +++ b/packages/codemirror-graphql/CHANGELOG.md @@ -0,0 +1,501 @@ +# Change Log + +## 2.0.9 + +### Patch Changes + +- [#3203](https://github.com/graphql/graphiql/pull/3203) [`61986469`](https://github.com/graphql/graphiql/commit/619864691941c46cc0b0848e8713028e20212c36) Thanks [@lesleydreyer](https://github.com/lesleydreyer)! - fix info tooltips to work when Graphiql is not used as full page + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5)]: + - graphql-language-service@5.1.7 + +## 2.0.9-alpha.1 + +### Patch Changes + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5)]: + - graphql-language-service@5.1.7-alpha.0 + +## 2.0.9-alpha.0 + +### Patch Changes + +- [#3203](https://github.com/graphql/graphiql/pull/3203) [`61986469`](https://github.com/graphql/graphiql/commit/619864691941c46cc0b0848e8713028e20212c36) Thanks [@lesleydreyer](https://github.com/lesleydreyer)! - fix info tooltips to work when Graphiql is not used as full page + +## 2.0.8 + +### Patch Changes + +- Updated dependencies [[`06007498`](https://github.com/graphql/graphiql/commit/06007498880528ed75dd4d705dcbcd7c9e775939)]: + - graphql-language-service@5.1.6 + +## 2.0.7 + +### Patch Changes + +- Updated dependencies [[`4d33b221`](https://github.com/graphql/graphiql/commit/4d33b2214e941f171385a1b72a1fa995714bb284)]: + - graphql-language-service@5.1.5 + +## 2.0.6 + +### Patch Changes + +- [#3113](https://github.com/graphql/graphiql/pull/3113) [`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `.forEach` with `for..of` + +- [#3109](https://github.com/graphql/graphiql/pull/3109) [`51007002`](https://github.com/graphql/graphiql/commit/510070028b7d8e98f2ba25f396519976aea5fa4b) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-floating-promises` eslint rule + +- Updated dependencies [[`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9)]: + - graphql-language-service@5.1.4 + +## 2.0.5 + +### Patch Changes + +- [#3046](https://github.com/graphql/graphiql/pull/3046) [`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer .at() method for index access + +- Updated dependencies [[`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d), [`881a2024`](https://github.com/graphql/graphiql/commit/881a202497d5a58eb5260a5aa54c0c88930d69a0)]: + - graphql-language-service@5.1.3 + +## 2.0.4 + +### Patch Changes + +- [#2993](https://github.com/graphql/graphiql/pull/2993) [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4) Thanks [@B2o5T](https://github.com/B2o5T)! - add `unicorn/consistent-destructuring` rule + +- [#2962](https://github.com/graphql/graphiql/pull/2962) [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d) Thanks [@B2o5T](https://github.com/B2o5T)! - clean all ESLint warnings, add `--max-warnings=0` and `--cache` flags + +- [#2940](https://github.com/graphql/graphiql/pull/2940) [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-node-protocol` rule + +- Updated dependencies [[`e68cb8bc`](https://github.com/graphql/graphiql/commit/e68cb8bcaf9baddf6fca747abab871ecd1bc7a4c), [`f788e65a`](https://github.com/graphql/graphiql/commit/f788e65aff267ec873237034831d1fd936222a9b), [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4), [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d), [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba)]: + - graphql-language-service@5.1.2 + +## 2.0.3 + +### Patch Changes + +- [#2931](https://github.com/graphql/graphiql/pull/2931) [`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-negated-condition` and `no-else-return` rules + +- [#2922](https://github.com/graphql/graphiql/pull/2922) [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b) Thanks [@B2o5T](https://github.com/B2o5T)! - extends `plugin:import/recommended` and fix warnings + +- [#2941](https://github.com/graphql/graphiql/pull/2941) [`4a8b2e17`](https://github.com/graphql/graphiql/commit/4a8b2e1766a38eb4828cf9a81bf9d767070041de) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-logical-operator-over-ternary` rule + +- [#2937](https://github.com/graphql/graphiql/pull/2937) [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-includes` + +- [#2936](https://github.com/graphql/graphiql/pull/2936) [`18f8e80a`](https://github.com/graphql/graphiql/commit/18f8e80ae12edfd0c36adcb300cf9e06ac27ea49) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `lonely-if`/`unicorn/lonely-if` rules + +- [#2963](https://github.com/graphql/graphiql/pull/2963) [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `prefer-destructuring` rule + +- Updated dependencies [[`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147), [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b), [`4a8b2e17`](https://github.com/graphql/graphiql/commit/4a8b2e1766a38eb4828cf9a81bf9d767070041de), [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9), [`c44ea4f1`](https://github.com/graphql/graphiql/commit/c44ea4f1917b97daac815c08299b934c8ca57ed9), [`0669767e`](https://github.com/graphql/graphiql/commit/0669767e1e2196a78cbefe3679a52bcbb341e913), [`18f8e80a`](https://github.com/graphql/graphiql/commit/18f8e80ae12edfd0c36adcb300cf9e06ac27ea49), [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215), [`6a9d913f`](https://github.com/graphql/graphiql/commit/6a9d913f0d1b847124286b3fa1f3a2649d315171)]: + - graphql-language-service@5.1.1 + +## 2.0.2 + +### Patch Changes + +- [#2852](https://github.com/graphql/graphiql/pull/2852) [`20869583`](https://github.com/graphql/graphiql/commit/20869583eff563f5d6494e93302a835f0e034f4b) Thanks [@acao](https://github.com/acao)! - increment @codemirror/language peer dependency to 6.0.0 + +## 2.0.1 + +### Patch Changes + +- [#2847](https://github.com/graphql/graphiql/pull/2847) [`353f434e`](https://github.com/graphql/graphiql/commit/353f434e5f6bfd1bf6f8ee97d4ae8ce4f897085f) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Don't show error in variable editor linting for missing input objects that have a default value + +## 2.0.0 + +### Major Changes + +- [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - BREAKING: Change the implementation of the info popup when hovering items in the code editor: + - For fields the type prefix was removed, i.e. `MyType.myField` -> `myField` + - For args, the type and field was removed, i.e. `MyType.myField(myArg: MyArgType)` -> `myArg: MyArgType` + - The DOM structure of the info tooltip changed to enable more flexible styling: + - The first section (i.e. the clickable parts like type and field name) are wrapped in an additional div + - The markdown content for deprecation reasons is wrapped in an additional div + +## 1.3.3 + +### Patch Changes + +- Updated dependencies [[`d6ff4d7a`](https://github.com/graphql/graphiql/commit/d6ff4d7a5d535a0c43fe5914016bac9ef0c2b782)]: + - graphql-language-service@5.1.0 + +## 1.3.2 + +### Patch Changes + +- Updated dependencies [[`cccefa70`](https://github.com/graphql/graphiql/commit/cccefa70c0466d60e8496e1df61aeb1490af723c)]: + - graphql-language-service@5.0.6 + +## 1.3.1 + +### Patch Changes + +- Updated dependencies [[`c9c51b8a`](https://github.com/graphql/graphiql/commit/c9c51b8a98e1f0427272d3e9ad60989b32f1a1aa)]: + - graphql-language-service@5.0.5 + +## 1.3.0 + +### Minor Changes + +- [#2369](https://github.com/graphql/graphiql/pull/2369) [`2dec55f2`](https://github.com/graphql/graphiql/commit/2dec55f2c5e979cc7bb1adadff4fb063775b088c) Thanks [@sergeichestakov](https://github.com/sergeichestakov)! - Moved @codemirror/language to peer dependencies and upgraded to 0.20.0 + +### Patch Changes + +- Updated dependencies [[`d22f6111`](https://github.com/graphql/graphiql/commit/d22f6111a60af25727d8dbc1058c79607df76af2)]: + - graphql-language-service@5.0.4 + +## 1.2.17 + +### Patch Changes + +- Updated dependencies [[`45cbc759`](https://github.com/graphql/graphiql/commit/45cbc759c732999e8b1eb4714d6047ab77c17902)]: + - graphql-language-service@5.0.3 + +## 1.2.16 + +### Patch Changes + +- Updated dependencies [[`c36504a8`](https://github.com/graphql/graphiql/commit/c36504a804d8cc54a5136340152999b4a1a2c69f)]: + - graphql-language-service@5.0.2 + +## 1.2.15 + +### Patch Changes + +- [#2261](https://github.com/graphql/graphiql/pull/2261) [`261f2044`](https://github.com/graphql/graphiql/commit/261f2044066412e40f9962bef55295f7c9c35aec) Thanks [@acao](https://github.com/acao)! - Fix typescript path resolution bug in codemirror-graphql + +## 1.2.14 + +### Patch Changes + +- Updated dependencies [[`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa), [`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa)]: + - graphql-language-service@5.0.1 + +## 1.2.13 + +### Patch Changes + +- Updated dependencies [[`2502a364`](https://github.com/graphql/graphiql/commit/2502a364b74dc754d92baa1579b536cf42139958)]: + - graphql-language-service@5.0.0 + +## 1.2.12 + +### Patch Changes + +- Updated dependencies [[`484c0523`](https://github.com/graphql/graphiql/commit/484c0523cdd529f9e261d61a38616b6745075c7f), [`5852ba47`](https://github.com/graphql/graphiql/commit/5852ba47c720a2577817aed512bef9a262254f2c), [`48c5df65`](https://github.com/graphql/graphiql/commit/48c5df654e323cee3b8c57d7414247465235d1b5)]: + - graphql-language-service@4.1.5 + +## 1.2.11 + +### Patch Changes + +- Updated dependencies []: + - graphql-language-service@4.1.4 + +## 1.2.10 + +### Patch Changes + +- Updated dependencies [[`a44772d6`](https://github.com/graphql/graphiql/commit/a44772d6af97254c4f159ea7237e842a3e3719e8)]: + - graphql-language-service@4.1.3 + +## 1.2.9 + +### Patch Changes + +- Updated dependencies [[`e20760fb`](https://github.com/graphql/graphiql/commit/e20760fbd95c13d6d549cba3faa15a59aee9a2c0)]: + - graphql-language-service@4.1.2 + +## 1.2.8 + +### Patch Changes + +- [#2091](https://github.com/graphql/graphiql/pull/2091) [`ff9cebe5`](https://github.com/graphql/graphiql/commit/ff9cebe515a3539f85b9479954ae644dfeb68b63) Thanks [@acao](https://github.com/acao)! - Fix graphql 15 related issues. Should now build & test interchangeably. + +- Updated dependencies []: + - graphql-language-service@4.1.1 + +## 1.2.7 + +### Patch Changes + +- Updated dependencies [[`0f1f90ce`](https://github.com/graphql/graphiql/commit/0f1f90ce8f4a25ddebdaf7a9ddbe136214aa64a3)]: + - graphql-language-service@4.1.0 + +## 1.2.6 + +### Patch Changes + +- Updated dependencies [[`9df315b4`](https://github.com/graphql/graphiql/commit/9df315b44896efa313ed6744445fc8f9e702ebc3)]: + - graphql-language-service@4.0.0 + +## 1.2.5 + +### Patch Changes + +- Updated dependencies [[`df57cd25`](https://github.com/graphql/graphiql/commit/df57cd2556302d6aa5dd140e7bee3f7bdab4deb1)]: + - graphql-language-service@3.2.5 + +## 1.2.4 + +### Patch Changes + +- Updated dependencies []: + - graphql-language-service@3.2.4 + +## 1.2.3 + +### Patch Changes + +- [`c42b145f`](https://github.com/graphql/graphiql/commit/c42b145fffeaefbd1103bc7addee1873e939bc83) [#2052](https://github.com/graphql/graphiql/pull/2052) Thanks [@imolorhe](https://github.com/imolorhe)! - Added cm6-legacy to published files list + +## 1.2.2 + +### Patch Changes + +- [`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf) [#2047](https://github.com/graphql/graphiql/pull/2047) Thanks [@willstott101](https://github.com/willstott101)! - Source code included in all packages to fix source maps. codemirror-graphql includes esm build in package. + +* [`8b486555`](https://github.com/graphql/graphiql/commit/8b486555e2aa4d90891070a1bbc52b59d9c670c4) [#2046](https://github.com/graphql/graphiql/pull/2046) Thanks [@willstott101](https://github.com/willstott101)! - Further resolves #1944, replaces graphql-language-service-parser with graphql-language-service in codemirror-graphql + +* Updated dependencies [[`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf)]: + - graphql-language-service@3.2.3 + +## 1.2.1 + +### Patch Changes + +- [`858907d2`](https://github.com/graphql/graphiql/commit/858907d2106742a65ec52eb017f2e91268cc37bf) [#2045](https://github.com/graphql/graphiql/pull/2045) Thanks [@acao](https://github.com/acao)! - fix graphql-js peer dependencies - [#2044](https://github.com/graphql/graphiql/pull/2044) + +- Updated dependencies [[`858907d2`](https://github.com/graphql/graphiql/commit/858907d2106742a65ec52eb017f2e91268cc37bf)]: + - graphql-language-service@3.2.2 + +## 1.2.0 + +### Minor Changes + +- [`d0c22c4f`](https://github.com/graphql/graphiql/commit/d0c22c4fce5ea39611c7ecee553943fdf27fd03e) [#2035](https://github.com/graphql/graphiql/pull/2035) Thanks [@imolorhe](https://github.com/imolorhe)! - Added Codemirror 6 legacy support + +### Patch Changes + +- [`b79bf304`](https://github.com/graphql/graphiql/commit/b79bf304045add4b5c3b2539dd6b551a64e6ed87) [#2037](https://github.com/graphql/graphiql/pull/2037) Thanks [@acao](https://github.com/acao)! - Resolves #1944, replaces graphql-language-service-utils with graphql-language-service in codemirror-graphql + +## 1.1.0 + +### Minor Changes + +- [`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a) [#2010](https://github.com/graphql/graphiql/pull/2010) Thanks [@acao](https://github.com/acao)! - upgrade to `graphql@16.0.0-experimental-stream-defer.5`. thanks @saihaj! + +### Patch Changes + +- Updated dependencies [[`8869c4b1`](https://github.com/graphql/graphiql/commit/8869c4b18c900b9b35556255587ef5130a96a4d5), [`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a)]: + - graphql-language-service-interface@2.9.0 + - graphql-language-service-parser@1.10.0 + +## 1.0.3 + +### Patch Changes + +- [`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8) [#1777](https://github.com/graphql/graphiql/pull/1777) Thanks [@dwwoelfel](https://github.com/dwwoelfel)! - adopt block string parsing for variables in language parser + +- Updated dependencies [[`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8)]: + - graphql-language-service-parser@1.9.3 + +## 1.0.2 + +### Patch Changes + +- [`5b8a057d`](https://github.com/graphql/graphiql/commit/5b8a057dd64ebecc391be32176a2403bb9d9ff92) [#1838](https://github.com/graphql/graphiql/pull/1838) Thanks [@acao](https://github.com/acao)! - Set all cross-runtime build targets to es6 + +## 1.0.1 + +### Patch Changes + +- [`6869ce77`](https://github.com/graphql/graphiql/commit/6869ce7767050787db5f1017abf82fa5a52fc97a) [#1816](https://github.com/graphql/graphiql/pull/1816) Thanks [@acao](https://github.com/acao)! - improve peer resolutions for graphql 14 & 15. `14.5.0` minimum is for built-in typescript types, and another method only available in `14.4.0` + +## 1.0.0 + +### Major Changes + +- [`b4fc16c0`](https://github.com/graphql/graphiql/commit/b4fc16c025da6f466727dc17cab6026d14c6e7fe) Thanks [@imolorhe](https://github.com/imolorhe)! - BREAKING CHANGE Migrate to Typescript - [@imolorhe](https://github.com/imolorhe) + +All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.15.2](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.15.1...codemirror-graphql@0.15.2) (2021-01-07) + +**Note:** Version bump only for package codemirror-graphql + +## [0.15.1](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.15.0...codemirror-graphql@0.15.1) (2021-01-07) + +### Bug Fixes + +- bug with externalFragments in codemirror ([#1751](https://github.com/graphql/graphiql/issues/1751)) ([f423e61](https://github.com/graphql/graphiql/commit/f423e615330bf8529f4068889d6760501b732527)) + +## [0.15.0](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.14.0...codemirror-graphql@0.15.0) (2021-01-07) + +### Features + +- implied or external fragments, for [#612](https://github.com/graphql/graphiql/issues/612) ([#1750](https://github.com/graphql/graphiql/issues/1750)) ([cfed265](https://github.com/graphql/graphiql/commit/cfed265e3cf31875b39ea517781a217fcdfcadc2)) + +## [0.14.0](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.13.1...codemirror-graphql@0.14.0) (2021-01-03) + +### Features + +- merge completion logic (for implements &, variables) ([#1747](https://github.com/graphql/graphiql/issues/1747)) ([0ac0a85](https://github.com/graphql/graphiql/commit/0ac0a856cfc715d7885a9965a9a9114ef2ca4b1a)) + +## [0.13.1](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.13.0...codemirror-graphql@0.13.1) (2020-12-28) + +**Note:** Version bump only for package codemirror-graphql + +## [0.13.0](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.4...codemirror-graphql@0.13.0) (2020-12-08) + +### Features + +- provide validation rules via props ([#1716](https://github.com/graphql/graphiql/issues/1716)) ([0c5785c](https://github.com/graphql/graphiql/commit/0c5785c82adbd4affb25300ae2d128b42c9b81fe)) + +## [0.12.4](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.3...codemirror-graphql@0.12.4) (2020-11-28) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.3](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.2...codemirror-graphql@0.12.3) (2020-10-20) + +### Bug Fixes + +- **codemirror-graphql:** give interface field name suggestions ([#1695](https://github.com/graphql/graphiql/issues/1695)) ([669b301](https://github.com/graphql/graphiql/commit/669b3013fc679eca7c4e5c8ed6b0cd2fb2dbf2dc)) + +## [0.12.2](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.2-alpha.2...codemirror-graphql@0.12.2) (2020-09-18) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.2-alpha.2](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.2-alpha.1...codemirror-graphql@0.12.2-alpha.2) (2020-09-11) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.2-alpha.1](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.2-alpha.0...codemirror-graphql@0.12.2-alpha.1) (2020-08-12) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.2-alpha.0](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.1...codemirror-graphql@0.12.2-alpha.0) (2020-08-10) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.1](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0...codemirror-graphql@0.12.1) (2020-08-06) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.11...codemirror-graphql@0.12.0) (2020-06-11) + +### Bug Fixes + +- value of documentation in completion list ([#1567](https://github.com/graphql/graphiql/issues/1567)) ([39c00a5](https://github.com/graphql/graphiql/commit/39c00a55d7af43ce4e57ad9b1d5cd55393beb0d0)) + +## [0.12.0-alpha.11](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.10...codemirror-graphql@0.12.0-alpha.11) (2020-06-04) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.10](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.9...codemirror-graphql@0.12.0-alpha.10) (2020-06-04) + +### Bug Fixes + +- cleanup cache entry from lerna publish ([4a26218](https://github.com/graphql/graphiql/commit/4a2621808a1aea8b30d5d27b8d86a60bf2b44b01)) +- make list type and non-nullable type available ([#902](https://github.com/graphql/graphiql/issues/902)) ([cea837f](https://github.com/graphql/graphiql/commit/cea837ff77c36dadb01b4302282821b00d7f5f2f)) + +## [0.12.0-alpha.9](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.8...codemirror-graphql@0.12.0-alpha.9) (2020-05-28) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.8](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.7...codemirror-graphql@0.12.0-alpha.8) (2020-05-17) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.7](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.6...codemirror-graphql@0.12.0-alpha.7) (2020-04-10) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.6](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.5...codemirror-graphql@0.12.0-alpha.6) (2020-04-10) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.5](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.4...codemirror-graphql@0.12.0-alpha.5) (2020-04-06) + +### Features + +- upgrade to graphql@15.0.0 for [#1191](https://github.com/graphql/graphiql/issues/1191) ([#1204](https://github.com/graphql/graphiql/issues/1204)) ([f13c8e9](https://github.com/graphql/graphiql/commit/f13c8e9d0e66df4b051b332c7d02f4bb83e07ffd)) + +## [0.12.0-alpha.4](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.3...codemirror-graphql@0.12.0-alpha.4) (2020-04-03) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.3](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.2...codemirror-graphql@0.12.0-alpha.3) (2020-03-20) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.2](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.12.0-alpha.0...codemirror-graphql@0.12.0-alpha.2) (2020-03-20) + +**Note:** Version bump only for package codemirror-graphql + +## [0.12.0-alpha.1](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.11.6...codemirror-graphql@0.12.0-alpha.1) (2020-01-18) + +### Bug Fixes + +- linting issues, trailingCommas: all ([#1099](https://github.com/graphql/graphiql/issues/1099)) ([de4005b](https://github.com/graphql/graphiql/commit/de4005b)) +- screenshot/gif urls ([e3ea2fc](https://github.com/graphql/graphiql/commit/e3ea2fc)) + +### Features + +- convert LSP Server to Typescript, remove watchman ([#1138](https://github.com/graphql/graphiql/issues/1138)) ([8e33dbb](https://github.com/graphql/graphiql/commit/8e33dbb)) + +## [0.11.6](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.11.5...codemirror-graphql@0.11.6) (2019-12-09) + +### Bug Fixes + +- codemirror results bundle ([dd06eb5](https://github.com/graphql/graphiql/commit/dd06eb5)) + +## [0.11.5](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.11.4...codemirror-graphql@0.11.5) (2019-12-09) + +### Bug Fixes + +- a few more tweaks to babel ignore ([e0ad2c6](https://github.com/graphql/graphiql/commit/e0ad2c6)) + +## [0.11.4](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.11.3...codemirror-graphql@0.11.4) (2019-12-03) + +### Bug Fixes + +- convert browserify build to webpack, fixes [#976](https://github.com/graphql/graphiql/issues/976) ([#1001](https://github.com/graphql/graphiql/issues/1001)) ([3caf041](https://github.com/graphql/graphiql/commit/3caf041)) +- csp headers violation [@gracenoah](https://github.com/gracenoah) graphql/codemirror-graphql[#246](https://github.com/graphql/graphiql/issues/246) ([#1044](https://github.com/graphql/graphiql/issues/1044)) ([3c9dfa5](https://github.com/graphql/graphiql/commit/3c9dfa5)) + +## [0.11.3](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.11.2...codemirror-graphql@0.11.3) (2019-11-26) + +**Note:** Version bump only for package codemirror-graphql + +## [0.11.2](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.11.1...codemirror-graphql@0.11.2) (2019-10-19) + +**Note:** Version bump only for package codemirror-graphql + +## [0.11.1](https://github.com/graphql/graphiql/compare/codemirror-graphql@0.11.0...codemirror-graphql@0.11.1) (2019-10-04) + +### Bug Fixes + +- build tweaks ([0bc6a7c](https://github.com/graphql/graphiql/commit/0bc6a7c)) + +# 0.11.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +# 0.10.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 0.9.1-alpha.1 (2019-09-01) + +**Note:** Version bump only for package codemirror-graphql + +## 0.9.1-alpha.0 (2019-09-01) + +**Note:** Version bump only for package codemirror-graphql + +## 0.9.1 (2019-09-01) + +**Note:** Version bump only for package codemirror-graphql diff --git a/packages/codemirror-graphql/LICENSE b/packages/codemirror-graphql/LICENSE new file mode 100644 index 00000000000..7802f239a32 --- /dev/null +++ b/packages/codemirror-graphql/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 GraphQL Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/codemirror-graphql/README.md b/packages/codemirror-graphql/README.md new file mode 100644 index 00000000000..ac0ef31d5be --- /dev/null +++ b/packages/codemirror-graphql/README.md @@ -0,0 +1,125 @@ +# GraphQL mode for CodeMirror + +[![NPM](https://img.shields.io/npm/v/codemirror-graphql.svg?style=flat-square)](https://npmjs.com/codemirror-graphql) +![npm downloads](https://img.shields.io/npm/dm/codemirror-graphql?label=npm%20downloads) +[![License](https://img.shields.io/npm/l/codemirror-graphql.svg?style=flat-square)](LICENSE) +[Discord Channel](https://discord.gg/cffZwk8NJW) + +**NOTE: For CodeMirror 6, use [cm6-graphql](/packages/cm6-graphql/) instead** + +Provides CodeMirror with a parser mode for GraphQL along with a live linter and +typeahead hinter powered by your GraphQL Schema. + +![Demo .gif of GraphQL Codemirror Mode](https://raw.githubusercontent.com/graphql/graphiql/main/packages/codemirror-graphql/resources/example.gif) + +### Getting Started + +```sh +npm install --save codemirror-graphql +``` + +CodeMirror helpers install themselves to the global CodeMirror when they are +imported. + +```ts +import type { ValidationContext, SDLValidationContext } from 'graphql'; + +import CodeMirror from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/lint/lint'; +import 'codemirror-graphql/hint'; +import 'codemirror-graphql/lint'; +import 'codemirror-graphql/mode'; + +CodeMirror.fromTextArea(myTextarea, { + mode: 'graphql', + lint: { + schema: myGraphQLSchema, + validationRules: [ExampleRule], + }, + hintOptions: { + schema: myGraphQLSchema, + }, +}); +``` + +## External Fragments Example + +If you want to have autocompletion for external fragment definitions, there's a +new configuration setting available + +```ts +import CodeMirror from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/lint/lint'; +import 'codemirror-graphql/hint'; +import 'codemirror-graphql/lint'; +import 'codemirror-graphql/mode'; + +const externalFragments = /* GraphQL */ ` + fragment MyFragment on Example { + id: ID! + name: String! + } + fragment AnotherFragment on Example { + id: ID! + title: String! + } +`; + +CodeMirror.fromTextArea(myTextarea, { + mode: 'graphql', + lint: { + schema: myGraphQLSchema, + }, + hintOptions: { + schema: myGraphQLSchema, + // here we use a string, but + // you can also provide an array of FragmentDefinitionNodes + externalFragments, + }, +}); +``` + +### Custom Validation Rules + +If you want to show custom validation, you can do that too! It uses the +`ValidationRule` interface. + +```ts +import type { ValidationRule } from 'graphql'; + +import CodeMirror from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/lint/lint'; +import 'codemirror-graphql/hint'; +import 'codemirror-graphql/lint'; +import 'codemirror-graphql/mode'; + +const ExampleRule: ValidationRule = context => { + // your custom rules here + const schema = context.getSchema(); + const document = context.getDocument(); + return { + NamedType(node) { + if (node.name.value !== node.name.value.toLowercase()) { + context.reportError('only lowercase type names allowed!'); + } + }, + }; +}; + +CodeMirror.fromTextArea(myTextarea, { + mode: 'graphql', + lint: { + schema: myGraphQLSchema, + validationRules: [ExampleRule], + }, + hintOptions: { + schema: myGraphQLSchema, + }, +}); +``` + +Build for the web with [webpack](http://webpack.github.io/) or +[browserify](http://browserify.org/). diff --git a/packages/codemirror-graphql/babel.config.js b/packages/codemirror-graphql/babel.config.js new file mode 100644 index 00000000000..4c05afbee70 --- /dev/null +++ b/packages/codemirror-graphql/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); diff --git a/packages/codemirror-graphql/jest.config.js b/packages/codemirror-graphql/jest.config.js new file mode 100644 index 00000000000..342851e977e --- /dev/null +++ b/packages/codemirror-graphql/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config.base')(__dirname); diff --git a/packages/codemirror-graphql/package.json b/packages/codemirror-graphql/package.json new file mode 100644 index 00000000000..90b225ce659 --- /dev/null +++ b/packages/codemirror-graphql/package.json @@ -0,0 +1,64 @@ +{ + "name": "codemirror-graphql", + "version": "2.0.9", + "description": "GraphQL mode and helpers for CodeMirror.", + "contributors": [ + "Hyohyeon Jeong ", + "Lee Byron (http://leebyron.com/)", + "Angel Gomez Salazar " + ], + "homepage": "https://github.com/graphql/graphiql/tree/main/packages/codemirror-graphql#readme", + "repository": { + "type": "git", + "url": "http://github.com/graphql/graphiql", + "directory": "packages/codemirror-graphql" + }, + "bugs": { + "url": "https://github.com/graphql/graphiql/issues?q=issue+label:codemirror-graphql" + }, + "license": "MIT", + "main": "index.js", + "module": "esm/index.js", + "files": [ + "src", + "cm6-legacy", + "esm", + "utils", + "variables", + "results", + "/*.js", + "/*.js.flow", + "/*.js.map", + "/*.d.ts", + "/*.d.ts.map", + "!babel.config.js", + "!jest.config.js" + ], + "scripts": { + "build": "yarn build-clean && yarn build-js && yarn build-esm && yarn build-flow .", + "build-js": "yarn tsc", + "build-esm": "cross-env ESM=true yarn tsc --project tsconfig.esm.json && node ../../scripts/renameFileExtensions.js './esm/{**,!**/__tests__/}/*.js' . .esm.js", + "build-clean": "rimraf {mode,hint,info,jump,lint}.{js,esm.js,js.flow,js.map,d.ts,d.ts.map} && rimraf esm results utils variables coverage cm6-legacy __tests__", + "build-flow": "node ../../scripts/buildFlow.js", + "watch": "babel --optional runtime resources/watch.js | node", + "test": "jest", + "postbuild": "node ../../scripts/renameFileExtensions.js './esm/{**,!**/__tests__/}/*.js' . .esm.js" + }, + "peerDependencies": { + "@codemirror/language": "6.0.0", + "codemirror": "^5.65.3", + "graphql": "^15.5.0 || ^16.0.0" + }, + "// TEMPORARILY PINNED until we fix graphql 15 support": "", + "dependencies": { + "graphql-language-service": "5.1.7" + }, + "devDependencies": { + "@codemirror/language": "6.0.0", + "codemirror": "^5.65.3", + "cross-env": "^7.0.2", + "graphql": "^16.4.0", + "rimraf": "^3.0.2", + "sane": "2.0.0" + } +} diff --git a/packages/codemirror-graphql/resources/checkgit.sh b/packages/codemirror-graphql/resources/checkgit.sh new file mode 100644 index 00000000000..0f8545ea2ac --- /dev/null +++ b/packages/codemirror-graphql/resources/checkgit.sh @@ -0,0 +1,27 @@ +# +# This script determines if current git state is the up to date main. 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 main. +# + +# 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 main is checked out +if [ "$GITBRANCHNAME" != "main" ]; then + read -p "Git not on main 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/packages/codemirror-graphql/resources/example.gif b/packages/codemirror-graphql/resources/example.gif new file mode 100644 index 00000000000..93569b40158 Binary files /dev/null and b/packages/codemirror-graphql/resources/example.gif differ diff --git a/packages/codemirror-graphql/src/__tests__/hint-test.ts b/packages/codemirror-graphql/src/__tests__/hint-test.ts new file mode 100644 index 00000000000..c71d70a0798 --- /dev/null +++ b/packages/codemirror-graphql/src/__tests__/hint-test.ts @@ -0,0 +1,1123 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 'codemirror/addon/hint/show-hint'; +import { + GraphQLBoolean, + GraphQLFloat, + GraphQLID, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLString, + __Schema, + __Type, +} from 'graphql'; +import '../hint'; +import type { GraphQLHintOptions, IHint, IHints } from '../hint'; +import '../mode'; +import { + TestEnum, + TestInputObject, + TestSchema, + TestType, + TestUnion, + UnionFirst, + UnionSecond, +} from './testSchema'; + +function createEditorWithHint() { + return CodeMirror(document.createElement('div'), { + mode: 'graphql', + hintOptions: { + schema: TestSchema, + closeOnUnfocus: false, + completeSingle: false, + externalFragments: 'fragment Example on Test { id }', + }, + }); +} + +function getHintSuggestions(queryString: string, cursor: CodeMirror.Position) { + const editor = createEditorWithHint(); + + return new Promise(resolve => { + const graphqlHint = CodeMirror.hint.graphql; + CodeMirror.hint.graphql = ( + cm: CodeMirror.Editor, + options: GraphQLHintOptions, + ) => { + const result = graphqlHint(cm, options); + resolve(result); + CodeMirror.hint.graphql = graphqlHint; + return result; + }; + + editor.doc.setValue(queryString); + editor.doc.setCursor(cursor); + editor.execCommand('autocomplete'); + }); +} + +function getExpectedSuggestions(list: IHint[]) { + return list.map(item => ({ + text: item.text, + type: item.type, + description: item.description, + isDeprecated: item.isDeprecated, + deprecationReason: item.deprecationReason, + })); +} + +describe('graphql-hint', () => { + it('attaches a GraphQL hint function with correct mode/hint options', () => { + const editor = createEditorWithHint(); + expect(editor.getHelpers(editor.getCursor(), 'hint')).not.toHaveLength(0); + }); + + it('provides correct initial keywords', async () => { + const suggestions = await getHintSuggestions('', { line: 0, ch: 0 }); + const list = [ + { text: 'query' }, + { text: 'mutation' }, + { text: 'subscription' }, + { text: 'fragment' }, + { text: '{' }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct initial keywords after filtered', async () => { + const suggestions = await getHintSuggestions('q', { line: 0, ch: 1 }); + const list = [{ text: '{' }, { text: 'query' }]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct field name suggestions', async () => { + const suggestions = await getHintSuggestions('{ ', { line: 0, ch: 2 }); + const list = [ + { + text: 'test', + type: TestType, + isDeprecated: false, + }, + { + text: 'union', + type: TestUnion, + isDeprecated: false, + }, + { + text: 'first', + type: UnionFirst, + isDeprecated: false, + }, + { + text: 'id', + type: GraphQLInt, + isDeprecated: false, + }, + { + text: 'isTest', + type: GraphQLBoolean, + isDeprecated: false, + }, + { + text: 'hasArgs', + type: GraphQLString, + isDeprecated: false, + }, + { + text: '__typename', + type: new GraphQLNonNull(GraphQLString), + description: 'The name of the current Object type at runtime.', + isDeprecated: false, + }, + { + text: '__schema', + type: new GraphQLNonNull(__Schema), + description: 'Access the current type schema of this server.', + isDeprecated: false, + }, + { + text: '__type', + type: __Type, + description: 'Request the type information of a single type.', + isDeprecated: false, + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct field name suggestions after filtered', async () => { + const suggestions = await getHintSuggestions('{ i', { line: 0, ch: 3 }); + const list = [ + { + text: 'id', + type: GraphQLInt, + isDeprecated: false, + }, + { + text: 'isTest', + type: GraphQLBoolean, + isDeprecated: false, + }, + { + text: 'union', + type: TestUnion, + isDeprecated: false, + }, + { + text: 'first', + type: UnionFirst, + isDeprecated: false, + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct field name suggestions when using aliases', async () => { + const suggestions = await getHintSuggestions('{ aliasTest: first { ', { + line: 0, + ch: 21, + }); + const list = [ + { + text: 'scalar', + type: GraphQLString, + isDeprecated: false, + }, + { + text: 'first', + type: TestType, + isDeprecated: false, + }, + { + text: 'example', + type: GraphQLString, + isDeprecated: false, + }, + { + text: '__typename', + type: new GraphQLNonNull(GraphQLString), + description: 'The name of the current Object type at runtime.', + isDeprecated: false, + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct field name suggestion indentation', async () => { + const suggestions = await getHintSuggestions('{\n ', { line: 1, ch: 2 }); + expect(suggestions?.from).toEqual({ line: 1, ch: 2, sticky: null }); + expect(suggestions?.to).toEqual({ line: 1, ch: 2, sticky: null }); + }); + + it('provides correct argument suggestions', async () => { + const suggestions = await getHintSuggestions('{ hasArgs ( ', { + line: 0, + ch: 12, + }); + const list = [ + { + text: 'string', + type: GraphQLString, + }, + { + text: 'int', + type: GraphQLInt, + }, + { + text: 'float', + type: GraphQLFloat, + }, + { + text: 'boolean', + type: GraphQLBoolean, + }, + { + text: 'id', + type: GraphQLID, + }, + { + text: 'enum', + type: TestEnum, + }, + { + text: 'object', + type: TestInputObject, + }, + { + text: 'listString', + type: new GraphQLList(GraphQLString), + }, + { + text: 'listInt', + type: new GraphQLList(GraphQLInt), + }, + { + text: 'listFloat', + type: new GraphQLList(GraphQLFloat), + }, + { + text: 'listBoolean', + type: new GraphQLList(GraphQLBoolean), + }, + { + text: 'listID', + type: new GraphQLList(GraphQLID), + }, + { + text: 'listEnum', + type: new GraphQLList(TestEnum), + }, + { + text: 'listObject', + type: new GraphQLList(TestInputObject), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct argument suggestions after filtered', async () => { + const suggestions = await getHintSuggestions('{ hasArgs ( f', { + line: 0, + ch: 13, + }); + const list = [ + { + text: 'float', + type: GraphQLFloat, + }, + { + text: 'listFloat', + type: new GraphQLList(GraphQLFloat), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct argument suggestions when using aliases', async () => { + const suggestions = await getHintSuggestions('{ aliasTest: hasArgs ( ', { + line: 0, + ch: 23, + }); + const list = [ + { + text: 'string', + type: GraphQLString, + }, + { + text: 'int', + type: GraphQLInt, + }, + { + text: 'float', + type: GraphQLFloat, + }, + { + text: 'boolean', + type: GraphQLBoolean, + }, + { + text: 'id', + type: GraphQLID, + }, + { + text: 'enum', + type: TestEnum, + }, + { + text: 'object', + type: TestInputObject, + }, + { + text: 'listString', + type: new GraphQLList(GraphQLString), + }, + { + text: 'listInt', + type: new GraphQLList(GraphQLInt), + }, + { + text: 'listFloat', + type: new GraphQLList(GraphQLFloat), + }, + { + text: 'listBoolean', + type: new GraphQLList(GraphQLBoolean), + }, + { + text: 'listID', + type: new GraphQLList(GraphQLID), + }, + { + text: 'listEnum', + type: new GraphQLList(TestEnum), + }, + { + text: 'listObject', + type: new GraphQLList(TestInputObject), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct directive suggestions', async () => { + const suggestions = await getHintSuggestions('{ test (@', { + line: 0, + ch: 9, + }); + const list = [ + { + text: 'include', + description: + 'Directs the executor to include this field or fragment only when the `if` argument is true.', + }, + { + text: 'skip', + description: + 'Directs the executor to skip this field or fragment when the `if` argument is true.', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct directive suggestion after filtered', async () => { + const suggestions = await getHintSuggestions('{ test (@s', { + line: 0, + ch: 10, + }); + const list = [ + { + text: 'skip', + description: + 'Directs the executor to skip this field or fragment when the `if` argument is true.', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct directive suggestions when using aliases', async () => { + const suggestions = await getHintSuggestions('{ aliasTest: test (@', { + line: 0, + ch: 20, + }); + const list = [ + { + text: 'include', + description: + 'Directs the executor to include this field or fragment only when the `if` argument is true.', + }, + { + text: 'skip', + description: + 'Directs the executor to skip this field or fragment when the `if` argument is true.', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct directive suggestions on definitions', async () => { + const suggestions = await getHintSuggestions('type Type @', { + line: 0, + ch: 11, + }); + const list = [ + { + text: 'onAllDefs', + description: '', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct directive suggestions on args definitions', async () => { + const suggestions = await getHintSuggestions( + 'type Type { field(arg: String @', + { line: 0, ch: 31 }, + ); + const list = [ + { + text: 'deprecated', + description: + 'Marks an element of a GraphQL schema as no longer supported.', + }, + { + text: 'onArg', + description: '', + }, + { + text: 'onAllDefs', + description: '', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides interface suggestions for type when using implements keyword', async () => { + const suggestions = await getHintSuggestions('type Type implements ', { + line: 0, + ch: 21, + }); + const list = [ + { + text: 'TestInterface', + type: TestSchema.getType('TestInterface'), + }, + { + text: 'AnotherTestInterface', + type: TestSchema.getType('AnotherTestInterface'), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides interface suggestions for interface when using implements keyword', async () => { + const suggestions = await getHintSuggestions( + 'interface MyInt implements An', + { line: 0, ch: 29 }, + ); + const list = [ + { + text: 'AnotherTestInterface', + type: TestSchema.getType('AnotherTestInterface'), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides interface suggestions for interface when using implements keyword and multiple interfaces', async () => { + const suggestions = await getHintSuggestions( + 'interface MyInt implements AnotherTestInterface & T', + { line: 0, ch: 51 }, + ); + const list = [ + { + text: 'TestInterface', + type: TestSchema.getType('TestInterface'), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct typeCondition suggestions', async () => { + const suggestions = await getHintSuggestions('{ union { ... on ', { + line: 0, + ch: 17, + }); + const list = [ + { + text: 'First', + description: '', + }, + { + text: 'Second', + description: '', + }, + { + text: 'TestInterface', + description: '', + }, + { + text: 'AnotherTestInterface', + description: '', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct typeCondition suggestions after filtered', async () => { + const suggestions = await getHintSuggestions('{ union { ... on F', { + line: 0, + ch: 18, + }); + const list = [ + { + text: 'First', + description: '', + }, + { + text: 'TestInterface', + description: '', + }, + { + text: 'AnotherTestInterface', + description: '', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct typeCondition suggestions on fragment', async () => { + const suggestions = await getHintSuggestions('fragment Foo on ', { + line: 0, + ch: 16, + }); + const list = [ + { + text: 'Test', + description: '', + }, + { + text: 'TestUnion', + description: '', + }, + { + text: 'First', + description: '', + }, + { + text: 'TestInterface', + description: '', + }, + { + text: 'AnotherTestInterface', + description: '', + }, + { + text: 'Second', + description: '', + }, + { + text: 'MutationType', + description: 'This is a simple mutation type', + }, + { + text: 'SubscriptionType', + description: 'This is a simple subscription type', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct ENUM suggestions', async () => { + const suggestions = await getHintSuggestions('{ hasArgs (enum: ', { + line: 0, + ch: 17, + }); + const list = [ + { + text: 'RED', + type: TestEnum, + isDeprecated: false, + }, + { + text: 'GREEN', + type: TestEnum, + isDeprecated: false, + }, + { + text: 'BLUE', + type: TestEnum, + isDeprecated: false, + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct testInput suggestions', async () => { + const suggestions = await getHintSuggestions('{ hasArgs (object: { ', { + line: 0, + ch: 21, + }); + const list = [ + { + text: 'string', + type: GraphQLString, + }, + { + text: 'int', + type: GraphQLInt, + }, + { + text: 'float', + type: GraphQLFloat, + }, + { + text: 'boolean', + type: GraphQLBoolean, + }, + { + text: 'id', + type: GraphQLID, + }, + { + text: 'enum', + type: TestEnum, + }, + { + text: 'object', + type: TestInputObject, + }, + { + text: 'listString', + type: new GraphQLList(GraphQLString), + }, + { + text: 'listInt', + type: new GraphQLList(GraphQLInt), + }, + { + text: 'listFloat', + type: new GraphQLList(GraphQLFloat), + }, + { + text: 'listBoolean', + type: new GraphQLList(GraphQLBoolean), + }, + { + text: 'listID', + type: new GraphQLList(GraphQLID), + }, + { + text: 'listEnum', + type: new GraphQLList(TestEnum), + }, + { + text: 'listObject', + type: new GraphQLList(TestInputObject), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct object field suggestions after filtered', async () => { + const suggestions = await getHintSuggestions('{ hasArgs (object: { f', { + line: 0, + ch: 22, + }); + const list = [ + { + text: 'float', + type: GraphQLFloat, + }, + { + text: 'listFloat', + type: new GraphQLList(GraphQLFloat), + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides fragment name suggestion', async () => { + const suggestions = await getHintSuggestions( + 'fragment Foo on Test { id } query { ...', + { line: 0, ch: 40 }, + ); + const list = [ + { + text: 'Foo', + type: TestType, + description: 'fragment Foo on Test', + }, + { + text: 'Example', + type: TestType, + description: 'fragment Example on Test', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides fragment names for fragments defined lower', async () => { + const suggestions = await getHintSuggestions( + 'query { ... }\nfragment Foo on Test { id }', + { line: 0, ch: 11 }, + ); + const list = [ + { + text: 'Foo', + type: TestType, + description: 'fragment Foo on Test', + }, + { + text: 'Example', + type: TestType, + description: 'fragment Example on Test', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides only appropriate fragment names', async () => { + const suggestions = await getHintSuggestions( + 'fragment Foo on TestUnion { ... } ' + + 'fragment Bar on First { name } ' + + 'fragment Baz on Second { name } ' + + 'fragment Qux on TestUnion { name } ' + + 'fragment Nrf on Test { id } ' + + 'fragment Quux on TestInputObject { string } ' + + 'fragment Abc on Xyz { abcdef }', + { line: 0, ch: 31 }, + ); + const list = [ + { + text: 'Bar', + type: UnionFirst, + description: 'fragment Bar on First', + }, + { + text: 'Baz', + type: UnionSecond, + description: 'fragment Baz on Second', + }, + { + text: 'Qux', + type: TestUnion, + description: 'fragment Qux on TestUnion', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct field name suggestion inside inline fragment', async () => { + const suggestions = await getHintSuggestions( + 'fragment Foo on TestUnion { ... on First { ', + { line: 0, ch: 43 }, + ); + const list = [ + { + text: 'scalar', + type: GraphQLString, + isDeprecated: false, + }, + { + text: 'first', + type: TestType, + isDeprecated: false, + }, + { + text: 'example', + type: GraphQLString, + isDeprecated: false, + }, + { + text: '__typename', + type: new GraphQLNonNull(GraphQLString), + description: 'The name of the current Object type at runtime.', + isDeprecated: false, + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct field name suggestion inside type-less inline fragment', async () => { + const suggestions = await getHintSuggestions( + 'fragment Foo on First { ... { ', + { line: 0, ch: 30 }, + ); + const list = [ + { + text: 'scalar', + type: GraphQLString, + isDeprecated: false, + }, + { + text: 'first', + type: TestType, + isDeprecated: false, + }, + { + text: 'example', + type: GraphQLString, + isDeprecated: false, + }, + { + text: '__typename', + type: new GraphQLNonNull(GraphQLString), + description: 'The name of the current Object type at runtime.', + isDeprecated: false, + }, + ]; + + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct boolean suggestions', async () => { + const suggestions1 = await getHintSuggestions('{ hasArgs(listBoolean: [ ', { + line: 0, + ch: 27, + }); + const list1 = [ + { + text: 'true', + type: GraphQLBoolean, + description: 'Not false.', + }, + { + text: 'false', + type: GraphQLBoolean, + description: 'Not true.', + }, + ]; + const expectedSuggestions1 = getExpectedSuggestions(list1); + expect(suggestions1?.list).toEqual(expectedSuggestions1); + + const suggestions2 = await getHintSuggestions( + '{ hasArgs(object: { boolean: t', + { line: 0, ch: 30 }, + ); + const list2 = [ + { + text: 'true', + type: GraphQLBoolean, + description: 'Not false.', + }, + ]; + const expectedSuggestions2 = getExpectedSuggestions(list2); + expect(suggestions2?.list).toEqual(expectedSuggestions2); + + const suggestions3 = await getHintSuggestions('{ hasArgs(boolean: f', { + line: 0, + ch: 20, + }); + const list3 = [ + { + text: 'false', + type: GraphQLBoolean, + description: 'Not true.', + }, + ]; + const expectedSuggestions3 = getExpectedSuggestions(list3); + expect(suggestions3?.list).toEqual(expectedSuggestions3); + }); + + it('provides correct variable type suggestions', async () => { + const suggestions = await getHintSuggestions('query($foo: ', { + line: 0, + ch: 12, + }); + const list = [ + { + text: 'String', + description: + 'The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.', + }, + { + text: 'Int', + description: + 'The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.', + }, + { + text: 'Boolean', + description: 'The `Boolean` scalar type represents `true` or `false`.', + }, + { + text: 'Float', + description: + 'The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).', + }, + { + text: 'ID', + description: + 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.', + }, + { text: 'TestEnum' }, + { text: 'TestInput' }, + { + text: '__TypeKind', + description: + 'An enum describing what kind of type a given `__Type` is.', + }, + { + text: '__DirectiveLocation', + description: + 'A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + + it('provides correct variable type suggestions inside list type', async () => { + const suggestions = await getHintSuggestions('query($foo: [ ', { + line: 0, + ch: 14, + }); + const list = [ + { + text: 'String', + description: + 'The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.', + }, + { + text: 'Int', + description: + 'The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.', + }, + { + text: 'Boolean', + description: 'The `Boolean` scalar type represents `true` or `false`.', + }, + { + text: 'Float', + description: + 'The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).', + }, + { + text: 'ID', + description: + 'The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.', + }, + { text: 'TestEnum' }, + { text: 'TestInput' }, + { + text: '__TypeKind', + description: + 'An enum describing what kind of type a given `__Type` is.', + }, + { + text: '__DirectiveLocation', + description: + 'A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.', + }, + ]; + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); + it('provides no suggestions', async () => { + const list: IHint[] = []; + const expectedSuggestions = getExpectedSuggestions(list); + + // kind is FragmentSpread, step is 2 + const suggestions1 = await getHintSuggestions( + 'fragment Foo on Test { id } query { ...Foo ', + { line: 0, ch: 45 }, + ); + expect(suggestions1?.list).toEqual(expectedSuggestions); + + // kind is ListType, step is 3 + const suggestions2 = await getHintSuggestions('query($foo: [string] ', { + line: 0, + ch: 21, + }); + expect(suggestions2?.list).toEqual(expectedSuggestions); + + // kind is ListValue, step is 1 + const suggestions3 = await getHintSuggestions( + '{ hasArgs(listString: ["foo" ', + { + line: 0, + ch: 29, + }, + ); + expect(suggestions3?.list).toEqual(expectedSuggestions); + + // kind is VariableDefinition, step is 1 + const suggestions4 = await getHintSuggestions('query($foo ', { + line: 0, + ch: 11, + }); + expect(suggestions4?.list).toEqual(expectedSuggestions); + + // kind is Argument, step is 1 + const suggestions5 = await getHintSuggestions('{ hasArgs(string ', { + line: 0, + ch: 17, + }); + expect(suggestions5?.list).toEqual(expectedSuggestions); + + // kind is Argument, step is 2, and input type isn't GraphQLEnumType or GraphQLBoolean + const suggestions6 = await getHintSuggestions('{ hasArgs(string: ', { + line: 0, + ch: 18, + }); + expect(suggestions6?.list).toEqual(expectedSuggestions); + + const suggestions7 = await getHintSuggestions( + '{ hasArgs(object: { string ', + { line: 0, ch: 27 }, + ); + expect(suggestions7?.list).toEqual(expectedSuggestions); + }); + it('provides variable completion for arguments', async () => { + const expectedSuggestions = getExpectedSuggestions([ + { text: 'string', type: GraphQLString }, + { text: 'listString', type: new GraphQLList(GraphQLString) }, + ]); + // kind is Argument, step is 2, and input type isn't GraphQLEnumType or GraphQLBoolean + const suggestions9 = await getHintSuggestions( + 'query myQuery($arg: String){ hasArgs(string: ', + { + line: 0, + ch: 42, + }, + ); + expect(suggestions9?.list).toEqual(expectedSuggestions); + }); + it('provides variable completion for arguments with $', async () => { + const expectedSuggestions = getExpectedSuggestions([ + { text: 'string', type: GraphQLString }, + { text: 'listString', type: new GraphQLList(GraphQLString) }, + ]); + // kind is Argument, step is 2, and input type isn't GraphQLEnumType or GraphQLBoolean + const suggestions9 = await getHintSuggestions( + 'query myQuery($arg: String){ hasArgs(string: $', + { + line: 0, + ch: 42, + }, + ); + expect(suggestions9?.list).toEqual(expectedSuggestions); + }); + it('provides correct field name suggestions for an interface type', async () => { + const suggestions = await getHintSuggestions( + '{ first { ... on TestInterface { ', + { + line: 0, + ch: 33, + }, + ); + const list = [ + { + text: 'scalar', + type: GraphQLString, + isDeprecated: false, + }, + { + description: 'The name of the current Object type at runtime.', + isDeprecated: false, + text: '__typename', + type: new GraphQLNonNull(GraphQLString), + deprecationReason: undefined, + }, + ]; + + const expectedSuggestions = getExpectedSuggestions(list); + expect(suggestions?.list).toEqual(expectedSuggestions); + }); +}); diff --git a/packages/codemirror-graphql/src/__tests__/kitchen-sink.graphql b/packages/codemirror-graphql/src/__tests__/kitchen-sink.graphql new file mode 100644 index 00000000000..dfd00d08399 --- /dev/null +++ b/packages/codemirror-graphql/src/__tests__/kitchen-sink.graphql @@ -0,0 +1,53 @@ +# Copyright (c) 2021 GraphQL Contributors +# 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 + ... @skip(if: false) { + id + } + ... { + id + } +} + +mutation mutationName { + setString(value: "newString") +} + +subscription subscriptionName { + subscribeToTest(id: "anId") { + ... on Test { + id + } + } +} + +fragment frag on Test { + test @include(if: true) { + union { + __typename + } + } +} diff --git a/packages/codemirror-graphql/src/__tests__/lint-test.ts b/packages/codemirror-graphql/src/__tests__/lint-test.ts new file mode 100644 index 00000000000..0df6a8a3c9e --- /dev/null +++ b/packages/codemirror-graphql/src/__tests__/lint-test.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 'codemirror/addon/lint/lint'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { GraphQLError, OperationDefinitionNode } from 'graphql'; +import '../lint'; +import '../mode'; +import { TestSchema } from './testSchema'; + +function createEditorWithLint(lintConfig?: any) { + return CodeMirror(document.createElement('div'), { + mode: 'graphql', + lint: lintConfig || true, + }); +} + +function printLintErrors(queryString: string, configOverrides = {}) { + const editor = createEditorWithLint({ + schema: TestSchema, + ...configOverrides, + }); + + return new Promise(resolve => { + editor.state.lint.options.onUpdateLinting = (errors: any[]) => { + if (errors?.[0] && !errors[0].message.match('Unexpected EOF')) { + resolve(errors); + return; + } + resolve([]); + }; + editor.doc.setValue(queryString); + }); +} + +describe('graphql-lint', () => { + it('attaches a GraphQL lint function with correct mode/lint options', () => { + const editor = createEditorWithLint(); + expect(editor.getHelpers(editor.getCursor(), 'lint')).not.toHaveLength(0); + }); + + const kitchenSink = readFileSync( + join(__dirname, '/kitchen-sink.graphql'), + 'utf8', + ); + + it('returns no syntactic/validation errors after parsing kitchen-sink query', async () => { + const errors = await printLintErrors(kitchenSink); + expect(errors).toHaveLength(0); + }); + + it('returns a validation error for a invalid query', async () => { + const noMutationOperationRule = (context: any) => ({ + OperationDefinition(node: OperationDefinitionNode) { + if (node.operation === 'mutation') { + context.reportError(new GraphQLError('I like turtles.', node)); + } + return false; + }, + }); + const errors = await printLintErrors(kitchenSink, { + validationRules: [noMutationOperationRule], + }); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('I like turtles.'); + }); +}); diff --git a/packages/codemirror-graphql/src/__tests__/mode-test.ts b/packages/codemirror-graphql/src/__tests__/mode-test.ts new file mode 100644 index 00000000000..ad640aa60eb --- /dev/null +++ b/packages/codemirror-graphql/src/__tests__/mode-test.ts @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 'codemirror/addon/runmode/runmode'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import '../mode'; + +describe('graphql-mode', () => { + it('provides correct tokens and styles after parsing', () => { + const queryStr = 'query name { }'; + const tokens: string[] = []; + const styles: string[] = []; + + CodeMirror.runMode(queryStr, 'graphql', (token, style) => { + if (style && style !== 'ws') { + tokens.push(token); + styles.push(style); + } + }); + + expect(tokens).toEqual(['query', 'name', '{', '}']); + expect(styles).toEqual(['keyword', 'def', 'punctuation', 'punctuation']); + }); + + it('parses Relay-style anonymous FragmentDefinitions', () => { + CodeMirror.runMode('fragment on Test { id }', 'graphql', (_token, style) => + expect(style).not.toBe('invalidchar'), + ); + }); + + it('parses inline fragments with optional syntax correctly', () => { + CodeMirror.runMode( + '{ ... on OptionalType { name } }', + 'graphql', + (_token, style) => expect(style).not.toBe('invalidchar'), + ); + + CodeMirror.runMode('{ ... { name } }', 'graphql', (_token, style) => + expect(style).not.toBe('invalidchar'), + ); + + CodeMirror.runMode( + '{ ... @optionalDirective { name } }', + 'graphql', + (_token, style) => expect(style).not.toBe('invalidchar'), + ); + }); + + it('returns "invalidchar" message when there is no matching token', () => { + CodeMirror.runMode('invalidKeyword name', 'graphql', (token, style) => { + if (token.trim()) { + expect(style).toBe('invalidchar'); + } + }); + + CodeMirror.runMode('query %', 'graphql', (token, style) => { + if (token === '%') { + expect(style).toBe('invalidchar'); + } + }); + }); + + it('parses kitchen-sink query without invalidchar', () => { + const kitchenSink = readFileSync( + join(__dirname, '/kitchen-sink.graphql'), + 'utf8', + ); + + CodeMirror.runMode(kitchenSink, 'graphql', (_token, style) => { + expect(style).not.toBe('invalidchar'); + }); + }); + + it('parses schema-kitchen-sink query without invalidchar', () => { + const schemaKitchenSink = readFileSync( + join(__dirname, '/schema-kitchen-sink.graphql'), + 'utf8', + ); + + CodeMirror.runMode(schemaKitchenSink, 'graphql', (_token, style) => { + expect(style).not.toBe('invalidchar'); + }); + }); + + it('parses anonymous operations without invalidchar', () => { + CodeMirror.runMode('{ id }', 'graphql', (_token, style) => { + expect(style).not.toBe('invalidchar'); + }); + + CodeMirror.runMode( + ` + mutation { + setString(value: "newString") + } + `, + 'graphql', + (_token, style) => { + expect(style).not.toBe('invalidchar'); + }, + ); + + CodeMirror.runMode( + ` + subscription { + subscribeToTest(id: "anId") { + id + } + } + `, + 'graphql', + (_token, style) => { + expect(style).not.toBe('invalidchar'); + }, + ); + }); +}); diff --git a/packages/codemirror-graphql/src/__tests__/schema-kitchen-sink.graphql b/packages/codemirror-graphql/src/__tests__/schema-kitchen-sink.graphql new file mode 100644 index 00000000000..929ea307043 --- /dev/null +++ b/packages/codemirror-graphql/src/__tests__/schema-kitchen-sink.graphql @@ -0,0 +1,75 @@ +# Copyright (c) 2021 GraphQL Contributors +# 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. + +schema { + query: QueryType + mutation: MutationType +} + +type Foo implements Bar { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type +} + +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField +} + +interface Bar { + one: Type + four(argument: String = "string"): String +} + +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArg): Type @onField +} + +union Feed = Story | Article | Advert + +union AnnotatedUnion @onUnion = A | B + +scalar CustomScalar + +scalar AnnotatedScalar @onScalar + +enum Site { + DESKTOP + MOBILE +} + +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE +} + +input InputType { + key: String! + answer: Int = 42 +} + +input AnnotatedInput @onInputObjectType { + annotatedField: Type @onField +} + +extend type Foo { + seven(argument: [[String!]!]!): Type +} + +extend type Foo @onType {} + +type NoFields {} + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) + on FIELD + | FRAGMENT_SPREAD + | INLINE_FRAGMENT diff --git a/packages/codemirror-graphql/src/__tests__/testSchema.ts b/packages/codemirror-graphql/src/__tests__/testSchema.ts new file mode 100644 index 00000000000..e0ed060bb28 --- /dev/null +++ b/packages/codemirror-graphql/src/__tests__/testSchema.ts @@ -0,0 +1,240 @@ +/* istanbul ignore file */ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + DirectiveLocation, + GraphQLBoolean, + GraphQLDeprecatedDirective, + GraphQLDirective, + GraphQLEnumType, + GraphQLFloat, + GraphQLID, + GraphQLIncludeDirective, + GraphQLInputObjectType, + GraphQLInt, + GraphQLInterfaceType, + GraphQLList, + GraphQLObjectType, + GraphQLSchema, + GraphQLSkipDirective, + GraphQLString, + GraphQLUnionType, +} from 'graphql'; + +// Test Schema + +export const TestEnum = new GraphQLEnumType({ + name: 'TestEnum', + values: { + RED: {}, + GREEN: {}, + BLUE: {}, + }, +}); + +export const TestInputObject: GraphQLInputObjectType = + 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) }, + }), + }); + +const TestInterface: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: 'TestInterface', + resolveType: () => UnionFirst, + fields: { + scalar: { + type: GraphQLString, + resolve: () => ({}), + }, + }, +}); + +const AnotherTestInterface: GraphQLInterfaceType = new GraphQLInterfaceType({ + name: 'AnotherTestInterface', + resolveType: () => UnionFirst, + fields: { + example: { + type: GraphQLString, + resolve: () => ({}), + }, + }, +}); + +export const UnionFirst = new GraphQLObjectType({ + name: 'First', + interfaces: [TestInterface, AnotherTestInterface], + fields: () => ({ + scalar: { + type: GraphQLString, + resolve: () => ({}), + }, + first: { + type: TestType, + resolve: () => ({}), + }, + example: { + type: GraphQLString, + resolve: () => ({}), + }, + }), +}); + +export const UnionSecond = new GraphQLObjectType({ + name: 'Second', + fields: () => ({ + second: { + type: TestType, + resolve: () => ({}), + }, + }), +}); + +export const TestUnion = new GraphQLUnionType({ + name: 'TestUnion', + types: [UnionFirst, UnionSecond], + resolveType() { + return UnionFirst; + }, +}); + +export const TestType: GraphQLObjectType = new GraphQLObjectType({ + name: 'Test', + fields: () => ({ + test: { + type: TestType, + resolve: () => ({}), + }, + deprecatedTest: { + type: TestType, + deprecationReason: 'Use test instead.', + resolve: () => ({}), + }, + union: { + type: TestUnion, + resolve: () => ({}), + }, + first: { + type: UnionFirst, + 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) }, + }, + }, + }), +}); + +const 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 TestSubscriptionType = new GraphQLObjectType({ + name: 'SubscriptionType', + description: 'This is a simple subscription type', + fields: { + subscribeToTest: { + type: TestType, + description: 'Subscribe to the test type', + args: { + id: { type: GraphQLString }, + }, + }, + }, +}); + +const OnArgDirective = new GraphQLDirective({ + name: 'onArg', + locations: [DirectiveLocation.ARGUMENT_DEFINITION], +}); + +const OnAllDefsDirective = new GraphQLDirective({ + name: 'onAllDefs', + locations: [ + DirectiveLocation.SCHEMA, + DirectiveLocation.SCALAR, + DirectiveLocation.OBJECT, + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.INTERFACE, + DirectiveLocation.UNION, + DirectiveLocation.ENUM, + DirectiveLocation.ENUM_VALUE, + DirectiveLocation.INPUT_OBJECT, + DirectiveLocation.ARGUMENT_DEFINITION, + DirectiveLocation.INPUT_FIELD_DEFINITION, + ], +}); + +export const TestSchema = new GraphQLSchema({ + query: TestType, + mutation: TestMutationType, + subscription: TestSubscriptionType, + directives: [ + GraphQLIncludeDirective, + GraphQLSkipDirective, + GraphQLDeprecatedDirective, + OnArgDirective, + OnAllDefsDirective, + ], +}); diff --git a/packages/codemirror-graphql/src/cm6-legacy/mode.ts b/packages/codemirror-graphql/src/cm6-legacy/mode.ts new file mode 100644 index 00000000000..31d285d228b --- /dev/null +++ b/packages/codemirror-graphql/src/cm6-legacy/mode.ts @@ -0,0 +1,6 @@ +import type { StreamParser } from '@codemirror/language'; +import graphqlModeFactory from '../utils/mode-factory'; + +// Types of property 'token' are incompatible. +// Type '((stream: StringStream, state: any) => string | null) | undefined' is not comparable to type '(stream: StringStream, state: any) => string | null'. +export const graphql = graphqlModeFactory({}) as unknown as StreamParser; diff --git a/packages/codemirror-graphql/src/hint.ts b/packages/codemirror-graphql/src/hint.ts new file mode 100644 index 00000000000..a3a0ae0e308 --- /dev/null +++ b/packages/codemirror-graphql/src/hint.ts @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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, { Hints, Hint } from 'codemirror'; +import 'codemirror/addon/hint/show-hint'; + +import { FragmentDefinitionNode, GraphQLSchema, GraphQLType } from 'graphql'; +import type { Maybe } from 'graphql-language-service'; +import { getAutocompleteSuggestions, Position } from 'graphql-language-service'; + +export interface GraphQLHintOptions { + schema?: GraphQLSchema; + externalFragments?: string | FragmentDefinitionNode[]; +} + +interface IHint extends Hint { + isDeprecated?: boolean; + type?: Maybe; + description?: Maybe; + deprecationReason?: Maybe; +} + +interface IHints extends Hints { + list: IHint[]; +} + +declare module 'codemirror' { + interface ShowHintOptions { + schema?: GraphQLSchema; + externalFragments?: string | FragmentDefinitionNode[]; + } + + interface CodeMirrorHintMap { + graphql: ( + editor: CodeMirror.Editor, + options: GraphQLHintOptions, + ) => IHints | undefined; + } +} + +/** + * Registers a "hint" helper for CodeMirror. + * + * Using CodeMirror's "hint" addon: https://codemirror.net/demo/complete.html + * Given an editor, this helper will take the token at the cursor and return a + * list of suggested tokens. + * + * Options: + * + * - schema: GraphQLSchema provides the hinter with positionally relevant info + * + * Additional Events: + * + * - hasCompletion (codemirror, data, token) - signaled when the hinter has a + * new list of completion suggestions. + * + */ +CodeMirror.registerHelper( + 'hint', + 'graphql', + ( + editor: CodeMirror.Editor, + options: GraphQLHintOptions, + ): IHints | undefined => { + const { schema, externalFragments } = options; + if (!schema) { + return; + } + + const cur = editor.getCursor(); + const token = editor.getTokenAt(cur); + + const tokenStart = + token.type !== null && /"|\w/.test(token.string[0]) + ? token.start + : token.end; + + const position = new Position(cur.line, tokenStart); + + const rawResults = getAutocompleteSuggestions( + schema, + editor.getValue(), + position, + token, + externalFragments, + ); + + const results = { + list: rawResults.map(item => ({ + text: item.label, + type: item.type, + description: item.documentation, + isDeprecated: item.isDeprecated, + deprecationReason: item.deprecationReason, + })), + from: { line: cur.line, ch: tokenStart }, + to: { line: cur.line, ch: token.end }, + }; + + if (results?.list && results.list.length > 0) { + results.from = CodeMirror.Pos(results.from.line, results.from.ch); + results.to = CodeMirror.Pos(results.to.line, results.to.ch); + CodeMirror.signal(editor, 'hasCompletion', editor, results, token); + } + + return results; + }, +); +// exporting here so we don't need to import the codemirror show-hint addon module (and its implementation) +export type { IHint, IHints }; diff --git a/packages/codemirror-graphql/src/index.d.ts b/packages/codemirror-graphql/src/index.d.ts new file mode 100644 index 00000000000..7866c87f5a5 --- /dev/null +++ b/packages/codemirror-graphql/src/index.d.ts @@ -0,0 +1,19 @@ +import 'codemirror/addon/hint/show-hint'; + +declare module 'codemirror' { + let Init: any; + + interface Editor { + doc: CodeMirror.Doc; + getHelper(pos: { line: number; ch: number }, type: string): any; + getHelpers(pos: { line: number; ch: number }, type: string): any[]; + } + + interface ShowHintOptions { + hint?: ShowHintOptions['hint']; + } + + interface CodeMirrorHintMap {} + + const hint: CodeMirrorHintMap; +} diff --git a/packages/codemirror-graphql/src/info.ts b/packages/codemirror-graphql/src/info.ts new file mode 100644 index 00000000000..dedea07e170 --- /dev/null +++ b/packages/codemirror-graphql/src/info.ts @@ -0,0 +1,313 @@ +/* @flow */ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + GraphQLDirective, + GraphQLEnumType, + GraphQLEnumValue, + GraphQLInputField, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLType, +} from 'graphql'; +import CodeMirror from 'codemirror'; + +import getTypeInfo, { TypeInfo } from './utils/getTypeInfo'; +import { + getArgumentReference, + getDirectiveReference, + getEnumValueReference, + getFieldReference, + getTypeReference, + SchemaReference, +} from './utils/SchemaReference'; +import './utils/info-addon'; +import type { Maybe } from 'graphql-language-service'; + +export interface GraphQLInfoOptions { + schema?: GraphQLSchema; + onClick?: Maybe<(ref: Maybe, e: MouseEvent) => void>; + renderDescription?: (str: string) => string; + render?: () => string; +} + +/** + * Registers GraphQL "info" tooltips for CodeMirror. + * + * When hovering over a token, this presents a tooltip explaining it. + * + * Options: + * + * - schema: GraphQLSchema provides positionally relevant info. + * - hoverTime: The number of ms to wait before showing info. (Default 500) + * - renderDescription: Convert a description to some HTML, Useful since + * descriptions are often Markdown formatted. + * - onClick: A function called when a named thing is clicked. + * + */ +CodeMirror.registerHelper( + 'info', + 'graphql', + (token: CodeMirror.Token, options: GraphQLInfoOptions) => { + if (!options.schema || !token.state) { + return; + } + const { kind, step } = token.state; + const typeInfo = getTypeInfo(options.schema, token.state); + + // Given a Schema and a Token, produce the contents of an info tooltip. + // To do this, create a div element that we will render "into" and then pass + // it to various rendering functions. + if ( + (kind === 'Field' && step === 0 && typeInfo.fieldDef) || + (kind === 'AliasedField' && step === 2 && typeInfo.fieldDef) + ) { + const header = document.createElement('div'); + header.className = 'CodeMirror-info-header'; + renderField(header, typeInfo, options); + const into = document.createElement('div'); + into.append(header); + renderDescription(into, options, typeInfo.fieldDef as any); + return into; + } + if (kind === 'Directive' && step === 1 && typeInfo.directiveDef) { + const header = document.createElement('div'); + header.className = 'CodeMirror-info-header'; + renderDirective(header, typeInfo, options); + const into = document.createElement('div'); + into.append(header); + renderDescription(into, options, typeInfo.directiveDef); + return into; + } + if (kind === 'Argument' && step === 0 && typeInfo.argDef) { + const header = document.createElement('div'); + header.className = 'CodeMirror-info-header'; + renderArg(header, typeInfo, options); + const into = document.createElement('div'); + into.append(header); + renderDescription(into, options, typeInfo.argDef); + return into; + } + if ( + kind === 'EnumValue' && + typeInfo.enumValue && + typeInfo.enumValue.description + ) { + const header = document.createElement('div'); + header.className = 'CodeMirror-info-header'; + renderEnumValue(header, typeInfo, options); + const into = document.createElement('div'); + into.append(header); + renderDescription(into, options, typeInfo.enumValue); + return into; + } + if ( + kind === 'NamedType' && + typeInfo.type && + (typeInfo.type as GraphQLObjectType).description + ) { + const header = document.createElement('div'); + header.className = 'CodeMirror-info-header'; + renderType(header, typeInfo, options, typeInfo.type); + const into = document.createElement('div'); + into.append(header); + renderDescription(into, options, typeInfo.type); + return into; + } + }, +); + +function renderField( + into: HTMLElement, + typeInfo: TypeInfo, + options: GraphQLInfoOptions, +) { + renderQualifiedField(into, typeInfo, options); + renderTypeAnnotation(into, typeInfo, options, typeInfo.type); +} + +function renderQualifiedField( + into: HTMLElement, + typeInfo: TypeInfo, + options: GraphQLInfoOptions, +) { + const fieldName = typeInfo.fieldDef?.name || ''; + text(into, fieldName, 'field-name', options, getFieldReference(typeInfo)); +} + +function renderDirective( + into: HTMLElement, + typeInfo: TypeInfo, + options: GraphQLInfoOptions, +) { + const name = '@' + (typeInfo.directiveDef?.name || ''); + text(into, name, 'directive-name', options, getDirectiveReference(typeInfo)); +} + +function renderArg( + into: HTMLElement, + typeInfo: TypeInfo, + options: GraphQLInfoOptions, +) { + const name = typeInfo.argDef?.name || ''; + text(into, name, 'arg-name', options, getArgumentReference(typeInfo)); + renderTypeAnnotation(into, typeInfo, options, typeInfo.inputType); +} + +function renderEnumValue( + into: HTMLElement, + typeInfo: TypeInfo, + options: GraphQLInfoOptions, +) { + const name = typeInfo.enumValue?.name || ''; + renderType(into, typeInfo, options, typeInfo.inputType); + text(into, '.'); + text(into, name, 'enum-value', options, getEnumValueReference(typeInfo)); +} + +function renderTypeAnnotation( + into: HTMLElement, + typeInfo: TypeInfo, + options: GraphQLInfoOptions, + t: Maybe, +) { + const typeSpan = document.createElement('span'); + typeSpan.className = 'type-name-pill'; + if (t instanceof GraphQLNonNull) { + renderType(typeSpan, typeInfo, options, t.ofType); + text(typeSpan, '!'); + } else if (t instanceof GraphQLList) { + text(typeSpan, '['); + renderType(typeSpan, typeInfo, options, t.ofType); + text(typeSpan, ']'); + } else { + text( + typeSpan, + t?.name || '', + 'type-name', + options, + getTypeReference(typeInfo, t), + ); + } + into.append(typeSpan); +} + +function renderType( + into: HTMLElement, + typeInfo: TypeInfo, + options: GraphQLInfoOptions, + t: Maybe, +) { + if (t instanceof GraphQLNonNull) { + renderType(into, typeInfo, options, t.ofType); + text(into, '!'); + } else if (t instanceof GraphQLList) { + text(into, '['); + renderType(into, typeInfo, options, t.ofType); + text(into, ']'); + } else { + text( + into, + t?.name || '', + 'type-name', + options, + getTypeReference(typeInfo, t), + ); + } +} + +function renderDescription( + into: HTMLElement, + options: GraphQLInfoOptions, + def: + | GraphQLInputField + | GraphQLEnumType + | GraphQLDirective + | GraphQLEnumValue + | GraphQLType, +) { + const { description } = def as GraphQLInputField; + if (description) { + const descriptionDiv = document.createElement('div'); + descriptionDiv.className = 'info-description'; + if (options.renderDescription) { + descriptionDiv.innerHTML = options.renderDescription(description); + } else { + descriptionDiv.append(document.createTextNode(description)); + } + into.append(descriptionDiv); + } + + renderDeprecation(into, options, def); +} + +function renderDeprecation( + into: HTMLElement, + options: GraphQLInfoOptions, + def: + | GraphQLInputField + | GraphQLEnumType + | GraphQLDirective + | GraphQLEnumValue + | GraphQLType, +) { + const reason = (def as GraphQLInputField).deprecationReason; + if (reason) { + const deprecationDiv = document.createElement('div'); + deprecationDiv.className = 'info-deprecation'; + into.append(deprecationDiv); + + const label = document.createElement('span'); + label.className = 'info-deprecation-label'; + label.append(document.createTextNode('Deprecated')); + deprecationDiv.append(label); + + const reasonDiv = document.createElement('div'); + reasonDiv.className = 'info-deprecation-reason'; + if (options.renderDescription) { + reasonDiv.innerHTML = options.renderDescription(reason); + } else { + reasonDiv.append(document.createTextNode(reason)); + } + deprecationDiv.append(reasonDiv); + } +} + +function text( + into: HTMLElement, + content: string, + className = '', + options: GraphQLInfoOptions = { onClick: null }, + ref: Maybe = null, +) { + if (className) { + const { onClick } = options; + let node; + if (onClick) { + node = document.createElement('a'); + + // Providing a href forces proper a tag behavior, though we don't actually + // want clicking the node to navigate anywhere. + node.href = 'javascript:void 0'; // eslint-disable-line no-script-url + node.addEventListener('click', (e: MouseEvent) => { + onClick(ref, e); + }); + } else { + node = document.createElement('span'); + } + node.className = className; + node.append(document.createTextNode(content)); + into.append(node); + } else { + into.append(document.createTextNode(content)); + } +} diff --git a/packages/codemirror-graphql/src/jump.ts b/packages/codemirror-graphql/src/jump.ts new file mode 100644 index 00000000000..7a0be9ca03a --- /dev/null +++ b/packages/codemirror-graphql/src/jump.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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. + * + * @flow + * + */ + +import CodeMirror from 'codemirror'; + +import getTypeInfo from './utils/getTypeInfo'; +import { + getArgumentReference, + getDirectiveReference, + getEnumValueReference, + getFieldReference, + getTypeReference, +} from './utils/SchemaReference'; +import './utils/jump-addon'; +import { GraphQLSchema } from 'graphql'; +import type { State } from 'graphql-language-service'; + +export interface GraphQLJumpOptions { + schema?: GraphQLSchema; + onClick?: () => void; + state?: State; +} + +/** + * Registers GraphQL "jump" links for CodeMirror. + * + * When command-hovering over a token, this converts it to a link, which when + * pressed will call the provided onClick handler. + * + * Options: + * + * - schema: GraphQLSchema provides positionally relevant info. + * - onClick: A function called when a named thing is clicked. + * + */ +CodeMirror.registerHelper( + 'jump', + 'graphql', + (token: CodeMirror.Token, options: GraphQLJumpOptions) => { + if (!options.schema || !options.onClick || !token.state) { + return; + } + + // Given a Schema and a Token, produce a "SchemaReference" which refers to + // the particular artifact from the schema (such as a type, field, argument, + // or directive) that token references. + const { state } = token; + const { kind, step } = state; + const typeInfo = getTypeInfo(options.schema, state); + + if ( + (kind === 'Field' && step === 0 && typeInfo.fieldDef) || + (kind === 'AliasedField' && step === 2 && typeInfo.fieldDef) + ) { + return getFieldReference(typeInfo); + } + if (kind === 'Directive' && step === 1 && typeInfo.directiveDef) { + return getDirectiveReference(typeInfo); + } + if (kind === 'Argument' && step === 0 && typeInfo.argDef) { + return getArgumentReference(typeInfo); + } + if (kind === 'EnumValue' && typeInfo.enumValue) { + return getEnumValueReference(typeInfo); + } + if (kind === 'NamedType' && typeInfo.type) { + return getTypeReference(typeInfo); + } + }, +); diff --git a/packages/codemirror-graphql/src/lint.ts b/packages/codemirror-graphql/src/lint.ts new file mode 100644 index 00000000000..0849d03070e --- /dev/null +++ b/packages/codemirror-graphql/src/lint.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { FragmentDefinitionNode, GraphQLSchema, ValidationRule } from 'graphql'; +import { getDiagnostics } from 'graphql-language-service'; + +const SEVERITY = ['error', 'warning', 'information', 'hint']; +const TYPE: Record = { + 'GraphQL: Validation': 'validation', + 'GraphQL: Deprecation': 'deprecation', + 'GraphQL: Syntax': 'syntax', +}; + +interface GraphQLLintOptions { + schema?: GraphQLSchema; + validationRules: ValidationRule[]; + externalFragments?: string | FragmentDefinitionNode[]; +} + +/** + * Registers a "lint" helper for CodeMirror. + * + * Using CodeMirror's "lint" addon: https://codemirror.net/demo/lint.html + * Given the text within an editor, this helper will take that text and return + * a list of linter issues, derived from GraphQL's parse and validate steps. + * Also, this uses `graphql-language-service-parser` to power the diagnostics + * service. + * + * Options: + * + * - schema: GraphQLSchema provides the linter with positionally relevant info + * + */ +CodeMirror.registerHelper( + 'lint', + 'graphql', + (text: string, options: GraphQLLintOptions): CodeMirror.Annotation[] => { + const { schema, validationRules, externalFragments } = options; + const rawResults = getDiagnostics( + text, + schema, + validationRules, + undefined, + externalFragments, + ); + + const results = rawResults.map(error => ({ + message: error.message, + severity: error.severity ? SEVERITY[error.severity - 1] : SEVERITY[0], + type: error.source ? TYPE[error.source] : undefined, + from: CodeMirror.Pos(error.range.start.line, error.range.start.character), + to: CodeMirror.Pos(error.range.end.line, error.range.end.character), + })); + + return results; + }, +); diff --git a/packages/codemirror-graphql/src/mode.ts b/packages/codemirror-graphql/src/mode.ts new file mode 100644 index 00000000000..61a2a47fc14 --- /dev/null +++ b/packages/codemirror-graphql/src/mode.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 modeFactory from './utils/mode-factory'; + +CodeMirror.defineMode('graphql', modeFactory); diff --git a/packages/codemirror-graphql/src/results/__tests__/mode-test.ts b/packages/codemirror-graphql/src/results/__tests__/mode-test.ts new file mode 100644 index 00000000000..96512462843 --- /dev/null +++ b/packages/codemirror-graphql/src/results/__tests__/mode-test.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 'codemirror/addon/runmode/runmode'; +import '../mode'; + +describe('graphql-results-mode', () => { + it('provides correct tokens and styles after parsing', () => { + const queryStr = + '{ "data": { "field": "value" }, "errors": [ { "message": "bork" } ] }'; + const tokens: [string, string][] = []; + + CodeMirror.runMode(queryStr, 'graphql-results', (token, style) => { + if (style && style !== 'ws') { + tokens.push([token, style]); + } + }); + + expect(tokens).toEqual([ + ['{', 'punctuation'], + ['"data"', 'def'], + [':', 'punctuation'], + ['{', 'punctuation'], + ['"field"', 'property'], + [':', 'punctuation'], + ['"value"', 'string'], + ['}', 'punctuation'], + [',', 'punctuation'], + ['"errors"', 'def'], + [':', 'punctuation'], + ['[', 'punctuation'], + ['{', 'punctuation'], + ['"message"', 'property'], + [':', 'punctuation'], + ['"bork"', 'string'], + ['}', 'punctuation'], + [']', 'punctuation'], + ['}', 'punctuation'], + ]); + }); +}); diff --git a/packages/codemirror-graphql/src/results/mode.ts b/packages/codemirror-graphql/src/results/mode.ts new file mode 100644 index 00000000000..369ed8ecf83 --- /dev/null +++ b/packages/codemirror-graphql/src/results/mode.ts @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { list, t, onlineParser, p, Token } from 'graphql-language-service'; +import indent from '../utils/mode-indent'; + +/** + * This mode defines JSON, but provides a data-laden parser state to enable + * better code intelligence. + */ +CodeMirror.defineMode('graphql-results', config => { + const parser = onlineParser({ + eatWhitespace: stream => stream.eatSpace(), + lexRules: LexRules, + parseRules: ParseRules, + editorConfig: { tabSize: config.tabSize }, + }); + + return { + config, + startState: parser.startState, + token: parser.token as unknown as CodeMirror.Mode['token'], // TODO: Check if the types are indeed compatible + indent, + electricInput: /^\s*[}\]]/, + fold: 'brace', + closeBrackets: { + pairs: '[]{}""', + explode: '[]{}', + }, + }; +}); + +/** + * The lexer rules. These are exactly as described by the spec. + */ +const LexRules = { + // All Punctuation used in JSON. + Punctuation: /^\[|]|\{|\}|:|,/, + + // JSON Number. + Number: /^-?(?:0|(?:[1-9][0-9]*))(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?/, + + // JSON String. + String: /^"(?:[^"\\]|\\(?:"|\/|\\|b|f|n|r|t|u[0-9a-fA-F]{4}))*"?/, + + // JSON literal keywords. + Keyword: /^true|false|null/, +}; + +/** + * The parser rules for JSON. + */ +const ParseRules = { + Document: [p('{'), list('Entry', p(',')), p('}')], + Entry: [t('String', 'def'), p(':'), 'Value'], + Value(token: Token) { + switch (token.kind) { + case 'Number': + return 'NumberValue'; + case 'String': + return 'StringValue'; + case 'Punctuation': + switch (token.value) { + case '[': + return 'ListValue'; + case '{': + return 'ObjectValue'; + } + return null; + case 'Keyword': + switch (token.value) { + case 'true': + case 'false': + return 'BooleanValue'; + case 'null': + return 'NullValue'; + } + return null; + } + }, + NumberValue: [t('Number', 'number')], + StringValue: [t('String', 'string')], + BooleanValue: [t('Keyword', 'builtin')], + NullValue: [t('Keyword', 'keyword')], + ListValue: [p('['), list('Value', p(',')), p(']')], + ObjectValue: [p('{'), list('ObjectField', p(',')), p('}')], + ObjectField: [t('String', 'property'), p(':'), 'Value'], +}; diff --git a/packages/codemirror-graphql/src/utils/SchemaReference.ts b/packages/codemirror-graphql/src/utils/SchemaReference.ts new file mode 100644 index 00000000000..60afc203b5c --- /dev/null +++ b/packages/codemirror-graphql/src/utils/SchemaReference.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { getNamedType, GraphQLSchema } from 'graphql'; + +import type { + GraphQLArgument, + GraphQLDirective, + GraphQLEnumValue, + GraphQLEnumType, + GraphQLField, + GraphQLNamedType, +} from 'graphql'; +import { Maybe } from 'graphql/jsutils/Maybe'; +import { TypeInfo } from './getTypeInfo'; + +export type SchemaReference = + | FieldReference + | DirectiveReference + | ArgumentReference + | EnumValueReference + | TypeReference; + +export type FieldReference = { + kind: 'Field'; + field: GraphQLField; + type: Maybe; + schema?: GraphQLSchema; +}; + +export type DirectiveReference = { + kind: 'Directive'; + directive: GraphQLDirective; + schema?: GraphQLSchema; +}; + +export type ArgumentReference = { + kind: 'Argument'; + argument: GraphQLArgument; + field?: GraphQLField; + type?: GraphQLNamedType; + directive?: GraphQLDirective; + schema?: GraphQLSchema; +}; + +export type EnumValueReference = { + kind: 'EnumValue'; + value?: GraphQLEnumValue; + type?: GraphQLEnumType; + schema?: GraphQLSchema; +}; + +export type TypeReference = { + kind: 'Type'; + type: GraphQLNamedType; + schema?: GraphQLSchema; +}; + +export function getFieldReference(typeInfo: any): FieldReference { + return { + kind: 'Field', + schema: typeInfo.schema, + field: typeInfo.fieldDef, + type: isMetaField(typeInfo.fieldDef) ? null : typeInfo.parentType, + }; +} + +export function getDirectiveReference(typeInfo: any): DirectiveReference { + return { + kind: 'Directive', + schema: typeInfo.schema, + directive: typeInfo.directiveDef, + }; +} + +export function getArgumentReference(typeInfo: any): ArgumentReference { + return typeInfo.directiveDef + ? { + kind: 'Argument', + schema: typeInfo.schema, + argument: typeInfo.argDef, + directive: typeInfo.directiveDef, + } + : { + kind: 'Argument', + schema: typeInfo.schema, + argument: typeInfo.argDef, + field: typeInfo.fieldDef, + type: isMetaField(typeInfo.fieldDef) ? null : typeInfo.parentType, + }; +} + +export function getEnumValueReference(typeInfo: TypeInfo): EnumValueReference { + return { + kind: 'EnumValue', + value: typeInfo.enumValue || undefined, + // $FlowFixMe + type: typeInfo.inputType + ? (getNamedType(typeInfo.inputType) as GraphQLEnumType) + : undefined, + }; +} + +// Note: for reusability, getTypeReference can produce a reference to any type, +// though it defaults to the current type. +export function getTypeReference( + typeInfo: any, + type?: Maybe, +): TypeReference { + return { + kind: 'Type', + schema: typeInfo.schema, + type: type || typeInfo.type, + }; +} + +function isMetaField(fieldDef: GraphQLField) { + return fieldDef.name.slice(0, 2) === '__'; +} diff --git a/packages/codemirror-graphql/src/utils/__tests__/jsonParse-test.ts b/packages/codemirror-graphql/src/utils/__tests__/jsonParse-test.ts new file mode 100644 index 00000000000..6d2a8044e00 --- /dev/null +++ b/packages/codemirror-graphql/src/utils/__tests__/jsonParse-test.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 jsonParse, { ParseTokenOutput } from '../jsonParse'; + +describe('jsonParse', () => { + function expectEscapedString( + str: string, + key: ParseTokenOutput, + value: ParseTokenOutput, + ) { + const ast = jsonParse(str); + expect(ast.kind).toBe('Object'); + expect(ast.members[0].key).toStrictEqual(key); + expect(ast.members[0].value).toStrictEqual(value); + } + + it('correctly parses escaped strings', () => { + expectEscapedString( + '{ "test": "\\"" }', + { kind: 'String', start: 2, end: 8, value: 'test' }, + { kind: 'String', start: 10, end: 14, value: '"' }, + ); + expectEscapedString( + '{ "test": "\\\\" }', + { kind: 'String', start: 2, end: 8, value: 'test' }, + { kind: 'String', start: 10, end: 14, value: '\\' }, + ); + expectEscapedString( + '{ "slash": "\\/" }', + { kind: 'String', start: 2, end: 9, value: 'slash' }, + { kind: 'String', start: 11, end: 15, value: '/' }, + ); + }); +}); diff --git a/packages/codemirror-graphql/src/utils/collectVariables.ts b/packages/codemirror-graphql/src/utils/collectVariables.ts new file mode 100644 index 00000000000..b6ef1674ba1 --- /dev/null +++ b/packages/codemirror-graphql/src/utils/collectVariables.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + DocumentNode, + GraphQLSchema, + NamedTypeNode, + typeFromAST, +} from 'graphql'; + +/** + * Provided a schema and a document, produces a `variableToType` Object. + */ +export default function collectVariables( + schema: GraphQLSchema, + documentAST: DocumentNode, +) { + const variableToType = Object.create(null); + for (const definition of documentAST.definitions) { + if (definition.kind === 'OperationDefinition') { + const { variableDefinitions } = definition; + if (variableDefinitions) { + for (const { variable, type } of variableDefinitions) { + const inputType = typeFromAST(schema, type as NamedTypeNode); + if (inputType) { + variableToType[variable.name.value] = inputType; + } + } + } + } + } + return variableToType; +} diff --git a/packages/codemirror-graphql/src/utils/forEachState.ts b/packages/codemirror-graphql/src/utils/forEachState.ts new file mode 100644 index 00000000000..b8e73434f1c --- /dev/null +++ b/packages/codemirror-graphql/src/utils/forEachState.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 type { State, Maybe } from 'graphql-language-service'; + +// Utility for iterating through a CodeMirror parse state stack bottom-up. +export default function forEachState(stack: State, fn: (state: State) => void) { + const reverseStateStack = []; + let state: Maybe = stack; + while (state?.kind) { + reverseStateStack.push(state); + state = state.prevState; + } + for (let i = reverseStateStack.length - 1; i >= 0; i--) { + fn(reverseStateStack[i]); + } +} diff --git a/packages/codemirror-graphql/src/utils/getTypeInfo.ts b/packages/codemirror-graphql/src/utils/getTypeInfo.ts new file mode 100644 index 00000000000..d61d172e126 --- /dev/null +++ b/packages/codemirror-graphql/src/utils/getTypeInfo.ts @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + isCompositeType, + getNullableType, + getNamedType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLSchema, + GraphQLType, + GraphQLObjectType, + GraphQLField, + GraphQLDirective, + GraphQLArgument, + GraphQLInputType, + GraphQLEnumValue, + GraphQLInputFieldMap, + SchemaMetaFieldDef, + TypeMetaFieldDef, + TypeNameMetaFieldDef, +} from 'graphql'; +import type { State, Maybe } from 'graphql-language-service'; +import forEachState from './forEachState'; + +export interface TypeInfo { + schema: GraphQLSchema; + type?: Maybe; + parentType?: Maybe; + inputType?: Maybe; + directiveDef?: Maybe; + fieldDef?: Maybe>; + argDef?: Maybe; + argDefs?: Maybe; + enumValue?: Maybe; + objectFieldDefs?: Maybe; +} + +/** + * Utility for collecting rich type information given any token's state + * from the graphql-mode parser. + */ +export default function getTypeInfo(schema: GraphQLSchema, tokenState: State) { + const info: TypeInfo = { + schema, + type: null, + parentType: null, + inputType: null, + directiveDef: null, + fieldDef: null, + argDef: null, + argDefs: null, + objectFieldDefs: null, + }; + + forEachState(tokenState, (state: State) => { + switch (state.kind) { + case 'Query': + case 'ShortQuery': + info.type = schema.getQueryType(); + break; + case 'Mutation': + info.type = schema.getMutationType(); + break; + case 'Subscription': + info.type = schema.getSubscriptionType(); + break; + case 'InlineFragment': + case 'FragmentDefinition': + if (state.type) { + info.type = schema.getType(state.type); + } + break; + case 'Field': + case 'AliasedField': + info.fieldDef = + info.type && state.name + ? getFieldDef(schema, info.parentType, state.name) + : null; + info.type = info.fieldDef?.type; + break; + case 'SelectionSet': + info.parentType = info.type ? getNamedType(info.type) : null; + break; + case 'Directive': + info.directiveDef = state.name ? schema.getDirective(state.name) : null; + break; + case 'Arguments': + const parentDef = state.prevState + ? state.prevState.kind === 'Field' + ? info.fieldDef + : state.prevState.kind === 'Directive' + ? info.directiveDef + : state.prevState.kind === 'AliasedField' + ? state.prevState.name && + getFieldDef(schema, info.parentType, state.prevState.name) + : null + : null; + info.argDefs = parentDef ? (parentDef.args as GraphQLArgument[]) : null; + break; + case 'Argument': + info.argDef = null; + if (info.argDefs) { + for (let i = 0; i < info.argDefs.length; i++) { + if (info.argDefs[i].name === state.name) { + info.argDef = info.argDefs[i]; + break; + } + } + } + info.inputType = info.argDef?.type; + break; + case 'EnumValue': + const enumType = info.inputType ? getNamedType(info.inputType) : null; + info.enumValue = + enumType instanceof GraphQLEnumType + ? find( + enumType.getValues() as GraphQLEnumValue[], + val => val.value === state.name, + ) + : null; + break; + case 'ListValue': + const nullableType = info.inputType + ? getNullableType(info.inputType) + : null; + info.inputType = + nullableType instanceof GraphQLList ? nullableType.ofType : null; + break; + case 'ObjectValue': + const objectType = info.inputType ? getNamedType(info.inputType) : null; + info.objectFieldDefs = + objectType instanceof GraphQLInputObjectType + ? objectType.getFields() + : null; + break; + case 'ObjectField': + const objectField = + state.name && info.objectFieldDefs + ? info.objectFieldDefs[state.name] + : null; + info.inputType = objectField?.type; + break; + case 'NamedType': + info.type = state.name ? schema.getType(state.name) : null; + break; + } + }); + + return info; +} + +// Gets the field definition given a type and field name +function getFieldDef( + schema: GraphQLSchema, + type: Maybe, + fieldName: string, +) { + 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 && (type as GraphQLObjectType).getFields) { + return (type as GraphQLObjectType).getFields()[fieldName]; + } +} + +// Returns the first item in the array which causes predicate to return truthy. +function find(array: T[], predicate: (item: T) => boolean) { + for (let i = 0; i < array.length; i++) { + if (predicate(array[i])) { + return array[i]; + } + } +} diff --git a/packages/codemirror-graphql/src/utils/hintList.ts b/packages/codemirror-graphql/src/utils/hintList.ts new file mode 100644 index 00000000000..dc842ab04fa --- /dev/null +++ b/packages/codemirror-graphql/src/utils/hintList.ts @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 type CodeMirror from 'codemirror'; +import { IHint, IHints } from '../hint'; + +// Create the expected hint response given a possible list and a token +export default function hintList( + cursor: CodeMirror.Position, + token: CodeMirror.Token, + list: IHint[], +): IHints | undefined { + const hints = filterAndSortList(list, normalizeText(token.string)); + if (!hints) { + return; + } + + const tokenStart = + token.type !== null && /"|\w/.test(token.string[0]) + ? token.start + : token.end; + + return { + list: hints, + from: { line: cursor.line, ch: tokenStart }, // TODO: Confirm. Was changed column to ch + to: { line: cursor.line, ch: token.end }, + }; +} + +// Given a list of hint entries and currently typed text, sort and filter to +// provide a concise list. +function filterAndSortList(list: IHint[], text: string) { + if (!text) { + return filterNonEmpty(list, entry => !entry.isDeprecated); + } + + const byProximity = list.map(entry => ({ + proximity: getProximity(normalizeText(entry.text), text), + entry, + })); + + const conciseMatches = filterNonEmpty( + filterNonEmpty(byProximity, pair => pair.proximity <= 2), + pair => !pair.entry.isDeprecated, + ); + + const sortedMatches = conciseMatches.sort( + (a, b) => + (a.entry.isDeprecated ? 1 : 0) - (b.entry.isDeprecated ? 1 : 0) || + a.proximity - b.proximity || + a.entry.text.length - b.entry.text.length, + ); + + return sortedMatches.map(pair => pair.entry); +} + +// Filters the array by the predicate, unless it results in an empty array, +// in which case return the original array. +function filterNonEmpty(array: T[], predicate: (item: T) => boolean) { + const filtered = array.filter(predicate); + return filtered.length === 0 ? array : filtered; +} + +function normalizeText(text: string) { + return text.toLowerCase().replaceAll(/\W/g, ''); +} + +// Determine a numeric proximity for a suggestion based on current text. +function getProximity(suggestion: string, text: string) { + // start with lexical distance + let 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; +} + +/** + * 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 + */ +function lexicalDistance(a: string, b: string) { + let i; + let j; + const d = []; + const aLength = a.length; + const 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++) { + const 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/packages/codemirror-graphql/src/utils/info-addon.ts b/packages/codemirror-graphql/src/utils/info-addon.ts new file mode 100644 index 00000000000..5b017faae8d --- /dev/null +++ b/packages/codemirror-graphql/src/utils/info-addon.ts @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { GraphQLInfoOptions } from '../info'; + +CodeMirror.defineOption( + 'info', + false, + ( + cm: CodeMirror.Editor, + options: GraphQLInfoOptions, + old?: GraphQLInfoOptions, + ) => { + if (old && old !== CodeMirror.Init) { + const oldOnMouseOver = cm.state.info.onMouseOver; + CodeMirror.off(cm.getWrapperElement(), 'mouseover', oldOnMouseOver); + clearTimeout(cm.state.info.hoverTimeout); + delete cm.state.info; + } + + if (options) { + const state: Record = (cm.state.info = createState(options)); + state.onMouseOver = onMouseOver.bind(null, cm); + CodeMirror.on(cm.getWrapperElement(), 'mouseover', state.onMouseOver); + } + }, +); + +function createState(options: GraphQLInfoOptions) { + return { + options: + options instanceof Function + ? { render: options } + : options === true + ? {} + : options, + }; +} + +function getHoverTime(cm: CodeMirror.Editor) { + const { options } = cm.state.info; + return options?.hoverTime || 500; +} + +function onMouseOver(cm: CodeMirror.Editor, e: MouseEvent) { + const state = cm.state.info; + + const target = e.target || e.srcElement; + + if (!(target instanceof HTMLElement)) { + return; + } + if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) { + return; + } + + const box = target.getBoundingClientRect(); + + const onMouseMove = function () { + clearTimeout(state.hoverTimeout); + state.hoverTimeout = setTimeout(onHover, hoverTime); + }; + + const onMouseOut = function () { + CodeMirror.off(document, 'mousemove', onMouseMove); + CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut); + clearTimeout(state.hoverTimeout); + state.hoverTimeout = undefined; + }; + + const onHover = function () { + CodeMirror.off(document, 'mousemove', onMouseMove); + CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut); + state.hoverTimeout = undefined; + onMouseHover(cm, box); + }; + + const hoverTime = getHoverTime(cm); + state.hoverTimeout = setTimeout(onHover, hoverTime); + + CodeMirror.on(document, 'mousemove', onMouseMove); + CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut); +} + +function onMouseHover(cm: CodeMirror.Editor, box: DOMRect) { + const pos = cm.coordsChar( + { + left: (box.left + box.right) / 2, + top: (box.top + box.bottom) / 2, + }, + 'window', + ); // 'window' allows to work when editor is not full page and window has scrolled + + const state = cm.state.info; + const { options } = state; + const render = options.render || cm.getHelper(pos, 'info'); + if (render) { + const token = cm.getTokenAt(pos, true); + if (token) { + const info: HTMLDivElement = render(token, options, cm, pos); + if (info) { + showPopup(cm, box, info); + } + } + } +} + +function showPopup(cm: CodeMirror.Editor, box: DOMRect, info: HTMLDivElement) { + const popup = document.createElement('div'); + popup.className = 'CodeMirror-info'; + popup.append(info); + document.body.append(popup); + + const popupBox = popup.getBoundingClientRect(); + const popupStyle = window.getComputedStyle(popup); + const popupWidth = + popupBox.right - + popupBox.left + + parseFloat(popupStyle.marginLeft) + + parseFloat(popupStyle.marginRight); + const popupHeight = + popupBox.bottom - + popupBox.top + + parseFloat(popupStyle.marginTop) + + parseFloat(popupStyle.marginBottom); + + let topPos = box.bottom; + if ( + popupHeight > window.innerHeight - box.bottom - 15 && + box.top > window.innerHeight - box.bottom + ) { + topPos = box.top - popupHeight; + } + + if (topPos < 0) { + topPos = box.bottom; + } + + let leftPos = Math.max(0, window.innerWidth - popupWidth - 15); + if (leftPos > box.left) { + leftPos = box.left; + } + + popup.style.opacity = '1'; + popup.style.top = topPos + 'px'; + popup.style.left = leftPos + 'px'; + + let popupTimeout: NodeJS.Timeout; + + const onMouseOverPopup = function () { + clearTimeout(popupTimeout); + }; + + const onMouseOut = function () { + clearTimeout(popupTimeout); + popupTimeout = setTimeout(hidePopup, 200); + }; + + const hidePopup = function () { + CodeMirror.off(popup, 'mouseover', onMouseOverPopup); + CodeMirror.off(popup, 'mouseout', onMouseOut); + CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut); + + if (popup.style.opacity) { + popup.style.opacity = '0'; + setTimeout(() => { + if (popup.parentNode) { + popup.remove(); + } + }, 600); + } else if (popup.parentNode) { + popup.remove(); + } + }; + + CodeMirror.on(popup, 'mouseover', onMouseOverPopup); + CodeMirror.on(popup, 'mouseout', onMouseOut); + CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut); +} diff --git a/packages/codemirror-graphql/src/utils/jsonParse.ts b/packages/codemirror-graphql/src/utils/jsonParse.ts new file mode 100644 index 00000000000..92fded83cbf --- /dev/null +++ b/packages/codemirror-graphql/src/utils/jsonParse.ts @@ -0,0 +1,347 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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, generating an AST. Use this in lieu + * of JSON.parse if you need character offset parse errors and an AST parse tree + * with location information. + * + * If an error is encountered, a SyntaxError will be thrown, with properties: + * + * - message: string + * - start: int - the start inclusive offset of the syntax error + * - end: int - the end exclusive offset of the syntax error + * + */ +export default function jsonParse(str: string) { + string = str; + strLen = str.length; + start = end = lastEnd = -1; + ch(); + lex(); + const ast = parseObj(); + expect('EOF'); + return ast; +} + +let string: string; +let strLen: number; +let start: number; +let end: number; +let lastEnd: number; +let code: number; +let kind: string; + +interface BaseParseOutput { + kind: string; + start: number; + end: number; +} +export interface ParseTokenOutput extends BaseParseOutput { + value: any; +} +export interface ParseObjectOutput extends BaseParseOutput { + kind: 'Object'; + members: ParseMemberOutput[]; +} +export interface ParseArrayOutput extends BaseParseOutput { + kind: 'Array'; + values?: ParseValueOutput[]; +} +export interface ParseMemberOutput extends BaseParseOutput { + key: ParseTokenOutput | null; + value?: ParseValueOutput; +} +export type ParseValueOutput = + | ParseTokenOutput + | ParseObjectOutput + | ParseArrayOutput + | undefined; + +function parseObj(): ParseObjectOutput { + const nodeStart = start; + const members = []; + expect('{'); + if (!skip('}')) { + do { + members.push(parseMember()); + } while (skip(',')); + expect('}'); + } + return { + kind: 'Object', + start: nodeStart, + end: lastEnd, + members, + }; +} + +function parseMember(): ParseMemberOutput { + const nodeStart = start; + const key = kind === 'String' ? curToken() : null; + expect('String'); + expect(':'); + const value = parseVal(); + return { + kind: 'Member', + start: nodeStart, + end: lastEnd, + key, + value, + }; +} + +function parseArr(): ParseArrayOutput { + const nodeStart = start; + const values = []; + expect('['); + if (!skip(']')) { + do { + values.push(parseVal()); + } while (skip(',')); + expect(']'); + } + return { + kind: 'Array', + start: nodeStart, + end: lastEnd, + values, + }; +} + +function parseVal(): ParseValueOutput | undefined { + switch (kind) { + case '[': + return parseArr(); + case '{': + return parseObj(); + case 'String': + case 'Number': + case 'Boolean': + case 'Null': + const token = curToken(); + lex(); + return token; + } + expect('Value'); +} + +function curToken(): ParseTokenOutput { + return { kind, start, end, value: JSON.parse(string.slice(start, end)) }; +} + +function expect(str: string) { + if (kind === str) { + lex(); + return; + } + + let found; + if (kind === 'EOF') { + found = '[end of file]'; + } else if (end - start > 1) { + found = '`' + string.slice(start, end) + '`'; + } else { + const match = string.slice(start).match(/^.+?\b/); + found = '`' + (match ? match[0] : string[start]) + '`'; + } + + throw syntaxError(`Expected ${str} but found ${found}.`); +} + +type SyntaxErrorPosition = { start: number; end: number }; + +export class JSONSyntaxError extends Error { + readonly position: SyntaxErrorPosition; + constructor(message: string, position: SyntaxErrorPosition) { + super(message); + this.position = position; + } +} + +function syntaxError(message: string) { + return new JSONSyntaxError(message, { start, end }); +} + +function skip(k: string) { + if (kind === k) { + lex(); + return true; + } +} + +function ch() { + if (end < strLen) { + end++; + code = end === strLen ? 0 : string.charCodeAt(end); + } + return code; +} + +function lex() { + lastEnd = end; + + 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(); + // -, 0-9 + case 45: + case 48: + case 49: + case 50: + case 51: + case 52: + case 53: + case 54: + case 55: + case 56: + case 57: + kind = 'Number'; + return readNumber(); + // f + case 102: + if (string.slice(start, start + 5) !== 'false') { + break; + } + end += 4; + ch(); + + kind = 'Boolean'; + return; + // n + case 110: + if (string.slice(start, start + 4) !== 'null') { + break; + } + end += 3; + ch(); + + kind = 'Null'; + return; + // t + case 116: + if (string.slice(start, start + 4) !== 'true') { + break; + } + end += 3; + ch(); + + kind = 'Boolean'; + return; + } + + kind = string[start]; + ch(); +} + +function readString() { + ch(); + while (code !== 34 && code > 31) { + if (code === 92) { + // \ + code = 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.'); + } + } else if (end === strLen) { + throw syntaxError('Unterminated string.'); + } else { + ch(); + } + } + + 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 + code = 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/packages/codemirror-graphql/src/utils/jump-addon.ts b/packages/codemirror-graphql/src/utils/jump-addon.ts new file mode 100644 index 00000000000..0977eb1a903 --- /dev/null +++ b/packages/codemirror-graphql/src/utils/jump-addon.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { GraphQLJumpOptions } from '../jump'; + +CodeMirror.defineOption( + 'jump', + false, + ( + cm: CodeMirror.Editor, + options: GraphQLJumpOptions, + old?: GraphQLJumpOptions, + ) => { + if (old && old !== CodeMirror.Init) { + const oldOnMouseOver = cm.state.jump.onMouseOver; + CodeMirror.off(cm.getWrapperElement(), 'mouseover', oldOnMouseOver); + const oldOnMouseOut = cm.state.jump.onMouseOut; + CodeMirror.off(cm.getWrapperElement(), 'mouseout', oldOnMouseOut); + CodeMirror.off(document, 'keydown', cm.state.jump.onKeyDown); + delete cm.state.jump; + } + + if (options) { + const state = (cm.state.jump = { + options, + onMouseOver: onMouseOver.bind(null, cm), + onMouseOut: onMouseOut.bind(null, cm), + onKeyDown: onKeyDown.bind(null, cm), + }); + + CodeMirror.on(cm.getWrapperElement(), 'mouseover', state.onMouseOver); + CodeMirror.on(cm.getWrapperElement(), 'mouseout', state.onMouseOut); + CodeMirror.on(document, 'keydown', state.onKeyDown); + } + }, +); + +function onMouseOver(cm: CodeMirror.Editor, event: MouseEvent) { + const target = event.target || event.srcElement; + if (!(target instanceof HTMLElement)) { + return; + } + if (target?.nodeName !== 'SPAN') { + return; + } + + const box = target.getBoundingClientRect(); + const cursor = { + left: (box.left + box.right) / 2, + top: (box.top + box.bottom) / 2, + }; + + cm.state.jump.cursor = cursor; + + if (cm.state.jump.isHoldingModifier) { + enableJumpMode(cm); + } +} + +function onMouseOut(cm: CodeMirror.Editor) { + if (!cm.state.jump.isHoldingModifier && cm.state.jump.cursor) { + cm.state.jump.cursor = null; + return; + } + + if (cm.state.jump.isHoldingModifier && cm.state.jump.marker) { + disableJumpMode(cm); + } +} + +function onKeyDown(cm: CodeMirror.Editor, event: KeyboardEvent) { + if (cm.state.jump.isHoldingModifier || !isJumpModifier(event.key)) { + return; + } + + cm.state.jump.isHoldingModifier = true; + + if (cm.state.jump.cursor) { + enableJumpMode(cm); + } + + const onKeyUp = (upEvent: KeyboardEvent) => { + if (upEvent.code !== event.code) { + return; + } + + cm.state.jump.isHoldingModifier = false; + + if (cm.state.jump.marker) { + disableJumpMode(cm); + } + + CodeMirror.off(document, 'keyup', onKeyUp); + CodeMirror.off(document, 'click', onClick); + cm.off('mousedown', onMouseDown); + }; + + const onClick = (clickEvent: MouseEvent) => { + const { destination, options } = cm.state.jump; + if (destination) { + options.onClick(destination, clickEvent); + } + }; + + const onMouseDown = (_: any, downEvent: MouseEvent) => { + if (cm.state.jump.destination) { + (downEvent as any).codemirrorIgnore = true; + } + }; + + CodeMirror.on(document, 'keyup', onKeyUp); + CodeMirror.on(document, 'click', onClick); + cm.on('mousedown', onMouseDown); +} + +const isMac = + typeof navigator !== 'undefined' && + navigator && + navigator.appVersion.includes('Mac'); + +function isJumpModifier(key: string) { + return key === (isMac ? 'Meta' : 'Control'); +} + +function enableJumpMode(cm: CodeMirror.Editor) { + if (cm.state.jump.marker) { + return; + } + + const { cursor, options } = cm.state.jump; + const pos = cm.coordsChar(cursor); + const token = cm.getTokenAt(pos, true); + const getDestination = options.getDestination || cm.getHelper(pos, 'jump'); + if (getDestination) { + const destination = getDestination(token, options, cm); + if (destination) { + const marker = cm.markText( + { line: pos.line, ch: token.start }, + { line: pos.line, ch: token.end }, + { className: 'CodeMirror-jump-token' }, + ); + + cm.state.jump.marker = marker; + cm.state.jump.destination = destination; + } + } +} + +function disableJumpMode(cm: CodeMirror.Editor) { + const { marker } = cm.state.jump; + cm.state.jump.marker = null; + cm.state.jump.destination = null; + + marker.clear(); +} diff --git a/packages/codemirror-graphql/src/utils/mode-factory.ts b/packages/codemirror-graphql/src/utils/mode-factory.ts new file mode 100644 index 00000000000..116083565bf --- /dev/null +++ b/packages/codemirror-graphql/src/utils/mode-factory.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + LexRules, + ParseRules, + isIgnored, + onlineParser, +} from 'graphql-language-service'; +import indent from './mode-indent'; + +/** + * 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 typeahead. + */ +const graphqlModeFactory: CodeMirror.ModeFactory = config => { + const parser = onlineParser({ + eatWhitespace: stream => stream.eatWhile(isIgnored), + lexRules: LexRules, + parseRules: ParseRules, + editorConfig: { tabSize: config.tabSize }, + }); + + return { + config, + startState: parser.startState, + token: parser.token as unknown as NonNullable< + CodeMirror.Mode['token'] + >, // TODO: Check if the types are indeed compatible + indent, + electricInput: /^\s*[})\]]/, + fold: 'brace', + lineComment: '#', + closeBrackets: { + pairs: '()[]{}""', + explode: '()[]{}', + }, + }; +}; + +export default graphqlModeFactory; diff --git a/packages/codemirror-graphql/src/utils/mode-indent.ts b/packages/codemirror-graphql/src/utils/mode-indent.ts new file mode 100644 index 00000000000..10c417df99f --- /dev/null +++ b/packages/codemirror-graphql/src/utils/mode-indent.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { State } from 'graphql-language-service'; + +// Seems the electricInput type in @types/codemirror is wrong (i.e it is written all lowercase) +export default function indent( + this: CodeMirror.Mode & { + electricInput?: RegExp; + config?: CodeMirror.EditorConfiguration; + }, + state: State, + textAfter: string, +) { + const { levels, indentLevel } = state; + // If there is no stack of levels, use the current level. + // Otherwise, use the top level, preemptively dedenting for close braces. + const level = + !levels || levels.length === 0 + ? indentLevel + : levels.at(-1)! - (this.electricInput?.test(textAfter) ? 1 : 0); + return (level || 0) * (this.config?.indentUnit || 0); +} diff --git a/packages/codemirror-graphql/src/utils/runParser.ts b/packages/codemirror-graphql/src/utils/runParser.ts new file mode 100644 index 00000000000..034be6fb7e3 --- /dev/null +++ b/packages/codemirror-graphql/src/utils/runParser.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + CharacterStream, + onlineParser, + ParserOptions, + State, +} from 'graphql-language-service'; + +export default function runParser( + sourceText: string, + parserOptions: ParserOptions, + callbackFn: (stream: CharacterStream, state: State, style: string) => void, +) { + const parser = onlineParser(parserOptions); + const state = parser.startState(); + const lines = sourceText.split('\n'); + + for (const line of lines) { + const stream = new CharacterStream(line); + while (!stream.eol()) { + const style = parser.token(stream, state); + callbackFn(stream, state, style); + } + } +} diff --git a/packages/codemirror-graphql/src/variables/__tests__/hint-test.ts b/packages/codemirror-graphql/src/variables/__tests__/hint-test.ts new file mode 100644 index 00000000000..1ada1754e89 --- /dev/null +++ b/packages/codemirror-graphql/src/variables/__tests__/hint-test.ts @@ -0,0 +1,169 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 'codemirror/addon/hint/show-hint'; +import { GraphQLEnumType, GraphQLInputObjectType, parse } from 'graphql'; +import { IHint, IHints } from '../../hint'; +import collectVariables from '../../utils/collectVariables'; +import { TestSchema } from '../../__tests__/testSchema'; +import '../hint'; +import '../mode'; + +function createEditorWithHint(query: string) { + return CodeMirror(document.createElement('div'), { + mode: 'graphql-variables', + hintOptions: { + variableToType: query && collectVariables(TestSchema, parse(query)), + closeOnUnfocus: false, + completeSingle: false, + }, + }); +} + +function getHintSuggestions( + query: string, + variables: string, + cursor: CodeMirror.Position, +) { + const editor = createEditorWithHint(query); + return new Promise(resolve => { + const graphqlVariablesHint = CodeMirror.hint['graphql-variables']; + CodeMirror.hint['graphql-variables'] = (cm, options) => { + const result = graphqlVariablesHint(cm, options); + resolve(result); + CodeMirror.hint['graphql-variables'] = graphqlVariablesHint; + return result; + }; + + editor.doc.setValue(variables); + editor.doc.setCursor(cursor); + editor.execCommand('autocomplete'); + }); +} + +function expectSuggestions(source: string[], suggestions?: IHint[]) { + const titles = suggestions?.map(suggestion => suggestion.text); + expect(titles).toEqual(source); +} + +describe('graphql-variables-hint', () => { + it('attaches a GraphQL hint function with correct mode/hint options', () => { + const editor = createEditorWithHint('{ f }'); + expect(editor.getHelpers(editor.getCursor(), 'hint')).not.toHaveLength(0); + }); + + it('provides correct initial token', async () => { + const suggestions = await getHintSuggestions('', '', { line: 0, ch: 0 }); + const initialKeywords = ['{']; + expectSuggestions(initialKeywords, suggestions?.list); + }); + + it('provides correct field name suggestions', async () => { + const suggestions = await getHintSuggestions( + 'query ($foo: String!, $bar: Int) { f }', + '{ ', + { line: 0, ch: 2 }, + ); + expectSuggestions(['"foo": ', '"bar": '], suggestions?.list); + }); + + it('provides correct variable suggestion indentation', async () => { + const suggestions = await getHintSuggestions( + 'query ($foo: String!, $bar: Int) { f }', + '{\n ', + { line: 1, ch: 2 }, + ); + expect(suggestions?.from).toEqual({ line: 1, ch: 2, sticky: null }); + expect(suggestions?.to).toEqual({ line: 1, ch: 2, sticky: null }); + }); + + it('provides correct variable completion', async () => { + const suggestions = await getHintSuggestions( + 'query ($foo: String!, $bar: Int) { f }', + '{\n ba', + { line: 1, ch: 4 }, + ); + expectSuggestions(['"bar": '], suggestions?.list); + expect(suggestions?.from).toEqual({ line: 1, ch: 2, sticky: null }); + expect(suggestions?.to).toEqual({ line: 1, ch: 4, sticky: null }); + }); + + it('provides correct variable completion with open quote', async () => { + const suggestions = await getHintSuggestions( + 'query ($foo: String!, $bar: Int) { f }', + '{\n "', + { line: 1, ch: 4 }, + ); + expectSuggestions(['"foo": ', '"bar": '], suggestions?.list); + expect(suggestions?.from).toEqual({ line: 1, ch: 2, sticky: null }); + expect(suggestions?.to).toEqual({ line: 1, ch: 3, sticky: null }); + }); + + it('provides correct Enum suggestions', async () => { + const suggestions = await getHintSuggestions( + 'query ($myEnum: TestEnum) { f }', + '{\n "myEnum": ', + { line: 1, ch: 12 }, + ); + const TestEnum = TestSchema.getType('TestEnum'); + expectSuggestions( + (TestEnum as GraphQLEnumType) + ?.getValues() + .map(value => `"${value.name}"`), + suggestions?.list, + ); + }); + + it('suggests to open an Input Object', async () => { + const suggestions = await getHintSuggestions( + 'query ($myInput: TestInput) { f }', + '{\n "myInput": ', + { line: 1, ch: 13 }, + ); + expectSuggestions(['{'], suggestions?.list); + }); + + it('provides Input Object fields', async () => { + const suggestions = await getHintSuggestions( + 'query ($myInput: TestInput) { f }', + '{\n "myInput": {\n ', + { line: 2, ch: 4 }, + ); + const TestInput = TestSchema.getType('TestInput'); + expectSuggestions( + Object.keys((TestInput as GraphQLInputObjectType).getFields()).map( + name => `"${name}": `, + ), + suggestions?.list, + ); + expect(suggestions?.from).toEqual({ line: 2, ch: 4, sticky: null }); + expect(suggestions?.to).toEqual({ line: 2, ch: 4, sticky: null }); + }); + + it('provides correct Input Object field completion', async () => { + const suggestions = await getHintSuggestions( + 'query ($myInput: TestInput) { f }', + '{\n "myInput": {\n bool', + { line: 2, ch: 8 }, + ); + expectSuggestions(['"boolean": ', '"listBoolean": '], suggestions?.list); + expect(suggestions?.from).toEqual({ line: 2, ch: 4, sticky: null }); + expect(suggestions?.to).toEqual({ line: 2, ch: 8, sticky: null }); + }); + + it('provides correct Input Object field value completion', async () => { + const suggestions = await getHintSuggestions( + 'query ($myInput: TestInput) { f }', + '{\n "myInput": {\n "boolean": ', + { line: 2, ch: 15 }, + ); + expectSuggestions(['true', 'false'], suggestions?.list); + }); +}); diff --git a/packages/codemirror-graphql/src/variables/__tests__/lint-test.ts b/packages/codemirror-graphql/src/variables/__tests__/lint-test.ts new file mode 100644 index 00000000000..777a8845fe7 --- /dev/null +++ b/packages/codemirror-graphql/src/variables/__tests__/lint-test.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 'codemirror/addon/lint/lint'; +import { parse } from 'graphql'; +import { Maybe } from 'graphql-language-service'; +import collectVariables from '../../utils/collectVariables'; +import { TestSchema } from '../../__tests__/testSchema'; +import '../lint'; +import '../mode'; + +function createEditorWithLint(lintConfig?: any) { + return CodeMirror(document.createElement('div'), { + mode: 'graphql-variables', + lint: lintConfig || true, + }); +} + +function printLintErrors(query: Maybe, variables: string) { + const editor = createEditorWithLint({ + variableToType: query && collectVariables(TestSchema, parse(query)), + }); + + return new Promise(resolve => { + editor.state.lint.options.onUpdateLinting = ( + errors: CodeMirror.Annotation[], + ) => { + if (errors?.[0] && !errors[0].message?.match('Unexpected EOF')) { + resolve(errors); + return; + } + resolve([]); + }; + editor.doc.setValue(variables); + }); +} + +describe('graphql-variables-lint', () => { + it('attaches a GraphQL lint function with correct mode/lint options', () => { + const editor = createEditorWithLint(); + expect(editor.getHelpers(editor.getCursor(), 'lint')).not.toHaveLength(0); + }); + + it('catches syntax errors', async () => { + expect((await printLintErrors(null, '{ foo: "bar" }'))[0].message).toBe( + 'Expected String but found `foo`.', + ); + }); + + it('catches type validation errors', async () => { + const errors = await printLintErrors( + 'query ($foo: Int) { f }', + ' { "foo": "NaN" }', + ); + + expect(errors[0]).toEqual({ + message: 'Expected value of type "Int".', + severity: 'error', + type: 'validation', + from: { line: 0, ch: 10, sticky: null }, + to: { line: 0, ch: 15, sticky: null }, + }); + }); + + it('reports unknown variable names', async () => { + const errors = await printLintErrors( + 'query ($foo: Int) { f }', + ' { "food": "NaN" }', + ); + + expect(errors[0]).toEqual({ + message: 'Variable "$food" does not appear in any GraphQL query.', + severity: 'error', + type: 'validation', + from: { line: 0, ch: 3, sticky: null }, + to: { line: 0, ch: 9, sticky: null }, + }); + }); + + it('reports nothing when not configured', async () => { + const errors = await printLintErrors(null, ' { "foo": "NaN" }'); + expect(errors.length).toBe(0); + }); +}); diff --git a/packages/codemirror-graphql/src/variables/__tests__/mode-test.ts b/packages/codemirror-graphql/src/variables/__tests__/mode-test.ts new file mode 100644 index 00000000000..72857eec7f5 --- /dev/null +++ b/packages/codemirror-graphql/src/variables/__tests__/mode-test.ts @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 'codemirror/addon/runmode/runmode'; +import '../mode'; + +describe('graphql-variables-mode', () => { + it('provides correct tokens and styles after parsing', () => { + const queryStr = + '{ "variable": { "field": "value" }, "list": [ 1, true, null ] }'; + const tokens: [string, string][] = []; + + CodeMirror.runMode(queryStr, 'graphql-variables', (token, style) => { + if (style && style !== 'ws') { + tokens.push([token, style]); + } + }); + + expect(tokens).toEqual([ + ['{', 'punctuation'], + ['"variable"', 'variable'], + [':', 'punctuation'], + ['{', 'punctuation'], + ['"field"', 'attribute'], + [':', 'punctuation'], + ['"value"', 'string'], + ['}', 'punctuation'], + [',', 'punctuation'], + ['"list"', 'variable'], + [':', 'punctuation'], + ['[', 'punctuation'], + ['1', 'number'], + [',', 'punctuation'], + ['true', 'builtin'], + [',', 'punctuation'], + ['null', 'keyword'], + [']', 'punctuation'], + ['}', 'punctuation'], + ]); + }); + + it('is resilient to missing commas', () => { + const queryStr = + '{ "variable": { "field": "value" } "list": [ 1 true null ] }'; + const tokens: [string, string][] = []; + + CodeMirror.runMode(queryStr, 'graphql-variables', (token, style) => { + if (style && style !== 'ws') { + tokens.push([token, style]); + } + }); + + expect(tokens).toEqual([ + ['{', 'punctuation'], + ['"variable"', 'variable'], + [':', 'punctuation'], + ['{', 'punctuation'], + ['"field"', 'attribute'], + [':', 'punctuation'], + ['"value"', 'string'], + ['}', 'punctuation'], + ['"list"', 'variable'], + [':', 'punctuation'], + ['[', 'punctuation'], + ['1', 'number'], + ['true', 'builtin'], + ['null', 'keyword'], + [']', 'punctuation'], + ['}', 'punctuation'], + ]); + }); + + it('returns "invalidchar" message when there is no matching token', () => { + CodeMirror.runMode('nope', 'graphql-variables', (token, style) => { + if (token.trim()) { + expect(style).toBe('invalidchar'); + } + }); + + CodeMirror.runMode('{ foo', 'graphql-variables', (token, style) => { + if (token === 'foo') { + expect(style).toBe('invalidchar'); + } + }); + }); +}); diff --git a/packages/codemirror-graphql/src/variables/hint.ts b/packages/codemirror-graphql/src/variables/hint.ts new file mode 100644 index 00000000000..7129a36f648 --- /dev/null +++ b/packages/codemirror-graphql/src/variables/hint.ts @@ -0,0 +1,221 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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, { Hints } from 'codemirror'; +import { + getNullableType, + getNamedType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLBoolean, + GraphQLInputType, + GraphQLInputFieldMap, +} from 'graphql'; +import type { State, Maybe } from 'graphql-language-service'; +import { IHints } from '../hint'; + +import forEachState from '../utils/forEachState'; +import hintList from '../utils/hintList'; + +export type VariableToType = Record; +interface GraphQLVariableHintOptions { + variableToType: VariableToType; +} + +declare module 'codemirror' { + interface ShowHintOptions { + variableToType?: VariableToType; + } + + interface CodeMirrorHintMap { + 'graphql-variables': ( + editor: CodeMirror.Editor, + options: GraphQLVariableHintOptions, + ) => IHints | undefined; + } +} + +/** + * Registers a "hint" helper for CodeMirror. + * + * Using CodeMirror's "hint" addon: https://codemirror.net/demo/complete.html + * Given an editor, this helper will take the token at the cursor and return a + * list of suggested tokens. + * + * Options: + * + * - variableToType: { [variable: string]: GraphQLInputType } + * + * Additional Events: + * + * - hasCompletion (codemirror, data, token) - signaled when the hinter has a + * new list of completion suggestions. + * + */ +CodeMirror.registerHelper( + 'hint', + 'graphql-variables', + ( + editor: CodeMirror.Editor, + options: GraphQLVariableHintOptions, + ): Hints | undefined => { + const cur = editor.getCursor(); + const token = editor.getTokenAt(cur); + + const results = getVariablesHint(cur, token, options); + if (results?.list && results.list.length > 0) { + results.from = CodeMirror.Pos(results.from.line, results.from.ch); + results.to = CodeMirror.Pos(results.to.line, results.to.ch); + CodeMirror.signal(editor, 'hasCompletion', editor, results, token); + } + + return results; + }, +); + +function getVariablesHint( + cur: CodeMirror.Position, + token: CodeMirror.Token, + options: GraphQLVariableHintOptions, +) { + // If currently parsing an invalid state, attempt to hint to the prior state. + const state = + token.state.kind === 'Invalid' ? token.state.prevState : token.state; + + const { kind, step } = state; + // Variables can only be an object literal. + if (kind === 'Document' && step === 0) { + return hintList(cur, token, [{ text: '{' }]); + } + + const { variableToType } = options; + if (!variableToType) { + return; + } + + const typeInfo = getTypeInfo(variableToType, token.state); + + // Top level should typeahead possible variables. + if (kind === 'Document' || (kind === 'Variable' && step === 0)) { + const variableNames = Object.keys(variableToType); + return hintList( + cur, + token, + variableNames.map(name => ({ + text: `"${name}": `, + type: variableToType[name], + })), + ); + } + + // Input Object fields + if ( + (kind === 'ObjectValue' || (kind === 'ObjectField' && step === 0)) && + typeInfo.fields + ) { + const inputFields = Object.keys(typeInfo.fields).map( + fieldName => typeInfo.fields![fieldName], + ); + return hintList( + cur, + token, + inputFields.map(field => ({ + text: `"${field.name}": `, + type: field.type, + description: field.description, + })), + ); + } + + // Input values. + if ( + kind === 'StringValue' || + kind === 'NumberValue' || + kind === 'BooleanValue' || + kind === 'NullValue' || + (kind === 'ListValue' && step === 1) || + (kind === 'ObjectField' && step === 2) || + (kind === 'Variable' && step === 2) + ) { + const namedInputType = typeInfo.type + ? getNamedType(typeInfo.type) + : undefined; + if (namedInputType instanceof GraphQLInputObjectType) { + return hintList(cur, token, [{ text: '{' }]); + } + if (namedInputType instanceof GraphQLEnumType) { + const values = namedInputType.getValues(); + // const values = Object.keys(valueMap).map(name => valueMap[name]); // TODO: Previously added + return hintList( + cur, + token, + values.map(value => ({ + text: `"${value.name}"`, + type: namedInputType, + description: value.description, + })), + ); + } + if (namedInputType === GraphQLBoolean) { + return hintList(cur, token, [ + { text: 'true', type: GraphQLBoolean, description: 'Not false.' }, // TODO: type and description don't seem to be used. Added them as optional anyway. + { text: 'false', type: GraphQLBoolean, description: 'Not true.' }, + ]); + } + } +} + +interface VariableTypeInfo { + type?: Maybe; + fields?: Maybe; +} + +// Utility for collecting rich type information given any token's state +// from the graphql-variables-mode parser. +function getTypeInfo( + variableToType: Record, + tokenState: State, +) { + const info: VariableTypeInfo = { + type: null, + fields: null, + }; + + forEachState(tokenState, state => { + switch (state.kind) { + case 'Variable': { + info.type = variableToType[state.name!]; + break; + } + case 'ListValue': { + const nullableType = info.type ? getNullableType(info.type) : undefined; + info.type = + nullableType instanceof GraphQLList ? nullableType.ofType : null; + break; + } + case 'ObjectValue': { + const objectType = info.type ? getNamedType(info.type) : undefined; + info.fields = + objectType instanceof GraphQLInputObjectType + ? objectType.getFields() + : null; + break; + } + case 'ObjectField': { + const objectField = + state.name && info.fields ? info.fields[state.name] : null; + info.type = objectField?.type; + break; + } + } + }); + + return info; +} diff --git a/packages/codemirror-graphql/src/variables/lint.ts b/packages/codemirror-graphql/src/variables/lint.ts new file mode 100644 index 00000000000..c03ae1245f5 --- /dev/null +++ b/packages/codemirror-graphql/src/variables/lint.ts @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, + GraphQLType, +} from 'graphql'; + +import jsonParse, { + JSONSyntaxError, + ParseArrayOutput, + ParseObjectOutput, + ParseValueOutput, +} from '../utils/jsonParse'; +import { VariableToType } from './hint'; + +interface GraphQLVariableLintOptions { + variableToType: VariableToType; +} + +/** + * Registers a "lint" helper for CodeMirror. + * + * Using CodeMirror's "lint" addon: https://codemirror.net/demo/lint.html + * Given the text within an editor, this helper will take that text and return + * a list of linter issues ensuring that correct variables were provided. + * + * Options: + * + * - variableToType: { [variable: string]: GraphQLInputType } + * + */ +CodeMirror.registerHelper( + 'lint', + 'graphql-variables', + ( + text: string, + options: GraphQLVariableLintOptions, + editor: CodeMirror.Editor, + ) => { + // If there's no text, do nothing. + if (!text) { + return []; + } + + // First, linter needs to determine if there are any parsing errors. + let ast; + try { + ast = jsonParse(text); + } catch (error) { + if (error instanceof JSONSyntaxError) { + return [lintError(editor, error.position, error.message)]; + } + throw error; + } + + // If there are not yet known variables, do nothing. + const { variableToType } = options; + if (!variableToType) { + return []; + } + + // Then highlight any issues with the provided variables. + return validateVariables(editor, variableToType, ast); + }, +); + +// Given a variableToType object, a source text, and a JSON AST, produces a +// list of CodeMirror annotations for any variable validation errors. +function validateVariables( + editor: CodeMirror.Editor, + variableToType: VariableToType, + variablesAST: ParseObjectOutput, +) { + const errors: CodeMirror.Annotation[] = []; + + for (const member of variablesAST.members) { + if (member) { + const variableName = member.key?.value; + const type = variableToType[variableName]; + if (type) { + for (const [node, message] of validateValue(type, member.value)) { + errors.push(lintError(editor, node, message)); + } + } else { + errors.push( + lintError( + editor, + member.key!, + `Variable "$${variableName}" does not appear in any GraphQL query.`, + ), + ); + } + } + } + + return errors; +} + +// Returns a list of validation errors in the form Array<[Node, String]>. +function validateValue( + type?: GraphQLType, + valueAST?: ParseValueOutput, +): any[][] { + // TODO: Can't figure out the right type. + if (!type || !valueAST) { + return []; + } + + // Validate non-nullable values. + if (type instanceof GraphQLNonNull) { + if (valueAST.kind === 'Null') { + return [[valueAST, `Type "${type}" is non-nullable and cannot be null.`]]; + } + return validateValue(type.ofType, valueAST); + } + + if (valueAST.kind === 'Null') { + return []; + } + + // Validate lists of values, accepting a non-list as a list of one. + if (type instanceof GraphQLList) { + const itemType = type.ofType; + if (valueAST.kind === 'Array') { + const values = (valueAST as ParseArrayOutput).values || []; + return mapCat(values, item => validateValue(itemType, item)); + } + return validateValue(itemType, valueAST); + } + + // Validate input objects. + if (type instanceof GraphQLInputObjectType) { + if (valueAST.kind !== 'Object') { + return [[valueAST, `Type "${type}" must be an Object.`]]; + } + + // Validate each field in the input object. + const providedFields = Object.create(null); + const fieldErrors: any[][] = mapCat( + (valueAST as ParseObjectOutput).members, + member => { + // TODO: Can't figure out the right type here + const fieldName = member?.key?.value; + providedFields[fieldName] = true; + const inputField = type.getFields()[fieldName]; + if (!inputField) { + return [ + [ + member.key, + `Type "${type}" does not have a field "${fieldName}".`, + ], + ]; + } + const fieldType = inputField ? inputField.type : undefined; + return validateValue(fieldType, member.value); + }, + ); + + // Look for missing non-nullable fields. + for (const fieldName of Object.keys(type.getFields())) { + const field = type.getFields()[fieldName]; + if ( + !providedFields[fieldName] && + field.type instanceof GraphQLNonNull && + !field.defaultValue + ) { + fieldErrors.push([ + valueAST, + `Object of type "${type}" is missing required field "${fieldName}".`, + ]); + } + } + + return fieldErrors; + } + + // Validate common scalars. + if ( + (type.name === 'Boolean' && valueAST.kind !== 'Boolean') || + (type.name === 'String' && valueAST.kind !== 'String') || + (type.name === 'ID' && + valueAST.kind !== 'Number' && + valueAST.kind !== 'String') || + (type.name === 'Float' && valueAST.kind !== 'Number') || + (type.name === 'Int' && + // eslint-disable-next-line no-bitwise + (valueAST.kind !== 'Number' || (valueAST.value | 0) !== valueAST.value)) + ) { + return [[valueAST, `Expected value of type "${type}".`]]; + } + + // Validate enums and custom scalars. + if ( + (type instanceof GraphQLEnumType || type instanceof GraphQLScalarType) && + ((valueAST.kind !== 'String' && + valueAST.kind !== 'Number' && + valueAST.kind !== 'Boolean' && + valueAST.kind !== 'Null') || + isNullish(type.parseValue(valueAST.value))) + ) { + return [[valueAST, `Expected value of type "${type}".`]]; + } + + return []; +} + +// Give a parent text, an AST node with location, and a message, produces a +// CodeMirror annotation object. +function lintError( + editor: CodeMirror.Editor, + node: { start: number; end: number }, + message: string, +): CodeMirror.Annotation & { type: string } { + return { + message, + severity: 'error', + type: 'validation', + from: editor.posFromIndex(node.start), + to: editor.posFromIndex(node.end), + }; +} + +function isNullish(value: any): boolean { + // eslint-disable-next-line no-self-compare + return value === null || value === undefined || value !== value; +} + +function mapCat(array: T[], mapper: (item: T) => R[]): R[] { + return Array.prototype.concat.apply([], array.map(mapper)); +} diff --git a/packages/codemirror-graphql/src/variables/mode.ts b/packages/codemirror-graphql/src/variables/mode.ts new file mode 100644 index 00000000000..393fa8a835c --- /dev/null +++ b/packages/codemirror-graphql/src/variables/mode.ts @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * 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 { + list, + t, + onlineParser, + opt, + p, + State, + Token, +} from 'graphql-language-service'; +import indent from '../utils/mode-indent'; + +/** + * This mode defines JSON, but provides a data-laden parser state to enable + * better code intelligence. + */ +CodeMirror.defineMode('graphql-variables', config => { + const parser = onlineParser({ + eatWhitespace: stream => stream.eatSpace(), + lexRules: LexRules, + parseRules: ParseRules, + editorConfig: { tabSize: config.tabSize }, + }); + + return { + config, + startState: parser.startState, + token: parser.token as unknown as CodeMirror.Mode['token'], // TODO: Check if the types are indeed compatible + indent, + electricInput: /^\s*[}\]]/, + fold: 'brace', + closeBrackets: { + pairs: '[]{}""', + explode: '[]{}', + }, + }; +}); + +/** + * The lexer rules. These are exactly as described by the spec. + */ +const LexRules = { + // All Punctuation used in JSON. + Punctuation: /^\[|]|\{|\}|:|,/, + + // JSON Number. + Number: /^-?(?:0|(?:[1-9][0-9]*))(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?/, + + // JSON String. + String: /^"(?:[^"\\]|\\(?:"|\/|\\|b|f|n|r|t|u[0-9a-fA-F]{4}))*"?/, + + // JSON literal keywords. + Keyword: /^true|false|null/, +}; + +/** + * The parser rules for JSON. + */ +const ParseRules = { + Document: [p('{'), list('Variable', opt(p(','))), p('}')], + Variable: [namedKey('variable'), p(':'), 'Value'], + Value(token: Token) { + switch (token.kind) { + case 'Number': + return 'NumberValue'; + case 'String': + return 'StringValue'; + case 'Punctuation': + switch (token.value) { + case '[': + return 'ListValue'; + case '{': + return 'ObjectValue'; + } + return null; + case 'Keyword': + switch (token.value) { + case 'true': + case 'false': + return 'BooleanValue'; + case 'null': + return 'NullValue'; + } + return null; + } + }, + NumberValue: [t('Number', 'number')], + StringValue: [t('String', 'string')], + BooleanValue: [t('Keyword', 'builtin')], + NullValue: [t('Keyword', 'keyword')], + ListValue: [p('['), list('Value', opt(p(','))), p(']')], + ObjectValue: [p('{'), list('ObjectField', opt(p(','))), p('}')], + ObjectField: [namedKey('attribute'), p(':'), 'Value'], +}; + +// A namedKey Token which will decorate the state with a `name` +function namedKey(style: string) { + return { + style, + match: (token: Token) => token.kind === 'String', + update(state: State, token: Token) { + state.name = token.value.slice(1, -1); // Remove quotes. + }, + }; +} diff --git a/packages/codemirror-graphql/tsconfig.esm.json b/packages/codemirror-graphql/tsconfig.esm.json new file mode 100644 index 00000000000..aff3fd6574f --- /dev/null +++ b/packages/codemirror-graphql/tsconfig.esm.json @@ -0,0 +1,25 @@ +{ + "extends": "../../resources/tsconfig.base.esm.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./esm", + "composite": true, + "jsx": "react", + "strictPropertyInitialization": false, + "baseUrl": "." + }, + "include": ["src"], + "exclude": [ + "**/__tests__/**", + "**/dist/**.*", + "**/*.spec.ts", + "**/*.spec.js", + "**/*-test.ts", + "**/*-test.js" + ], + "references": [ + { + "path": "../graphql-language-service" + } + ] +} diff --git a/packages/codemirror-graphql/tsconfig.json b/packages/codemirror-graphql/tsconfig.json new file mode 100644 index 00000000000..661faaba187 --- /dev/null +++ b/packages/codemirror-graphql/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../resources/tsconfig.base.cjs.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./", + "composite": true, + "jsx": "react", + "target": "es5", + "baseUrl": ".", + "strictPropertyInitialization": false + }, + "include": ["src"], + "exclude": [ + "**/__tests__/**", + "**/dist/**.*", + "**/esm/**.*", + "**/*.spec.ts", + "**/*.spec.js", + "**/*-test.ts", + "**/*-test.js" + ], + "references": [ + { + "path": "../graphql-language-service" + } + ] +} diff --git a/packages/graphiql-plugin-code-exporter/CHANGELOG.md b/packages/graphiql-plugin-code-exporter/CHANGELOG.md new file mode 100644 index 00000000000..d7889b83c41 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/CHANGELOG.md @@ -0,0 +1,140 @@ +# @graphiql/plugin-code-exporter + +## 1.0.0 + +### Patch Changes + +- Updated dependencies [[`9a38de29`](https://github.com/graphql/graphiql/commit/9a38de29fddf174ba9e793ac5852407537244f87)]: + - @graphiql/react@0.19.0 + +## 0.3.0 + +### Minor Changes + +- [#3330](https://github.com/graphql/graphiql/pull/3330) [`bed5fc86`](https://github.com/graphql/graphiql/commit/bed5fc86173eb0e770f966fa529ee035b97a1349) Thanks [@acao](https://github.com/acao)! - **BREAKING CHANGE**: fix lifecycle issue in plugin-explorer, change implementation pattern + + `value` and `setValue` is no longer an implementation detail, and are handled internally by plugins. the plugin signature has changed slightly as well. + + now, instead of something like this: + + ```jsx + import { useExplorerPlugin } from '@graphiql/plugin-explorer'; + import { snippets } from './snippets'; + import { useExporterPlugin } from '@graphiql/plugin-code-exporter'; + + const App = () => { + const [query, setQuery] = React.useState(''); + const explorerPlugin = useExplorerPlugin({ + query, + onEdit: setQuery, + }); + const codeExporterPlugin = useExporterPlugin({ + query, + snippets, + }); + + const plugins = React.useMemo( + () => [explorerPlugin, codeExporterPlugin], + [explorerPlugin, codeExporterPlugin], + ); + + return ( + + ); + }; + ``` + + you can just do this: + + ```jsx + import { explorerPlugin } from '@graphiql/plugin-explorer'; + import { snippets } from './snippets'; + import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; + import { createGraphiQLFetcher } from '@graphiql/toolkit'; + + // only invoke these inside the component lifecycle + // if there are dynamic values, and then use useMemo() (see below) + const explorer = explorerPlugin(); + const exporter = codeExporterPlugin({ snippets }); + + const fetcher = createGraphiQLFetcher({ url: '/graphql' }); + + const App = () => { + return ; + }; + ``` + + or this, for more complex state-driven needs: + + ```jsx + import { useMemo } from 'react'; + import { explorerPlugin } from '@graphiql/plugin-explorer'; + import { snippets } from './snippets'; + import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; + + const explorer = explorerPlugin(); + const fetcher = createGraphiQLFetcher({ url: '/graphql' }); + + const App = () => { + const { snippets } = useMyUserSuppliedState(); + const exporter = useMemo( + () => codeExporterPlugin({ snippets }), + [snippets], + ); + + return ; + }; + ``` + +## 0.2.0 + +### Minor Changes + +- [#3293](https://github.com/graphql/graphiql/pull/3293) [`1b8f3fe9`](https://github.com/graphql/graphiql/commit/1b8f3fe9c41697855378ec13a76f1a908fda778a) Thanks [@B2o5T](https://github.com/B2o5T)! - BREAKING CHANGE: umd build was renamed to `index.umd.js` + +## 0.1.4 + +### Patch Changes + +- [#3292](https://github.com/graphql/graphiql/pull/3292) [`f86e4172`](https://github.com/graphql/graphiql/commit/f86e41721d4d990535253b579c810bc5e291b40b) Thanks [@B2o5T](https://github.com/B2o5T)! - fix umd build names `graphiql-plugin-code-exporter.umd.js` and `graphiql-plugin-explorer.umd.js` + +## 0.1.3 + +### Patch Changes + +- [#3229](https://github.com/graphql/graphiql/pull/3229) [`0a65e720`](https://github.com/graphql/graphiql/commit/0a65e7207b6bc4174896f6acca8a40f45d2fb1b8) Thanks [@B2o5T](https://github.com/B2o5T)! - exclude peer dependencies and dependencies from bundle + +- [#3251](https://github.com/graphql/graphiql/pull/3251) [`f8d8509b`](https://github.com/graphql/graphiql/commit/f8d8509b432803eaeb2e53b6b6d4321535e11c1d) Thanks [@B2o5T](https://github.com/B2o5T)! - always bundle `package.json#dependencies` for UMD build for `@graphiql/plugin-code-exporter` and `@graphiql/plugin-explorer` + +- [#3236](https://github.com/graphql/graphiql/pull/3236) [`64da8c30`](https://github.com/graphql/graphiql/commit/64da8c3074628bb411eb1c28aa4738843f60910c) Thanks [@B2o5T](https://github.com/B2o5T)! - update vite + +## 0.1.3-alpha.0 + +### Patch Changes + +- [#3229](https://github.com/graphql/graphiql/pull/3229) [`0a65e720`](https://github.com/graphql/graphiql/commit/0a65e7207b6bc4174896f6acca8a40f45d2fb1b8) Thanks [@B2o5T](https://github.com/B2o5T)! - exclude peer dependencies and dependencies from bundle + +## 0.1.2 + +### Patch Changes + +- [#3017](https://github.com/graphql/graphiql/pull/3017) [`4a2284f5`](https://github.com/graphql/graphiql/commit/4a2284f54809f91d03ba51b9eb4e3ba7b8b7e773) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Avoid bundling code from `react/jsx-runtime` so that the package can be used with Preact + +- [#3063](https://github.com/graphql/graphiql/pull/3063) [`5792aaa5`](https://github.com/graphql/graphiql/commit/5792aaa5b26b68dc396f7bfb5dc3defd9331b831) Thanks [@B2o5T](https://github.com/B2o5T)! - avoid `useMemo` with empty array `[]` since React can't guarantee stable reference, + lint restrict syntax for future mistakes + +## 0.1.1 + +### Patch Changes + +- [#2864](https://github.com/graphql/graphiql/pull/2864) [`f61a5574`](https://github.com/graphql/graphiql/commit/f61a55747a6ff3a125c54e2bf3512f8f4b8f4c50) Thanks [@LekoArts](https://github.com/LekoArts)! - chore(@graphiql/plugin-code-exporter): Fix Typo + +## 0.1.0 + +### Minor Changes + +- [#2758](https://github.com/graphql/graphiql/pull/2758) [`d63801fa`](https://github.com/graphql/graphiql/commit/d63801fad08e840eff7ff26f55694c6d18769466) Thanks [@LekoArts](https://github.com/LekoArts)! - Add code exported plugin diff --git a/packages/graphiql-plugin-code-exporter/README.md b/packages/graphiql-plugin-code-exporter/README.md new file mode 100644 index 00000000000..0b78e2cb558 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/README.md @@ -0,0 +1,105 @@ +# GraphiQL Code Exporter Plugin + +This package provides a plugin that integrates the +[GraphiQL Code Exporter](https://github.com/OneGraph/graphiql-code-exporter) +into the GraphiQL UI. + +## Install + +Use your favorite package manager to install the package: + +```sh +npm i -S @graphiql/plugin-code-exporter +``` + +The following packages are peer dependencies, so make sure you have them +installed as well: + +```sh +npm i -S react react-dom graphql +``` + +## Usage + +See +[GraphiQL Code Exporter README](https://github.com/OneGraph/graphiql-code-exporter) +for all details on available `props` and how to +[create snippets](https://github.com/OneGraph/graphiql-code-exporter#snippets). + +```jsx +import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQL } from 'graphiql'; +import { useState } from 'react'; + +import 'graphiql/graphiql.css'; +import '@graphiql/plugin-code-exporter/dist/style.css'; + +const fetcher = createGraphiQLFetcher({ + url: 'https://swapi-graphql.netlify.app/.netlify/functions/index', +}); + +/* +Example code for snippets. See https://github.com/OneGraph/graphiql-code-exporter#snippets for details +*/ + +const removeQueryName = query => + query.replace( + /^[^{(]+([{(])/, + (_match, openingCurlyBracketsOrParenthesis) => + `query ${openingCurlyBracketsOrParenthesis}`, + ); + +const getQuery = (arg, spaceCount) => { + const { operationDataList } = arg; + const { query } = operationDataList[0]; + const anonymousQuery = removeQueryName(query); + return ( + ' '.repeat(spaceCount) + + anonymousQuery.replaceAll('\n', '\n' + ' '.repeat(spaceCount)) + ); +}; + +const exampleSnippetOne = { + name: 'Example One', + language: 'JavaScript', + codeMirrorMode: 'jsx', + options: [], + generate: arg => `export const query = graphql\` +${getQuery(arg, 2)} +\` +`, +}; + +const exampleSnippetTwo = { + name: 'Example Two', + language: 'JavaScript', + codeMirrorMode: 'jsx', + options: [], + generate: arg => `import { graphql } from 'graphql' + +export const query = graphql\` +${getQuery(arg, 2)} +\` +`, +}; + +const snippets = [exampleSnippetOne, exampleSnippetTwo]; + +const exporter = codeExporterPlugin({ + snippets, + codeMirrorTheme: 'graphiql', +}); + +function GraphiQLWithExplorer() { + return ( + + ); +} +``` + +## CDN bundles + +You can also use this plugin when using the +[CDN bundle](../../examples/graphiql-cdn) to render GraphiQL. Check out the +[example HTML file](examples/index.html) that shows how you can do this. diff --git a/packages/graphiql-plugin-code-exporter/examples/index.html b/packages/graphiql-plugin-code-exporter/examples/index.html new file mode 100644 index 00000000000..d4dce712ec5 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/examples/index.html @@ -0,0 +1,123 @@ + + + + + + + + + + + + +
Loading...
+ + + + + + + + + + diff --git a/packages/graphiql-plugin-code-exporter/package.json b/packages/graphiql-plugin-code-exporter/package.json new file mode 100644 index 00000000000..894e076f9c5 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/package.json @@ -0,0 +1,47 @@ +{ + "name": "@graphiql/plugin-code-exporter", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/graphql/graphiql", + "directory": "packages/graphiql-plugin-code-exporter" + }, + "author": "LekoArts", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "types/index.d.ts", + "license": "MIT", + "keywords": [ + "react", + "graphql", + "graphiql", + "plugin", + "explorer" + ], + "files": [ + "dist", + "src", + "types" + ], + "scripts": { + "dev": "vite", + "build": "tsc --emitDeclarationOnly && node resources/copy-types.mjs && vite build && UMD=true vite build", + "preview": "vite preview" + }, + "dependencies": { + "graphiql-code-exporter": "^3.0.3" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18", + "@graphiql/react": "^0.19.0" + }, + "devDependencies": { + "@graphiql/react": "^0.19.0", + "@vitejs/plugin-react": "^4.0.1", + "postcss-nesting": "^10.1.7", + "typescript": "^4.6.3", + "vite": "^4.3.9" + } +} diff --git a/packages/graphiql-plugin-code-exporter/postcss.config.js b/packages/graphiql-plugin-code-exporter/postcss.config.js new file mode 100644 index 00000000000..1ab3f9d65bf --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require('postcss-nesting')], +}; diff --git a/packages/graphiql-plugin-code-exporter/resources/copy-types.mjs b/packages/graphiql-plugin-code-exporter/resources/copy-types.mjs new file mode 100644 index 00000000000..45e621b507b --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/resources/copy-types.mjs @@ -0,0 +1,11 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const base = path.resolve(path.dirname(__filename), '..'); + +fs.copyFileSync( + path.resolve(base, 'src', 'graphiql-code-exporter.d.ts'), + path.resolve(base, 'types', 'graphiql-code-exporter.d.ts'), +); diff --git a/packages/graphiql-plugin-code-exporter/src/graphiql-code-exporter.d.ts b/packages/graphiql-plugin-code-exporter/src/graphiql-code-exporter.d.ts new file mode 100644 index 00000000000..29264e30216 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/src/graphiql-code-exporter.d.ts @@ -0,0 +1,53 @@ +declare module 'graphiql-code-exporter' { + import { ComponentType } from 'react'; + import { + GraphQLSchema, + OperationTypeNode, + OperationDefinitionNode, + FragmentDefinitionNode, + } from 'graphql'; + + type OperationData = { + query: string; + name: string; + displayName: string; + type: OperationTypeNode | 'fragment'; + variableName: string; + variables: Record; + operationDefinition: OperationDefinitionNode | FragmentDefinitionNode; + fragmentDependencies: Array; + }; + + type GenerateOptions = { + serverUrl: string; + headers: Record; + context: Record; + operationDataList: Array; + options: Record; + }; + + type Snippet = { + language: string; + codeMirrorMode: string; + name: string; + options: Array<{ + id: string; + label: string; + initial: boolean; + }>; + generate: (options: GenerateOptions) => string; + }; + + export type GraphiQLCodeExporterProps = { + query: string; + snippets: Array; + codeMirrorTheme?: string; + variables?: string; + context?: Record; + schema?: GraphQLSchema | null | undefined; + }; + + const GraphiQLCodeExporter: ComponentType; + + export default GraphiQLCodeExporter; +} diff --git a/packages/graphiql-plugin-code-exporter/src/index.css b/packages/graphiql-plugin-code-exporter/src/index.css new file mode 100644 index 00000000000..c139a05d582 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/src/index.css @@ -0,0 +1,140 @@ +.docExplorerWrap { + height: unset !important; + min-width: unset !important; + width: unset !important; +} + +.doc-explorer-title { + font-size: var(--font-size-h2); + font-weight: var(--font-weight-medium); +} + +.doc-explorer-rhs { + display: none; +} + +.doc-explorer-contents { + border-top: none !important; +} + +.graphiql-code-exporter { + min-width: unset !important; + position: relative; + padding: var(--px-16) 0; + & > div { + font-family: var(--font-family) !important; + padding: 0 !important; + font-size: var(--font-size-body) !important; + } + & > div:first-of-type { + display: flex; + flex-direction: column; + gap: var(--px-16); + & > div { + padding: 0 !important; + } + & > div:first-of-type { + display: flex; + flex-direction: row; + gap: var(--px-16); + } + & > div:last-of-type { + & > div:first-of-type { + color: hsla(var(--color-neutral), var(--alpha-secondary)) !important; + font-variant: unset !important; + text-transform: unset !important; + font-weight: unset !important; + margin-bottom: var(--px-12); + } + } + } + & button.toolbar-button { + display: block; + height: var(--toolbar-width) !important; + width: var(--toolbar-width) !important; + border-radius: var(--border-radius-4) !important; + cursor: pointer; + display: inline-flex; + font-size: unset !important; + left: unset !important; + margin-top: unset !important; + top: var(--px-16); + right: 0; + justify-content: center; + align-items: center; + background-color: unset !important; + & svg { + fill: hsla(var(--color-neutral), var(--alpha-tertiary)); + } + } + & > div:last-of-type { + border-top: none !important; + display: flex; + flex: 1; + margin-top: var(--px-24) !important; + & > div { + position: relative; + min-height: 600px; + width: 100%; + } + } + & .toolbar-menu.toolbar-button { + position: relative; + cursor: pointer; + text-decoration: none; + padding: var(--px-8) var(--px-12); + color: hsla(var(--color-neutral), 1) !important; + border-radius: var(--border-radius-4) !important; + &:hover { + background-color: hsla( + var(--color-neutral), + var(--alpha-background-light) + ) !important; + } + } + & .toolbar-menu-items { + background-color: hsl(var(--color-base)) !important; + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow) !important; + padding: var(--px-4); + max-width: 250px; + font-size: inherit; + display: block; + white-space: nowrap; + outline: none; + position: absolute; + z-index: 100; + margin-top: var(--px-8); + visibility: hidden; + left: 0; + &.open { + visibility: visible; + } + & > li { + cursor: pointer; + display: block; + color: inherit; + font: inherit; + text-decoration: initial; + border-radius: var(--border-radius-4); + margin: var(--px-4); + overflow: hidden; + padding: var(--px-6) var(--px-8); + text-overflow: ellipsis; + white-space: nowrap; + &:hover { + color: inherit; + background-color: hsla( + var(--color-neutral), + var(--alpha-background-light) + ); + } + } + } + & .CodeMirror { + box-shadow: var(--popover-box-shadow); + border-radius: calc(var(--border-radius-12)); + padding: var(--px-16); + } +} diff --git a/packages/graphiql-plugin-code-exporter/src/index.tsx b/packages/graphiql-plugin-code-exporter/src/index.tsx new file mode 100644 index 00000000000..7c29254e183 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/src/index.tsx @@ -0,0 +1,48 @@ +import { useEditorContext, type GraphiQLPlugin } from '@graphiql/react'; +import React from 'react'; +import GraphiQLCodeExporter, { + GraphiQLCodeExporterProps, +} from 'graphiql-code-exporter'; + +import './graphiql-code-exporter.d.ts'; +import './index.css'; + +type GraphiQLCodeExporterPluginProps = Omit; + +function GraphiQLCodeExporterPlugin(props: GraphiQLCodeExporterPluginProps) { + const { queryEditor } = useEditorContext({ nonNull: true }); + + return ( + + ); +} + +export function codeExporterPlugin( + props: GraphiQLCodeExporterPluginProps, +): GraphiQLPlugin { + return { + title: 'GraphiQL Code Exporter', + icon: () => ( + + + + ), + content() { + return ; + }, + }; +} diff --git a/packages/graphiql-plugin-code-exporter/src/vite-env.d.ts b/packages/graphiql-plugin-code-exporter/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/graphiql-plugin-code-exporter/tsconfig.json b/packages/graphiql-plugin-code-exporter/tsconfig.json new file mode 100644 index 00000000000..8ad4d4a311c --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationDir": "types", + "jsx": "react" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/graphiql-plugin-code-exporter/tsconfig.node.json b/packages/graphiql-plugin-code-exporter/tsconfig.node.json new file mode 100644 index 00000000000..9d31e2aed93 --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/graphiql-plugin-code-exporter/vite.config.ts b/packages/graphiql-plugin-code-exporter/vite.config.ts new file mode 100644 index 00000000000..aa89e4a0b8c --- /dev/null +++ b/packages/graphiql-plugin-code-exporter/vite.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import packageJSON from './package.json'; + +const IS_UMD = process.env.UMD === 'true'; + +export default defineConfig({ + plugins: [react({ jsxRuntime: 'classic' })], + build: { + // avoid clean cjs/es builds + emptyOutDir: !IS_UMD, + lib: { + entry: 'src/index.tsx', + fileName: 'index', + name: 'GraphiQLPluginCodeExporter', + formats: IS_UMD ? ['umd'] : ['cjs', 'es'], + }, + rollupOptions: { + external: [ + // Exclude peer dependencies and dependencies from bundle + ...Object.keys(packageJSON.peerDependencies), + ...(IS_UMD ? [] : Object.keys(packageJSON.dependencies)), + ], + output: { + chunkFileNames: '[name].[format].js', + globals: { + graphql: 'GraphiQL.GraphQL', + react: 'React', + 'react-dom': 'ReactDOM', + }, + }, + }, + commonjsOptions: { + esmExternals: true, + requireReturnsDefault: 'auto', + }, + }, +}); diff --git a/packages/graphiql-plugin-explorer/CHANGELOG.md b/packages/graphiql-plugin-explorer/CHANGELOG.md new file mode 100644 index 00000000000..e6eb67254af --- /dev/null +++ b/packages/graphiql-plugin-explorer/CHANGELOG.md @@ -0,0 +1,307 @@ +# @graphiql/plugin-explorer + +## 1.0.0 + +### Patch Changes + +- [#3340](https://github.com/graphql/graphiql/pull/3340) [`59a898c0`](https://github.com/graphql/graphiql/commit/59a898c0ac5c7c78a4b81bb5d520bf18e10c880a) Thanks [@acao](https://github.com/acao)! - handle null editor in explorer plugin + +- Updated dependencies [[`9a38de29`](https://github.com/graphql/graphiql/commit/9a38de29fddf174ba9e793ac5852407537244f87)]: + - @graphiql/react@0.19.0 + +## 0.3.0 + +### Minor Changes + +- [#3330](https://github.com/graphql/graphiql/pull/3330) [`bed5fc86`](https://github.com/graphql/graphiql/commit/bed5fc86173eb0e770f966fa529ee035b97a1349) Thanks [@acao](https://github.com/acao)! - **BREAKING CHANGE**: fix lifecycle issue in plugin-explorer, change implementation pattern + + `value` and `setValue` is no longer an implementation detail, and are handled internally by plugins. the plugin signature has changed slightly as well. + + now, instead of something like this: + + ```jsx + import { useExplorerPlugin } from '@graphiql/plugin-explorer'; + import { snippets } from './snippets'; + import { useExporterPlugin } from '@graphiql/plugin-code-exporter'; + + const App = () => { + const [query, setQuery] = React.useState(''); + const explorerPlugin = useExplorerPlugin({ + query, + onEdit: setQuery, + }); + const codeExporterPlugin = useExporterPlugin({ + query, + snippets, + }); + + const plugins = React.useMemo( + () => [explorerPlugin, codeExporterPlugin], + [explorerPlugin, codeExporterPlugin], + ); + + return ( + + ); + }; + ``` + + you can just do this: + + ```jsx + import { explorerPlugin } from '@graphiql/plugin-explorer'; + import { snippets } from './snippets'; + import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; + import { createGraphiQLFetcher } from '@graphiql/toolkit'; + + // only invoke these inside the component lifecycle + // if there are dynamic values, and then use useMemo() (see below) + const explorer = explorerPlugin(); + const exporter = codeExporterPlugin({ snippets }); + + const fetcher = createGraphiQLFetcher({ url: '/graphql' }); + + const App = () => { + return ; + }; + ``` + + or this, for more complex state-driven needs: + + ```jsx + import { useMemo } from 'react'; + import { explorerPlugin } from '@graphiql/plugin-explorer'; + import { snippets } from './snippets'; + import { codeExporterPlugin } from '@graphiql/plugin-code-exporter'; + + const explorer = explorerPlugin(); + const fetcher = createGraphiQLFetcher({ url: '/graphql' }); + + const App = () => { + const { snippets } = useMyUserSuppliedState(); + const exporter = useMemo( + () => codeExporterPlugin({ snippets }), + [snippets], + ); + + return ; + }; + ``` + +## 0.2.0 + +### Minor Changes + +- [#3293](https://github.com/graphql/graphiql/pull/3293) [`1b8f3fe9`](https://github.com/graphql/graphiql/commit/1b8f3fe9c41697855378ec13a76f1a908fda778a) Thanks [@B2o5T](https://github.com/B2o5T)! - BREAKING CHANGE: umd build was renamed to `index.umd.js` + +### Patch Changes + +- [#3319](https://github.com/graphql/graphiql/pull/3319) [`2f51b1a5`](https://github.com/graphql/graphiql/commit/2f51b1a5f25ac515af89b708c009796c57a611fb) Thanks [@LekoArts](https://github.com/LekoArts)! - Use named `Explorer` import from `graphiql-explorer` to fix an issue where the bundler didn't correctly choose either the `default` or `Explorer` import. This change should ensure that `@graphiql/plugin-explorer` works correctly without `graphiql-explorer` being bundled. + +## 0.1.22 + +### Patch Changes + +- [#3292](https://github.com/graphql/graphiql/pull/3292) [`f86e4172`](https://github.com/graphql/graphiql/commit/f86e41721d4d990535253b579c810bc5e291b40b) Thanks [@B2o5T](https://github.com/B2o5T)! - fix umd build names `graphiql-plugin-code-exporter.umd.js` and `graphiql-plugin-explorer.umd.js` + +## 0.1.21 + +### Patch Changes + +- [#3229](https://github.com/graphql/graphiql/pull/3229) [`0a65e720`](https://github.com/graphql/graphiql/commit/0a65e7207b6bc4174896f6acca8a40f45d2fb1b8) Thanks [@B2o5T](https://github.com/B2o5T)! - exclude peer dependencies and dependencies from bundle + +- [#3251](https://github.com/graphql/graphiql/pull/3251) [`f8d8509b`](https://github.com/graphql/graphiql/commit/f8d8509b432803eaeb2e53b6b6d4321535e11c1d) Thanks [@B2o5T](https://github.com/B2o5T)! - always bundle `package.json#dependencies` for UMD build for `@graphiql/plugin-code-exporter` and `@graphiql/plugin-explorer` + +- [#3236](https://github.com/graphql/graphiql/pull/3236) [`64da8c30`](https://github.com/graphql/graphiql/commit/64da8c3074628bb411eb1c28aa4738843f60910c) Thanks [@B2o5T](https://github.com/B2o5T)! - update vite + +- [#3252](https://github.com/graphql/graphiql/pull/3252) [`c915a4ee`](https://github.com/graphql/graphiql/commit/c915a4eead4ae39cb5c9fa615b5b55945da06c01) Thanks [@B2o5T](https://github.com/B2o5T)! - `@graphiql/react` should be in `peerDependencies` not in `dependencies` + +- Updated dependencies [[`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`bc9d243d`](https://github.com/graphql/graphiql/commit/bc9d243d40b95f95fc9d00d25aa0dd1733952626), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`67bf93a3`](https://github.com/graphql/graphiql/commit/67bf93a33e98c60ae3a686063a1c47037f88ef49)]: + - @graphiql/react@0.18.0 + +## 0.1.21-alpha.1 + +### Patch Changes + +- [#3229](https://github.com/graphql/graphiql/pull/3229) [`0a65e720`](https://github.com/graphql/graphiql/commit/0a65e7207b6bc4174896f6acca8a40f45d2fb1b8) Thanks [@B2o5T](https://github.com/B2o5T)! - exclude peer dependencies and dependencies from bundle + +- Updated dependencies [[`bc9d243d`](https://github.com/graphql/graphiql/commit/bc9d243d40b95f95fc9d00d25aa0dd1733952626), [`67bf93a3`](https://github.com/graphql/graphiql/commit/67bf93a33e98c60ae3a686063a1c47037f88ef49)]: + - @graphiql/react@0.18.0-alpha.1 + +## 0.1.21-alpha.0 + +### Patch Changes + +- Updated dependencies [[`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696), [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696)]: + - @graphiql/react@0.18.0-alpha.0 + +## 0.1.20 + +### Patch Changes + +- [#3124](https://github.com/graphql/graphiql/pull/3124) [`c645932c`](https://github.com/graphql/graphiql/commit/c645932c7973e11ad917e1d1d897fd409f8c042f) Thanks [@B2o5T](https://github.com/B2o5T)! - avoid unecessary renders by using useMemo or useCallback + +- Updated dependencies [[`911cf3e0`](https://github.com/graphql/graphiql/commit/911cf3e0b0fa13268245463c8db8299279e5c461), [`c645932c`](https://github.com/graphql/graphiql/commit/c645932c7973e11ad917e1d1d897fd409f8c042f), [`2ca4841b`](https://github.com/graphql/graphiql/commit/2ca4841baf74e87a3f067b3415f8da3347ee3898), [`7bf90929`](https://github.com/graphql/graphiql/commit/7bf90929f62ba812c0946e0424f9f843f7b6b0ff), [`431b7fe1`](https://github.com/graphql/graphiql/commit/431b7fe1efefa4867f0ea617adc436b1117052e8)]: + - @graphiql/react@0.17.6 + +## 0.1.19 + +### Patch Changes + +- Updated dependencies [[`2b212941`](https://github.com/graphql/graphiql/commit/2b212941628498957d95ee89a7a5a0623f391b7a), [`9b333a04`](https://github.com/graphql/graphiql/commit/9b333a047d6b75db7681f484156d8772e9f91810)]: + - @graphiql/react@0.17.5 + +## 0.1.18 + +### Patch Changes + +- Updated dependencies [[`707f3cbc`](https://github.com/graphql/graphiql/commit/707f3cbca3ac2ce186058e7d2b145cdf69bf7d9c)]: + - @graphiql/react@0.17.4 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies []: + - @graphiql/react@0.17.3 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies [[`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9), [`4879984e`](https://github.com/graphql/graphiql/commit/4879984ea1803a6e9f97d81c97e8ba27aacddae9), [`51007002`](https://github.com/graphql/graphiql/commit/510070028b7d8e98f2ba25f396519976aea5fa4b)]: + - @graphiql/react@0.17.2 + +## 0.1.15 + +### Patch Changes + +- [#3017](https://github.com/graphql/graphiql/pull/3017) [`4a2284f5`](https://github.com/graphql/graphiql/commit/4a2284f54809f91d03ba51b9eb4e3ba7b8b7e773) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Avoid bundling code from `react/jsx-runtime` so that the package can be used with Preact + +- [#3063](https://github.com/graphql/graphiql/pull/3063) [`5792aaa5`](https://github.com/graphql/graphiql/commit/5792aaa5b26b68dc396f7bfb5dc3defd9331b831) Thanks [@B2o5T](https://github.com/B2o5T)! - avoid `useMemo` with empty array `[]` since React can't guarantee stable reference, + lint restrict syntax for future mistakes + +- Updated dependencies [[`2d5c60ec`](https://github.com/graphql/graphiql/commit/2d5c60ecf717abafde2bddd32b2772261d3eec8b), [`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d), [`4a2284f5`](https://github.com/graphql/graphiql/commit/4a2284f54809f91d03ba51b9eb4e3ba7b8b7e773), [`881a2024`](https://github.com/graphql/graphiql/commit/881a202497d5a58eb5260a5aa54c0c88930d69a0), [`7cf4908a`](https://github.com/graphql/graphiql/commit/7cf4908a5d4bd58af315047f4dec5236e8c701fc)]: + - @graphiql/react@0.17.1 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies [[`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4), [`65f5176a`](https://github.com/graphql/graphiql/commit/65f5176a408cfbbc514ca60e2e4bd2ea133a8b0b)]: + - @graphiql/react@0.17.0 + +## 0.1.13 + +### Patch Changes + +- Updated dependencies [[`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147), [`cec3fb2a`](https://github.com/graphql/graphiql/commit/cec3fb2a493c4a0c40df7dfad04e1a95ed35e786), [`11e6ad11`](https://github.com/graphql/graphiql/commit/11e6ad11e745c671eb320731697887bb8d7177b7), [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9), [`d502a33b`](https://github.com/graphql/graphiql/commit/d502a33b4332f1025e947c02d7cfdc5799365c8d), [`0669767e`](https://github.com/graphql/graphiql/commit/0669767e1e2196a78cbefe3679a52bcbb341e913), [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215), [`ccba2f33`](https://github.com/graphql/graphiql/commit/ccba2f33b67a03f492222f7afde1354cfd033b42), [`4ff2794c`](https://github.com/graphql/graphiql/commit/4ff2794c8b6032168e27252096cb276ce712878e)]: + - @graphiql/react@0.16.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies [[`16174a05`](https://github.com/graphql/graphiql/commit/16174a053ed89fb9554d096395ab7bf69c8f6911), [`f6cae4ea`](https://github.com/graphql/graphiql/commit/f6cae4eaa0258ea7fcde97ba6368830955f0abf4), [`3340fd74`](https://github.com/graphql/graphiql/commit/3340fd745e181ba8f1f5a6ed002a04d253a78d4a), [`0851d5f9`](https://github.com/graphql/graphiql/commit/0851d5f9ecf709597d0a698609d88f99c4395665), [`83364b28`](https://github.com/graphql/graphiql/commit/83364b28020b5946ed58908d6d977f1de766e75d), [`3a7d0007`](https://github.com/graphql/graphiql/commit/3a7d00071922e2005777c92daf6ad0c1ce3e2816)]: + - @graphiql/react@0.15.0 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies [[`29630c22`](https://github.com/graphql/graphiql/commit/29630c2219bca8b825ab0897840864364a9de2e8), [`8f926489`](https://github.com/graphql/graphiql/commit/8f9264896e9971951853463a283a90ba3d1310ef), [`2ba2f620`](https://github.com/graphql/graphiql/commit/2ba2f620b6e7de3ae6b5ea641f33e600f7f44e08)]: + - @graphiql/react@0.14.0 + +## 0.1.10 + +### Patch Changes + +- Updated dependencies []: + - @graphiql/react@0.13.7 + +## 0.1.9 + +### Patch Changes + +- Updated dependencies []: + - @graphiql/react@0.13.6 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies [[`682ad06e`](https://github.com/graphql/graphiql/commit/682ad06e58ded2f82fa973e8e6613dd654417fe2)]: + - @graphiql/react@0.13.5 + +## 0.1.7 + +### Patch Changes + +- Updated dependencies [[`4e2f7ff9`](https://github.com/graphql/graphiql/commit/4e2f7ff99c578ceae54a1ae17c02088bd91b89c3)]: + - @graphiql/react@0.13.4 + +## 0.1.6 + +### Patch Changes + +- Updated dependencies [[`42700076`](https://github.com/graphql/graphiql/commit/4270007671ce52f6c2250739916083611748b657), [`36839800`](https://github.com/graphql/graphiql/commit/36839800de128b05d11c262036c8240390c72a14), [`905f2e5e`](https://github.com/graphql/graphiql/commit/905f2e5ea3f0b304d27ea583e250ed4baff5016e)]: + - @graphiql/react@0.13.3 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies [[`39b4668d`](https://github.com/graphql/graphiql/commit/39b4668d43176526d37ecf07d8c86901d53e0d80)]: + - @graphiql/react@0.13.2 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies []: + - @graphiql/react@0.13.1 + +## 0.1.3 + +### Patch Changes + +- [#2735](https://github.com/graphql/graphiql/pull/2735) [`ca067d88`](https://github.com/graphql/graphiql/commit/ca067d88148c5d221d196790a997ad599038fad1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Use the new CSS variables for color alpha values defined in `@graphiql/react` in style definitions + +* [#2757](https://github.com/graphql/graphiql/pull/2757) [`32a70065`](https://github.com/graphql/graphiql/commit/32a70065434eaa7733e28cda0ea0e7d51952e62a) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Use different colors for field names and argument names + +* Updated dependencies [[`ca067d88`](https://github.com/graphql/graphiql/commit/ca067d88148c5d221d196790a997ad599038fad1), [`32a70065`](https://github.com/graphql/graphiql/commit/32a70065434eaa7733e28cda0ea0e7d51952e62a)]: + - @graphiql/react@0.13.0 + +## 0.1.2 + +### Patch Changes + +- [#2750](https://github.com/graphql/graphiql/pull/2750) [`cdc44aab`](https://github.com/graphql/graphiql/commit/cdc44aabdc549f5a0359b8f69506cc0c31661d16) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Remove `type` field from `package.json` to support both ES Modules and CommonJS + +- Updated dependencies []: + - @graphiql/react@0.12.1 + +## 0.1.1 + +### Patch Changes + +- [#2745](https://github.com/graphql/graphiql/pull/2745) [`92a17490`](https://github.com/graphql/graphiql/commit/92a17490c3842b4f83ed1065b73a803f73d02a17) Thanks [@acao](https://github.com/acao)! - Specify MIT license for `@graphiql/plugin-explorer` `package.json` + +* [#2731](https://github.com/graphql/graphiql/pull/2731) [`3e8f0d1f`](https://github.com/graphql/graphiql/commit/3e8f0d1fe4da5cdea94240119bbad587720ca324) Thanks [@hasparus](https://github.com/hasparus)! - Expose typings for graphiql-explorer + +- [#2738](https://github.com/graphql/graphiql/pull/2738) [`33bef178`](https://github.com/graphql/graphiql/commit/33bef17832edb29f5b26f4ed1cf33fd0d7fbbed1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix peer dependency versions + +* [#2747](https://github.com/graphql/graphiql/pull/2747) [`52d0003f`](https://github.com/graphql/graphiql/commit/52d0003fd0c405da65b7b23dcfed9f3aacbad067) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Make `@graphiql/react` a real dependency instead of a peer dependency + +* Updated dependencies [[`98e14155`](https://github.com/graphql/graphiql/commit/98e14155c650ee7c5ac639e594eb47f0052b7fa9), [`7dfea94a`](https://github.com/graphql/graphiql/commit/7dfea94afc0cfe79b5080f10d840bfdce53f02d7), [`3aa1f39f`](https://github.com/graphql/graphiql/commit/3aa1f39f6df559b54f703937ed510c8ba1f21058), [`0219eef3`](https://github.com/graphql/graphiql/commit/0219eef39146495749aca2487112db52fa3bb8fd)]: + - @graphiql/react@0.12.0 + +## 0.1.0 + +### Minor Changes + +- [#2724](https://github.com/graphql/graphiql/pull/2724) [`dd5db3b2`](https://github.com/graphql/graphiql/commit/dd5db3b2ee08b240ba7b77a9b7ff621115bd25f3) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a package that exports a plugin to use the GraphiQL Explorer from OneGraph diff --git a/packages/graphiql-plugin-explorer/README.md b/packages/graphiql-plugin-explorer/README.md new file mode 100644 index 00000000000..c6b5af09899 --- /dev/null +++ b/packages/graphiql-plugin-explorer/README.md @@ -0,0 +1,54 @@ +# GraphiQL Explorer Plugin + +This package provides a plugin that integrated the +[`GraphiQL Explorer`](https://github.com/OneGraph/graphiql-explorer) into the +GraphiQL UI. + +## Install + +Use your favorite package manager to install the package: + +```sh +npm i -S @graphiql/plugin-explorer +``` + +The following packages are peer dependencies, so make sure you have them +installed as well: + +```sh +npm i -S react react-dom graphql +``` + +## Usage + +```jsx +import { explorerPlugin } from '@graphiql/plugin-explorer'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQL } from 'graphiql'; +import { useState } from 'react'; + +import 'graphiql/graphiql.css'; +import '@graphiql/plugin-explorer/dist/style.css'; + +const fetcher = createGraphiQLFetcher({ + url: 'https://swapi-graphql.netlify.app/.netlify/functions/index', +}); + +// pass the explorer props here if you want +const explorer = explorerPlugin(); + +return ( + +); +``` + +## CDN bundles + +You can also use add this plugin when using the +[CDN bundle](../../examples/graphiql-cdn) to render GraphiQL. Check out the +[example HTML file](examples/index.html) that shows how you can do this. diff --git a/packages/graphiql-plugin-explorer/examples/index.html b/packages/graphiql-plugin-explorer/examples/index.html new file mode 100644 index 00000000000..442fc3aa580 --- /dev/null +++ b/packages/graphiql-plugin-explorer/examples/index.html @@ -0,0 +1,76 @@ + + + + + + + + + + + + +
Loading...
+ + + + + + + + + + diff --git a/packages/graphiql-plugin-explorer/package.json b/packages/graphiql-plugin-explorer/package.json new file mode 100644 index 00000000000..2fbad419e6a --- /dev/null +++ b/packages/graphiql-plugin-explorer/package.json @@ -0,0 +1,45 @@ +{ + "name": "@graphiql/plugin-explorer", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/graphql/graphiql", + "directory": "packages/graphiql-plugin-explorer" + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "types/index.d.ts", + "license": "MIT", + "keywords": [ + "react", + "graphql", + "graphiql", + "plugin", + "explorer" + ], + "files": [ + "dist", + "src", + "types" + ], + "scripts": { + "dev": "vite", + "build": "tsc --emitDeclarationOnly && node resources/copy-types.mjs && vite build && UMD=true vite build", + "preview": "vite preview" + }, + "dependencies": { + "graphiql-explorer": "^0.9.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18", + "@graphiql/react": "^0.19.0" + }, + "devDependencies": { + "@graphiql/react": "^0.19.0", + "@vitejs/plugin-react": "^4.0.1", + "typescript": "^4.6.3", + "vite": "^4.3.9" + } +} diff --git a/packages/graphiql-plugin-explorer/resources/copy-types.mjs b/packages/graphiql-plugin-explorer/resources/copy-types.mjs new file mode 100644 index 00000000000..8f6175f68c7 --- /dev/null +++ b/packages/graphiql-plugin-explorer/resources/copy-types.mjs @@ -0,0 +1,11 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const base = path.resolve(path.dirname(__filename), '..'); + +fs.copyFileSync( + path.resolve(base, 'src', 'graphiql-explorer.d.ts'), + path.resolve(base, 'types', 'graphiql-explorer.d.ts'), +); diff --git a/packages/graphiql-plugin-explorer/src/graphiql-explorer.d.ts b/packages/graphiql-plugin-explorer/src/graphiql-explorer.d.ts new file mode 100644 index 00000000000..66fffce659a --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/graphiql-explorer.d.ts @@ -0,0 +1,67 @@ +declare module 'graphiql-explorer' { + import { + FragmentDefinitionNode, + GraphQLArgument, + GraphQLField, + GraphQLInputField, + GraphQLLeafType, + GraphQLObjectType, + GraphQLSchema, + ValueNode, + } from 'graphql'; + import { ComponentType, ReactNode, CSSProperties } from 'react'; + + export type GraphiQLExplorerProps = { + query: string; + width?: number; + title?: string; + schema?: GraphQLSchema | null; + onEdit?(newQuery: string): void; + getDefaultFieldNames?(type: GraphQLObjectType): string[]; + getDefaultScalarArgValue?( + parentField: GraphQLField, + arg: GraphQLArgument | GraphQLInputField, + underlyingArgType: GraphQLLeafType, + ): ValueNode; + makeDefaultArg?( + parentField: GraphQLField, + arg: GraphQLArgument | GraphQLInputField, + ): boolean; + onToggleExplorer?(): void; + explorerIsOpen?: boolean; + onRunOperation?(name: string | null): void; + colors?: { + keyword: string; + def: string; + property: string; + qualifier: string; + attribute: string; + number: string; + string: string; + builtin: string; + string2: string; + variable: string; + atom: string; + } | null; + arrowOpen?: ReactNode; + arrowClosed?: ReactNode; + checkboxChecked?: ReactNode; + checkboxUnchecked?: ReactNode; + styles?: { + explorerActionsStyle?: CSSProperties; + buttonStyle?: CSSProperties; + actionButtonStyle?: CSSProperties; + } | null; + showAttribution: boolean; + hideActions?: boolean; + externalFragments?: FragmentDefinitionNode[]; + }; + + const GraphiQLExplorer: ComponentType & { + defaultValue: (arg: GraphQLLeafType) => ValueNode; + }; + + export { GraphiQLExplorer as Explorer }; + + export default GraphiQLExplorer; +} diff --git a/packages/graphiql-plugin-explorer/src/index.css b/packages/graphiql-plugin-explorer/src/index.css new file mode 100644 index 00000000000..a15d245283d --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/index.css @@ -0,0 +1,37 @@ +.docExplorerWrap { + height: unset !important; + min-width: unset !important; + width: unset !important; +} + +.doc-explorer-title { + font-size: var(--font-size-h2); + font-weight: var(--font-weight-medium); +} + +.doc-explorer-rhs { + display: none; +} + +.graphiql-explorer-root { + font-family: var(--font-family-mono) !important; + font-size: var(--font-size-body) !important; + padding: 0 !important; +} + +.graphiql-explorer-root > div:first-child { + padding-left: var(--px-8); +} + +.graphiql-explorer-root input { + background: hsl(var(--color-base)); +} + +.graphiql-explorer-root select { + background-color: hsl(var(--color-base)); + border: 1px solid hsla(var(--color-neutral), var(--alpha-secondary)); + border-radius: var(--border-radius-4); + color: hsl(var(--color-neutral)); + margin: 0 var(--px-4); + padding: var(--px-4) var(--px-6); +} diff --git a/packages/graphiql-plugin-explorer/src/index.tsx b/packages/graphiql-plugin-explorer/src/index.tsx new file mode 100644 index 00000000000..9851f67fd58 --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/index.tsx @@ -0,0 +1,189 @@ +import { + GraphiQLPlugin, + useEditorContext, + useExecutionContext, + useSchemaContext, +} from '@graphiql/react'; +import { + Explorer as GraphiQLExplorer, + GraphiQLExplorerProps, +} from 'graphiql-explorer'; +import React, { useCallback } from 'react'; + +import './graphiql-explorer.d.ts'; +import './index.css'; + +const colors = { + keyword: 'hsl(var(--color-primary))', + def: 'hsl(var(--color-tertiary))', + property: 'hsl(var(--color-info))', + qualifier: 'hsl(var(--color-secondary))', + attribute: 'hsl(var(--color-tertiary))', + number: 'hsl(var(--color-success))', + string: 'hsl(var(--color-warning))', + builtin: 'hsl(var(--color-success))', + string2: 'hsl(var(--color-secondary))', + variable: 'hsl(var(--color-secondary))', + atom: 'hsl(var(--color-tertiary))', +}; + +const arrowOpen = ( + + + +); + +const arrowClosed = ( + + + +); + +const checkboxUnchecked = ( + + + +); + +const checkboxChecked = ( + + + + +); + +const styles = { + buttonStyle: { + backgroundColor: 'transparent', + border: 'none', + color: 'hsla(var(--color-neutral), var(--alpha-secondary, 0.6))', + cursor: 'pointer', + fontSize: '1em', + }, + explorerActionsStyle: { + padding: 'var(--px-8) var(--px-4)', + }, + actionButtonStyle: { + backgroundColor: 'transparent', + border: 'none', + color: 'hsla(var(--color-neutral), var(--alpha-secondary, 0.6))', + cursor: 'pointer', + fontSize: '1em', + }, +}; + +export type GraphiQLExplorerPluginProps = Omit< + Omit, + 'onEdit' +>; + +function ExplorerPlugin(props: GraphiQLExplorerPluginProps) { + const { setOperationName, queryEditor } = useEditorContext({ nonNull: true }); + const { schema } = useSchemaContext({ nonNull: true }); + const { run } = useExecutionContext({ nonNull: true }); + const handleRunOperation = useCallback( + (operationName: string | null) => { + if (operationName) { + setOperationName(operationName); + } + run(); + }, + [run, setOperationName], + ); + // todo: document how to do this! + const handleEditOperation = useCallback( + (value: string) => queryEditor?.setValue(value), + [queryEditor], + ); + + const operationDocument = queryEditor?.getValue() ?? ''; + + return ( + + ); +} + +export function explorerPlugin(props: GraphiQLExplorerPluginProps) { + return { + title: 'GraphiQL Explorer', + icon: () => ( + + + + + + ), + content: () => , + } as GraphiQLPlugin; +} diff --git a/packages/graphiql-plugin-explorer/src/vite-env.d.ts b/packages/graphiql-plugin-explorer/src/vite-env.d.ts new file mode 100644 index 00000000000..11f02fe2a00 --- /dev/null +++ b/packages/graphiql-plugin-explorer/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/graphiql-plugin-explorer/tsconfig.json b/packages/graphiql-plugin-explorer/tsconfig.json new file mode 100644 index 00000000000..8ad4d4a311c --- /dev/null +++ b/packages/graphiql-plugin-explorer/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "declaration": true, + "declarationDir": "types", + "jsx": "react" + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/graphiql-plugin-explorer/tsconfig.node.json b/packages/graphiql-plugin-explorer/tsconfig.node.json new file mode 100644 index 00000000000..9d31e2aed93 --- /dev/null +++ b/packages/graphiql-plugin-explorer/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/graphiql-plugin-explorer/vite.config.ts b/packages/graphiql-plugin-explorer/vite.config.ts new file mode 100644 index 00000000000..cd4a63c8c99 --- /dev/null +++ b/packages/graphiql-plugin-explorer/vite.config.ts @@ -0,0 +1,39 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import packageJSON from './package.json'; + +const IS_UMD = process.env.UMD === 'true'; + +export default defineConfig({ + plugins: [react({ jsxRuntime: 'classic' })], + build: { + // avoid clean cjs/es builds + emptyOutDir: !IS_UMD, + lib: { + entry: 'src/index.tsx', + fileName: 'index', + name: 'GraphiQLPluginExplorer', + formats: IS_UMD ? ['umd'] : ['cjs', 'es'], + }, + rollupOptions: { + external: [ + // Exclude peer dependencies and dependencies from bundle + ...Object.keys(packageJSON.peerDependencies), + ...(IS_UMD ? [] : Object.keys(packageJSON.dependencies)), + ], + output: { + chunkFileNames: '[name].[format].js', + globals: { + '@graphiql/react': 'GraphiQL.React', + graphql: 'GraphiQL.GraphQL', + react: 'React', + 'react-dom': 'ReactDOM', + }, + }, + }, + commonjsOptions: { + esmExternals: true, + requireReturnsDefault: 'auto', + }, + }, +}); diff --git a/packages/graphiql-react/CHANGELOG.md b/packages/graphiql-react/CHANGELOG.md new file mode 100644 index 00000000000..2252a518c16 --- /dev/null +++ b/packages/graphiql-react/CHANGELOG.md @@ -0,0 +1,545 @@ +# @graphiql/react + +## 0.19.0 + +### Minor Changes + +- [#3130](https://github.com/graphql/graphiql/pull/3130) [`9a38de29`](https://github.com/graphql/graphiql/commit/9a38de29fddf174ba9e793ac5852407537244f87) Thanks [@lesleydreyer](https://github.com/lesleydreyer)! - - Add a "clear history" button to clear all history as well as trash icons to clear individual history items + + - Change so item is in history items or history favorites, not both + - Fix history label editing so if the same item is in the list more than once it edits the correct label + - Pass the entire history item in history functions (addToHistory, editLabel, toggleFavorite, etc.) so users building their own HistoryContext.Provider will get any additional props they added to the item in their customized functions + - Adds a "setActive" callback users can use to customize their UI when the history item is clicked + +## 0.18.0 + +### Minor Changes + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - remove `initialTabs`, use `defaultTabs` instead + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/dialog` by `@radix-ui/react-dialog` replace `@reach/visually-hidden` by `@radix-ui/react-visually-hidden` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/menu-button` by `@radix-ui/react-dropdown-menu` remove `@reach/listbox` remove `` and `` components (use `` instead) + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - fixed long list items in dropdown were hidden + + rename `` to `` rename `` to `` rename `` to `` rename `` to `` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/tooltip` by `@radix-ui/react-tooltip` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/combobox` with `Combobox` from `@headlessui/react` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - tabs could be reorderded + +### Patch Changes + +- [#2716](https://github.com/graphql/graphiql/pull/2716) [`bc9d243d`](https://github.com/graphql/graphiql/commit/bc9d243d40b95f95fc9d00d25aa0dd1733952626) Thanks [@SimenB](https://github.com/SimenB)! - Make `@types/codemirror` a dependency of `@graphiql/react` + +- [#3228](https://github.com/graphql/graphiql/pull/3228) [`67bf93a3`](https://github.com/graphql/graphiql/commit/67bf93a33e98c60ae3a686063a1c47037f88ef49) Thanks [@B2o5T](https://github.com/B2o5T)! - exclude peer dependencies and dependencies from bundle + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5), [`61986469`](https://github.com/graphql/graphiql/commit/619864691941c46cc0b0848e8713028e20212c36)]: + - graphql-language-service@5.1.7 + - codemirror-graphql@2.0.9 + +## 0.18.0-alpha.1 + +### Patch Changes + +- [#2716](https://github.com/graphql/graphiql/pull/2716) [`bc9d243d`](https://github.com/graphql/graphiql/commit/bc9d243d40b95f95fc9d00d25aa0dd1733952626) Thanks [@SimenB](https://github.com/SimenB)! - Make `@types/codemirror` a dependency of `@graphiql/react` + +- [#3228](https://github.com/graphql/graphiql/pull/3228) [`67bf93a3`](https://github.com/graphql/graphiql/commit/67bf93a33e98c60ae3a686063a1c47037f88ef49) Thanks [@B2o5T](https://github.com/B2o5T)! - exclude peer dependencies and dependencies from bundle + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5)]: + - graphql-language-service@5.1.7-alpha.0 + - codemirror-graphql@2.0.9-alpha.1 + +## 0.18.0-alpha.0 + +### Minor Changes + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - remove `initialTabs`, use `defaultTabs` instead + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/dialog` by `@radix-ui/react-dialog` replace `@reach/visually-hidden` by `@radix-ui/react-visually-hidden` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/menu-button` by `@radix-ui/react-dropdown-menu` remove `@reach/listbox` remove `` and `` components (use `` instead) + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - fixed long list items in dropdown were hidden + + rename `` to `` rename `` to `` rename `` to `` rename `` to `` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/tooltip` by `@radix-ui/react-tooltip` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `@reach/combobox` with `Combobox` from `@headlessui/react` + +- [#3181](https://github.com/graphql/graphiql/pull/3181) [`9ac84bfc`](https://github.com/graphql/graphiql/commit/9ac84bfc7b847105565852a01bdca122319e3696) Thanks [@B2o5T](https://github.com/B2o5T)! - tabs could be reorderded + +### Patch Changes + +- Updated dependencies [[`61986469`](https://github.com/graphql/graphiql/commit/619864691941c46cc0b0848e8713028e20212c36)]: + - codemirror-graphql@2.0.9-alpha.0 + +## 0.17.6 + +### Patch Changes + +- [#3194](https://github.com/graphql/graphiql/pull/3194) [`911cf3e0`](https://github.com/graphql/graphiql/commit/911cf3e0b0fa13268245463c8db8299279e5c461) Thanks [@dwwoelfel](https://github.com/dwwoelfel)! - fix tab content getting replaced on `changeTab` + +- [#3124](https://github.com/graphql/graphiql/pull/3124) [`c645932c`](https://github.com/graphql/graphiql/commit/c645932c7973e11ad917e1d1d897fd409f8c042f) Thanks [@B2o5T](https://github.com/B2o5T)! - avoid unecessary renders by using useMemo or useCallback + +- [#3197](https://github.com/graphql/graphiql/pull/3197) [`2ca4841b`](https://github.com/graphql/graphiql/commit/2ca4841baf74e87a3f067b3415f8da3347ee3898) Thanks [@B2o5T](https://github.com/B2o5T)! - remove confusing ligatures, set `font-variant-ligatures: none` + +- [#3136](https://github.com/graphql/graphiql/pull/3136) [`7bf90929`](https://github.com/graphql/graphiql/commit/7bf90929f62ba812c0946e0424f9f843f7b6b0ff) Thanks [@B2o5T](https://github.com/B2o5T)! - replace rest of `event.keyCode` usages by `event.code` + +- [#3118](https://github.com/graphql/graphiql/pull/3118) [`431b7fe1`](https://github.com/graphql/graphiql/commit/431b7fe1efefa4867f0ea617adc436b1117052e8) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer `.textContent` over `.innerText` + +## 0.17.5 + +### Patch Changes + +- [#3147](https://github.com/graphql/graphiql/pull/3147) [`2b212941`](https://github.com/graphql/graphiql/commit/2b212941628498957d95ee89a7a5a0623f391b7a) Thanks [@Yahkob](https://github.com/Yahkob)! - limit code-mirror css scope to .graphiql-container + +- [#3180](https://github.com/graphql/graphiql/pull/3180) [`9b333a04`](https://github.com/graphql/graphiql/commit/9b333a047d6b75db7681f484156d8772e9f91810) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Remove duplicate Vite config and again make sure to not include `react/jsx-runtime` in the bundle + +## 0.17.4 + +### Patch Changes + +- [#3077](https://github.com/graphql/graphiql/pull/3077) [`707f3cbc`](https://github.com/graphql/graphiql/commit/707f3cbca3ac2ce186058e7d2b145cdf69bf7d9c) Thanks [@Zolwiastyl](https://github.com/Zolwiastyl)! - show all schema types in explorer + +- Updated dependencies [[`06007498`](https://github.com/graphql/graphiql/commit/06007498880528ed75dd4d705dcbcd7c9e775939)]: + - graphql-language-service@5.1.6 + - codemirror-graphql@2.0.8 + +## 0.17.3 + +### Patch Changes + +- Updated dependencies [[`4d33b221`](https://github.com/graphql/graphiql/commit/4d33b2214e941f171385a1b72a1fa995714bb284)]: + - graphql-language-service@5.1.5 + - codemirror-graphql@2.0.7 + +## 0.17.2 + +### Patch Changes + +- [#3113](https://github.com/graphql/graphiql/pull/3113) [`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `.forEach` with `for..of` + +- [#3126](https://github.com/graphql/graphiql/pull/3126) [`4879984e`](https://github.com/graphql/graphiql/commit/4879984ea1803a6e9f97d81c97e8ba27aacddae9) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer KeyboardEvent#key over KeyboardEvent#keyCode + +- [#3109](https://github.com/graphql/graphiql/pull/3109) [`51007002`](https://github.com/graphql/graphiql/commit/510070028b7d8e98f2ba25f396519976aea5fa4b) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-floating-promises` eslint rule + +- Updated dependencies [[`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9), [`51007002`](https://github.com/graphql/graphiql/commit/510070028b7d8e98f2ba25f396519976aea5fa4b), [`15c26eb6`](https://github.com/graphql/graphiql/commit/15c26eb6d621a85df9eecb2b8a5fa009fa2fe040)]: + - codemirror-graphql@2.0.6 + - @graphiql/toolkit@0.8.4 + - graphql-language-service@5.1.4 + +## 0.17.1 + +### Patch Changes + +- [#3033](https://github.com/graphql/graphiql/pull/3033) [`2d5c60ec`](https://github.com/graphql/graphiql/commit/2d5c60ecf717abafde2bddd32b2772261d3eec8b) Thanks [@B2o5T](https://github.com/B2o5T)! - remove redundant `catch` statement + +- [#3046](https://github.com/graphql/graphiql/pull/3046) [`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer .at() method for index access + +- [#3017](https://github.com/graphql/graphiql/pull/3017) [`4a2284f5`](https://github.com/graphql/graphiql/commit/4a2284f54809f91d03ba51b9eb4e3ba7b8b7e773) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Avoid bundling code from `react/jsx-runtime` so that the package can be used with Preact + +- [#3042](https://github.com/graphql/graphiql/pull/3042) [`881a2024`](https://github.com/graphql/graphiql/commit/881a202497d5a58eb5260a5aa54c0c88930d69a0) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer String#slice() over String#substr() and String#substring() + +- [#3061](https://github.com/graphql/graphiql/pull/3061) [`7cf4908a`](https://github.com/graphql/graphiql/commit/7cf4908a5d4bd58af315047f4dec5236e8c701fc) Thanks [@B2o5T](https://github.com/B2o5T)! - remove unneeded `reference &&` assertion, convert to switch + +- Updated dependencies [[`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d), [`881a2024`](https://github.com/graphql/graphiql/commit/881a202497d5a58eb5260a5aa54c0c88930d69a0)]: + - codemirror-graphql@2.0.5 + - @graphiql/toolkit@0.8.3 + - graphql-language-service@5.1.3 + +## 0.17.0 + +### Minor Changes + +- [#3012](https://github.com/graphql/graphiql/pull/3012) [`65f5176a`](https://github.com/graphql/graphiql/commit/65f5176a408cfbbc514ca60e2e4bd2ea133a8b0b) Thanks [@benjie](https://github.com/benjie)! - GraphiQL now maintains the DocExplorer navigation stack as best it can when the schema is updated + +### Patch Changes + +- [#2993](https://github.com/graphql/graphiql/pull/2993) [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4) Thanks [@B2o5T](https://github.com/B2o5T)! - add `unicorn/consistent-destructuring` rule + +- Updated dependencies [[`e68cb8bc`](https://github.com/graphql/graphiql/commit/e68cb8bcaf9baddf6fca747abab871ecd1bc7a4c), [`f788e65a`](https://github.com/graphql/graphiql/commit/f788e65aff267ec873237034831d1fd936222a9b), [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4), [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d), [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba)]: + - graphql-language-service@5.1.2 + - codemirror-graphql@2.0.4 + - @graphiql/toolkit@0.8.2 + +## 0.16.0 + +### Minor Changes + +- [#2895](https://github.com/graphql/graphiql/pull/2895) [`ccba2f33`](https://github.com/graphql/graphiql/commit/ccba2f33b67a03f492222f7afde1354cfd033b42) Thanks [@TheMightyPenguin](https://github.com/TheMightyPenguin)! - Add user facing setting for persisting headers + +### Patch Changes + +- [#2931](https://github.com/graphql/graphiql/pull/2931) [`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-negated-condition` and `no-else-return` rules + +- [#2964](https://github.com/graphql/graphiql/pull/2964) [`cec3fb2a`](https://github.com/graphql/graphiql/commit/cec3fb2a493c4a0c40df7dfad04e1a95ed35e786) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-export-from` rule + +- [#2932](https://github.com/graphql/graphiql/pull/2932) [`11e6ad11`](https://github.com/graphql/graphiql/commit/11e6ad11e745c671eb320731697887bb8d7177b7) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `compose.ts` with `clsx` for class concatenation + +- [#2937](https://github.com/graphql/graphiql/pull/2937) [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-includes` + +- [#2933](https://github.com/graphql/graphiql/pull/2933) [`d502a33b`](https://github.com/graphql/graphiql/commit/d502a33b4332f1025e947c02d7cfdc5799365c8d) Thanks [@B2o5T](https://github.com/B2o5T)! - enable @typescript-eslint/no-unused-expressions + +- [#2965](https://github.com/graphql/graphiql/pull/2965) [`0669767e`](https://github.com/graphql/graphiql/commit/0669767e1e2196a78cbefe3679a52bcbb341e913) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-optional-catch-binding` rule + +- [#2963](https://github.com/graphql/graphiql/pull/2963) [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `prefer-destructuring` rule + +- [#2942](https://github.com/graphql/graphiql/pull/2942) [`4ff2794c`](https://github.com/graphql/graphiql/commit/4ff2794c8b6032168e27252096cb276ce712878e) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `sonarjs/no-redundant-jump` rule + +- Updated dependencies [[`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147), [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b), [`4a8b2e17`](https://github.com/graphql/graphiql/commit/4a8b2e1766a38eb4828cf9a81bf9d767070041de), [`695100bd`](https://github.com/graphql/graphiql/commit/695100bd317940ff3ffd8f56b54248c1dba1ac04), [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9), [`c44ea4f1`](https://github.com/graphql/graphiql/commit/c44ea4f1917b97daac815c08299b934c8ca57ed9), [`0669767e`](https://github.com/graphql/graphiql/commit/0669767e1e2196a78cbefe3679a52bcbb341e913), [`18f8e80a`](https://github.com/graphql/graphiql/commit/18f8e80ae12edfd0c36adcb300cf9e06ac27ea49), [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215), [`6a9d913f`](https://github.com/graphql/graphiql/commit/6a9d913f0d1b847124286b3fa1f3a2649d315171)]: + - codemirror-graphql@2.0.3 + - @graphiql/toolkit@0.8.1 + - graphql-language-service@5.1.1 + +## 0.15.0 + +### Minor Changes + +- [#2908](https://github.com/graphql/graphiql/pull/2908) [`3340fd74`](https://github.com/graphql/graphiql/commit/3340fd745e181ba8f1f5a6ed002a04d253a78d4a) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Deprecate the `initialTabs` prop and add a `defaultTabs` props that supersedes it + +- [#2907](https://github.com/graphql/graphiql/pull/2907) [`3a7d0007`](https://github.com/graphql/graphiql/commit/3a7d00071922e2005777c92daf6ad0c1ce3e2816) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Clearly separate the fetching and subscription states for multipart requests (like subscriptions) and show the stop-button as long as the subscription is running + +### Patch Changes + +- [#2910](https://github.com/graphql/graphiql/pull/2910) [`16174a05`](https://github.com/graphql/graphiql/commit/16174a053ed89fb9554d096395ab7bf69c8f6911) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix autocomplete styles for field type and description on the right + +- [#2919](https://github.com/graphql/graphiql/pull/2919) [`f6cae4ea`](https://github.com/graphql/graphiql/commit/f6cae4eaa0258ea7fcde97ba6368830955f0abf4) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix overflow when there are lots of tabs that don't fit into the tab bar at once + +- [#2905](https://github.com/graphql/graphiql/pull/2905) [`0851d5f9`](https://github.com/graphql/graphiql/commit/0851d5f9ecf709597d0a698609d88f99c4395665) Thanks [@ccbrown](https://github.com/ccbrown)! - Fix: prevent default event for graphiql-doc-explorer-back link + +- [#2912](https://github.com/graphql/graphiql/pull/2912) [`83364b28`](https://github.com/graphql/graphiql/commit/83364b28020b5946ed58908d6d977f1de766e75d) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add missing effect dependency to make sure updates to the `defaultHeaders` prop have the desired effect + +## 0.14.0 + +### Minor Changes + +- [#2821](https://github.com/graphql/graphiql/pull/2821) [`29630c22`](https://github.com/graphql/graphiql/commit/29630c2219bca8b825ab0897840864364a9de2e8) Thanks [@avaly](https://github.com/avaly)! - Initial tabs support + +### Patch Changes + +- [#2885](https://github.com/graphql/graphiql/pull/2885) [`8f926489`](https://github.com/graphql/graphiql/commit/8f9264896e9971951853463a283a90ba3d1310ef) Thanks [@simhnna](https://github.com/simhnna)! - Fix stop execution button showing a dropdown + +- [#2886](https://github.com/graphql/graphiql/pull/2886) [`2ba2f620`](https://github.com/graphql/graphiql/commit/2ba2f620b6e7de3ae6b5ea641f33e600f7f44e08) Thanks [@B2o5T](https://github.com/B2o5T)! - feat: add `defaultHeaders` prop + +## 0.13.7 + +### Patch Changes + +- Updated dependencies [[`20869583`](https://github.com/graphql/graphiql/commit/20869583eff563f5d6494e93302a835f0e034f4b)]: + - codemirror-graphql@2.0.2 + +## 0.13.6 + +### Patch Changes + +- Updated dependencies [[`353f434e`](https://github.com/graphql/graphiql/commit/353f434e5f6bfd1bf6f8ee97d4ae8ce4f897085f)]: + - codemirror-graphql@2.0.1 + +## 0.13.5 + +### Patch Changes + +- [#2839](https://github.com/graphql/graphiql/pull/2839) [`682ad06e`](https://github.com/graphql/graphiql/commit/682ad06e58ded2f82fa973e8e6613dd654417fe2) Thanks [@ClemensSahs](https://github.com/ClemensSahs)! - Export the `PluginContextProvider` component + +## 0.13.4 + +### Patch Changes + +- [#2824](https://github.com/graphql/graphiql/pull/2824) [`4e2f7ff9`](https://github.com/graphql/graphiql/commit/4e2f7ff99c578ceae54a1ae17c02088bd91b89c3) Thanks [@TheMightyPenguin](https://github.com/TheMightyPenguin)! - fix: prevent key down events when pressing escape to close autocomplete dialogs + +## 0.13.3 + +### Patch Changes + +- [#2791](https://github.com/graphql/graphiql/pull/2791) [`42700076`](https://github.com/graphql/graphiql/commit/4270007671ce52f6c2250739916083611748b657) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Make sure that the info overlay in editors is shown above the vertical scrollbar + +* [#2792](https://github.com/graphql/graphiql/pull/2792) [`36839800`](https://github.com/graphql/graphiql/commit/36839800de128b05d11c262036c8240390c72a14) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Avoid resetting visible plugin state when explorer or history context changes + +- [#2778](https://github.com/graphql/graphiql/pull/2778) [`905f2e5e`](https://github.com/graphql/graphiql/commit/905f2e5ea3f0b304d27ea583e250ed4baff5016e) Thanks [@jonathanawesome](https://github.com/jonathanawesome)! - Adds a box-model reset for all children of the `.graphiql-container` class. This change facilitated another change to the `--sidebar-width` variable. + +## 0.13.2 + +### Patch Changes + +- [#2653](https://github.com/graphql/graphiql/pull/2653) [`39b4668d`](https://github.com/graphql/graphiql/commit/39b4668d43176526d37ecf07d8c86901d53e0d80) Thanks [@dylanowen](https://github.com/dylanowen)! - Fix `fetchError` not being cleared when a new `fetcher` is used + +## 0.13.1 + +### Patch Changes + +- Updated dependencies [[`e244b782`](https://github.com/graphql/graphiql/commit/e244b78291c2e2bb02d5753db82437926ebb4df4)]: + - @graphiql/toolkit@0.8.0 + +## 0.13.0 + +### Minor Changes + +- [#2735](https://github.com/graphql/graphiql/pull/2735) [`ca067d88`](https://github.com/graphql/graphiql/commit/ca067d88148c5d221d196790a997ad599038fad1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add CSS variables for color alpha values: + - `--alpha-secondary`: A color for supplementary text that should be read but not be the main focus + - `--alpha-tertiary`: A color for supplementary text which is optional to read, i.e. the UI would function without the user reading this text + - `--alpha-background-light`, `--alpha-background-medium` and `--alpha-background-heavy`: Three alpha values used for backgrounds and borders that have different intensity + +### Patch Changes + +- [#2757](https://github.com/graphql/graphiql/pull/2757) [`32a70065`](https://github.com/graphql/graphiql/commit/32a70065434eaa7733e28cda0ea0e7d51952e62a) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Use different colors for field names and argument names + +- Updated dependencies [[`674bf3f8`](https://github.com/graphql/graphiql/commit/674bf3f8ff321dfb8471b0f6e5419bb77ddc94af)]: + - @graphiql/toolkit@0.7.3 + +## 0.12.1 + +### Patch Changes + +- Updated dependencies [[`bfa90f24`](https://github.com/graphql/graphiql/commit/bfa90f249be4f68049c1bb81abfb524ae623313f), [`8ab5fcd0`](https://github.com/graphql/graphiql/commit/8ab5fcd0a8399a0f8eb1b569751dd0e8390b9679)]: + - @graphiql/toolkit@0.7.2 + +## 0.12.0 + +### Minor Changes + +- [#2739](https://github.com/graphql/graphiql/pull/2739) [`98e14155`](https://github.com/graphql/graphiql/commit/98e14155c650ee7c5ac639e594eb47f0052b7fa9) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add `DocsFilledIcon` component and use show that icon in the sidebar when the docs plugin is visible + +### Patch Changes + +- [#2740](https://github.com/graphql/graphiql/pull/2740) [`7dfea94a`](https://github.com/graphql/graphiql/commit/7dfea94afc0cfe79b5080f10d840bfdce53f02d7) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Make SVG icon `stroke-width` consistent + +* [#2734](https://github.com/graphql/graphiql/pull/2734) [`3aa1f39f`](https://github.com/graphql/graphiql/commit/3aa1f39f6df559b54f703937ed510c8ba1f21058) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Stop propagating keyboard events too far upwards in the search component for the docs + +- [#2741](https://github.com/graphql/graphiql/pull/2741) [`0219eef3`](https://github.com/graphql/graphiql/commit/0219eef39146495749aca2487112db52fa3bb8fd) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add hover styles for buttons + +- Updated dependencies [[`48872a87`](https://github.com/graphql/graphiql/commit/48872a87e6edec0c301102baaf669ffcce043a13)]: + - @graphiql/toolkit@0.7.1 + +## 0.11.1 + +### Patch Changes + +- [#2712](https://github.com/graphql/graphiql/pull/2712) [`d65f00ea`](https://github.com/graphql/graphiql/commit/d65f00ea2d158cf532d1c71844630c5d9ec13410) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Make sure the back link and title are hidden when focussing the input field for searching the docs + +* [#2708](https://github.com/graphql/graphiql/pull/2708) [`f15ee38d`](https://github.com/graphql/graphiql/commit/f15ee38d56e4f749c145e0a17f0ed8e9a6096ac2) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix computing the initial state for editor values and tabs to avoid duplicating tabs on page reload + +- [#2712](https://github.com/graphql/graphiql/pull/2712) [`d65f00ea`](https://github.com/graphql/graphiql/commit/d65f00ea2d158cf532d1c71844630c5d9ec13410) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Make sure hidden editors don't overflow + +## 0.11.0 + +### Minor Changes + +- [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - BREAKING: The `onHasCompletion` export has been removed as it is only meant to be used internally. + +* [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - Add new components: + - UI components (`Button`, `ButtonGroup`, `Dialog`, `Menu`, `Spinner`, `Tab`, `Tabs`, `Tooltip`, `UnStyledButton` and lots of icon components) + - Editor components (`QueryEditor`, `VariableEditor`, `HeaderEditor` and `ResponseEditor`) + - Toolbar components (`ExecuteButton`, `ToolbarButton`, `ToolbarMenu` and `ToolbarSelect`) + - Docs components (`Argument`, `DefaultValue`, `DeprecationReason`, `Directive`, `DocExplorer`, `ExplorerSection`, `FieldDocumentation`, `FieldLink`, `SchemaDocumentation`, `Search`, `TypeDocumentation` and `TypeLink`) + - `History` component + - A `GraphiQLProvider` component that renders all other existing provider components from `@graphiql/react` for ease of use + +- [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - BREAKING: Add a new context provider for plugins. This induces changes to the following other contexts and their provider components: + - The property `isVisible` and the methods `hide` and `show` of the `ExplorerContext` have been removed. Also, the property `isVisible` and the methods `hide`, `show` and `toggle` of the `HistoryContext` have been removed. Visibility state of plugins is now part of the `PluginContext` using the `visiblePlugin` property. The visibility state can be altered using the `setVisiblePlugin` method of the `PluginContext`. + - The `isVisible` prop of the `ExplorerContextProvider` has been removed. For controlling the visibility state of plugins you can now use the `visiblePlugin` prop of the `PluginContextProvider`. + - The `onToggle` prop of the `HistoryContextProvider` and the `onToggleVisibility` prop of the `ExplorerContextProvider` have been removed. For listening on visibility changes for any plugin you can now use the `onTogglePluginVisibility` prop of the `PluginContextProvider`. + +* [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - BREAKING: The `ResponseTooltip` prop of the `ResponseEditor` has been renamed to `responseTooltip` + +### Patch Changes + +- Updated dependencies [[`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279), [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279), [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279)]: + - codemirror-graphql@2.0.0 + - @graphiql/toolkit@0.7.0 + +## 0.10.1 + +### Patch Changes + +- Updated dependencies [[`d6ff4d7a`](https://github.com/graphql/graphiql/commit/d6ff4d7a5d535a0c43fe5914016bac9ef0c2b782)]: + - graphql-language-service@5.1.0 + - codemirror-graphql@1.3.3 + +## 0.10.0 + +### Minor Changes + +- [#2651](https://github.com/graphql/graphiql/pull/2651) [`85d5af25`](https://github.com/graphql/graphiql/commit/85d5af25d77c29b7d02da90a431c8c15f610c22a) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - BREAKING: The following context properties have been removed as they are only meant for internal use: + - The `subscription` property of the `ExecutionContext` + - The `setSchema` method of the `SchemaContext` + - The `setFetchError` method of the `SchemaContext` + +* [#2652](https://github.com/graphql/graphiql/pull/2652) [`6ff0bab9`](https://github.com/graphql/graphiql/commit/6ff0bab978d63778b8ab4ba6e79fceb36c2db87f) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - BREAKING: The `validationErrors` property of the `SchemaContext` is now always non-null. If the schema is valid then it will contain an empty list. + +- [#2644](https://github.com/graphql/graphiql/pull/2644) [`0aff68a6`](https://github.com/graphql/graphiql/commit/0aff68a645cceb6b9689e0f394e8bece01710efc) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - BREAKING: The `ResponseEditor` component no longer accepts the prop `value`. Instead you can now pass the prop `response` to the `EditorContextProvider`. This aligns it with the API design of the other editor components. + +## 0.9.0 + +### Minor Changes + +- [#2642](https://github.com/graphql/graphiql/pull/2642) [`100af928`](https://github.com/graphql/graphiql/commit/100af9284de18ca89524c646e86854313c5d067b) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a new prop `operationName` to the `ExecutionContextProvider` component that controls the operation sent with the request + +* [#2642](https://github.com/graphql/graphiql/pull/2642) [`100af928`](https://github.com/graphql/graphiql/commit/100af9284de18ca89524c646e86854313c5d067b) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - BREAKING: The `ExecutionContextProvider` and `QueryEditor` components no longer accepts the `onEditOperationName` prop. Instead you can now pass this prop to the `EditorContextProvider` component. + +## 0.8.0 + +### Minor Changes + +- [#2636](https://github.com/graphql/graphiql/pull/2636) [`62317e0b`](https://github.com/graphql/graphiql/commit/62317e0bae6d4ccf89d9e1e6607fd8feeb100078) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - BREAKING: + - The `ExecutionContextProvider` and `QueryEditor` components no longer accept the `externalFragments` prop. Instead the prop can now be passed to the `EditorContextProvider` component. The provider component will normalize the prop value and provide a map of type `Map` (using the fragment names as keys) as part of the value of the `EditorContext`. + - The `QueryEditor` component no longer accept the `validationRules` prop. Instead the prop can now be passed to the `EditorContextProvider` component. The provider component will provide the list of validation rules (empty if there are none) as part of the value of the `EditorContext`. + - The `ExecutionContextProvider` and `HeaderEditor` components no longer accept the `shouldPersistHeaders` prop. Instead the `EditorContextProvider` component now provides the value of its equally named prop as part of the value of the `EditorContext`. + +## 0.7.1 + +### Patch Changes + +- Updated dependencies [[`ea732ea8`](https://github.com/graphql/graphiql/commit/ea732ea8e12272c998f1467af8b3b88b6b508e12)]: + - @graphiql/toolkit@0.6.1 + +## 0.7.0 + +### Minor Changes + +- [#2618](https://github.com/graphql/graphiql/pull/2618) [`4c814506`](https://github.com/graphql/graphiql/commit/4c814506183579b78731659d871cd4b0ba93305a) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a method `introspect` to the schema context and provide a short key (`Shift-Ctrl-R`) for triggering introspection + +## 0.6.0 + +### Minor Changes + +- [#2574](https://github.com/graphql/graphiql/pull/2574) [`0c98fa59`](https://github.com/graphql/graphiql/commit/0c98fa5924eadaee33713ccd8a9be6419d50cab1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Allow passing introspection data to the `schema` prop of the `SchemaContextProvider` component + +### Patch Changes + +- [#2574](https://github.com/graphql/graphiql/pull/2574) [`0c98fa59`](https://github.com/graphql/graphiql/commit/0c98fa5924eadaee33713ccd8a9be6419d50cab1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Set the schema correctly after refetching introspection (e.g. when the `fetcher` prop changes) + +## 0.5.2 + +### Patch Changes + +- [#2565](https://github.com/graphql/graphiql/pull/2565) [`f581b437`](https://github.com/graphql/graphiql/commit/f581b437e5bdab6f3ad817d230ee6d1b410bb591) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Don't invoke editor change callbacks when manually signaling "empty" changes. + +## 0.5.1 + +### Patch Changes + +- [#2561](https://github.com/graphql/graphiql/pull/2561) [`08346cba`](https://github.com/graphql/graphiql/commit/08346cba136825341881f9dfefc62a60d748e0ee) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add missing effect dependencies to make sure editors are recreated when changing the `keyMap` prop + +## 0.5.0 + +### Minor Changes + +- [#2541](https://github.com/graphql/graphiql/pull/2541) [`788d84ef`](https://github.com/graphql/graphiql/commit/788d84ef2784188981f1b4cfb78fba24153bf0cb) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add `onSchemaChange` callback prop to the `SchemaContextProvider` component + +### Patch Changes + +- [#2545](https://github.com/graphql/graphiql/pull/2545) [`8ce5b483`](https://github.com/graphql/graphiql/commit/8ce5b483ee190b5f5dd84eaf42e5d1359ce185e6) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Avoid top-level dynamic imports from `codemirror` that break importing the package in non-browser environments + +## 0.4.3 + +### Patch Changes + +- [#2526](https://github.com/graphql/graphiql/pull/2526) [`26e44120`](https://github.com/graphql/graphiql/commit/26e44120a18d49af451c97619fe3386a65579e05) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add missing `caller` arguments to hook calls so that the error message printed when a context provider is missing is more accurate about the component or hook that caused the error + +## 0.4.2 + +### Patch Changes + +- [#2501](https://github.com/graphql/graphiql/pull/2501) [`5437ee61`](https://github.com/graphql/graphiql/commit/5437ee61e1ba6cd28ccc1cb3543df1ea788278f4) Thanks [@acao](https://github.com/acao)! - Allow Codemirror 5 `keyMap` to be defined, default `vim` or `emacs` allowed in addition to the original default of `sublime`. + +- Updated dependencies [[`cccefa70`](https://github.com/graphql/graphiql/commit/cccefa70c0466d60e8496e1df61aeb1490af723c)]: + - graphql-language-service@5.0.6 + - codemirror-graphql@1.3.2 + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`c9c51b8a`](https://github.com/graphql/graphiql/commit/c9c51b8a98e1f0427272d3e9ad60989b32f1a1aa)]: + - graphql-language-service@5.0.5 + - codemirror-graphql@1.3.1 + +## 0.4.0 + +### Minor Changes + +- [#2461](https://github.com/graphql/graphiql/pull/2461) [`7dfe3ece`](https://github.com/graphql/graphiql/commit/7dfe3ece4e8ab6b3400888f7f357e394db63439d) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add `useDragResize` utility hook + +## 0.3.0 + +### Minor Changes + +- [#2453](https://github.com/graphql/graphiql/pull/2453) [`1b41e33c`](https://github.com/graphql/graphiql/commit/1b41e33c4a871a345836de58f415b7c461ced1f8) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add execution context to `@graphiql/react` and move over the logic from `graphiql` + +* [#2452](https://github.com/graphql/graphiql/pull/2452) [`ee0fd8bf`](https://github.com/graphql/graphiql/commit/ee0fd8bf4042053ec647080b83656dc5e54a7239) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move tab state from `graphiql` into editor context from `@graphiql/react` + +- [#2449](https://github.com/graphql/graphiql/pull/2449) [`a0b02eda`](https://github.com/graphql/graphiql/commit/a0b02edaa629c6113c1c5518fd3aa05b355a1921) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Assume all context values are nullable and create hooks to consume individual contexts + +* [#2450](https://github.com/graphql/graphiql/pull/2450) [`1e6fc68b`](https://github.com/graphql/graphiql/commit/1e6fc68b73941544ee64e0499e459f9c7d39aa14) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Extract the `copy`, `merge`, `prettify`, and `autoCompleteLeafs` functions into hooks and remove these functions from the editor context value + +### Patch Changes + +- [#2451](https://github.com/graphql/graphiql/pull/2451) [`0659e96e`](https://github.com/graphql/graphiql/commit/0659e96e07f98d532619f29f52cba59e2d528327) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Always use the current value of the headers for the introspection request + +## 0.2.1 + +### Patch Changes + +- [#2435](https://github.com/graphql/graphiql/pull/2435) [`89f0244f`](https://github.com/graphql/graphiql/commit/89f0244f7b7cdf01c168638a09f5137788401995) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix deriving default values for editors from storage + +* [#2437](https://github.com/graphql/graphiql/pull/2437) [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move prettify query functionality to editor context in `@graphiql/react` + +- [#2435](https://github.com/graphql/graphiql/pull/2435) [`89f0244f`](https://github.com/graphql/graphiql/commit/89f0244f7b7cdf01c168638a09f5137788401995) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic for deriving operation facts from the current query to `@graphiql/react` and store these facts as properties on the query editor instance + +* [#2448](https://github.com/graphql/graphiql/pull/2448) [`3dae62fc`](https://github.com/graphql/graphiql/commit/3dae62fc871385e148a799cde55a52a5e6b41d19) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - don't introspect the schema if it's provided via props + +- [#2437](https://github.com/graphql/graphiql/pull/2437) [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move copy query functionality to editor context in `@graphiql/react` + +* [#2437](https://github.com/graphql/graphiql/pull/2437) [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move merge query functionality to editor context in `@graphiql/react` + +- [#2436](https://github.com/graphql/graphiql/pull/2436) [`3e5295f0`](https://github.com/graphql/graphiql/commit/3e5295f0fd3b5f999643ea97e6cee706554f0b50) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Inline logic for clicking a reference to open the docs and remove the `onClickReference` and `onHintInformationRender` props of the editor components and hooks + +* [#2436](https://github.com/graphql/graphiql/pull/2436) [`3e5295f0`](https://github.com/graphql/graphiql/commit/3e5295f0fd3b5f999643ea97e6cee706554f0b50) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move visibility state for doc explorer from `graphiql` to the explorer context in `@graphiql/react` + +## 0.2.0 + +### Minor Changes + +- [#2413](https://github.com/graphql/graphiql/pull/2413) [`8be164b1`](https://github.com/graphql/graphiql/commit/8be164b1e158d00752d6d3f30630a797d07d08c9) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a `StorageContext` and a `HistoryContext` to `@graphiql/react` that replaces the logic in the `graphiql` package + +* [#2420](https://github.com/graphql/graphiql/pull/2420) [`3467cd33`](https://github.com/graphql/graphiql/commit/3467cd33264e0766a0a43cf53e52ec371df26962) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a `SchemaContext` to `@graphiql/react` that replaces the logic for fetching and validating the schema in the `graphiql` package + +### Patch Changes + +- Updated dependencies [[`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e), [`8be164b1`](https://github.com/graphql/graphiql/commit/8be164b1e158d00752d6d3f30630a797d07d08c9), [`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e), [`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e)]: + - @graphiql/toolkit@0.6.0 + +## 0.1.2 + +### Patch Changes + +- [#2427](https://github.com/graphql/graphiql/pull/2427) [`ebc864f0`](https://github.com/graphql/graphiql/commit/ebc864f0ab05000758cb2898daaa73a2f15255ec) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Mark `graphql` as external dependency to avoid importing multiple instances + +* [#2427](https://github.com/graphql/graphiql/pull/2427) [`ebc864f0`](https://github.com/graphql/graphiql/commit/ebc864f0ab05000758cb2898daaa73a2f15255ec) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix linting by also updating the options object in the internal codemirror state + +## 0.1.1 + +### Patch Changes + +- [#2423](https://github.com/graphql/graphiql/pull/2423) [`838e58da`](https://github.com/graphql/graphiql/commit/838e58dad652d8f5559af7b88d049b1c62348f2f) Thanks [@chentsulin](https://github.com/chentsulin)! - Fix peer dependency declaration by using `||` instead of `|` to link multiple major versions + +## 0.1.0 + +### Minor Changes + +- [#2409](https://github.com/graphql/graphiql/pull/2409) [`f2025ba0`](https://github.com/graphql/graphiql/commit/f2025ba06c5aa8e8ac68d29538ff135f3efc8e46) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic of the variable editor from the `graphiql` package into a hook `useVariableEditor` provided by `@graphiql/react` + +* [#2408](https://github.com/graphql/graphiql/pull/2408) [`d825bb75`](https://github.com/graphql/graphiql/commit/d825bb7569ca6b1ebbe534b893354645c790e003) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic of the query editor from the `graphiql` package into a hook `useQueryEditor` provided by `@graphiql/react` + +- [#2411](https://github.com/graphql/graphiql/pull/2411) [`ad448693`](https://github.com/graphql/graphiql/commit/ad4486934ba69247efd33ee500e30f8236ecd079) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic of the result viewer from the `graphiql` package into a hook `useResponseEditor` provided by `@graphiql/react` + +* [#2404](https://github.com/graphql/graphiql/pull/2404) [`029ddf82`](https://github.com/graphql/graphiql/commit/029ddf82c29754ab8518ae7df66f9b25361a8247) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a context provider for editors and move the logic of the headers editor from the `graphiql` package into a hook `useHeaderEditor` provided by `@graphiql/react` + +### Patch Changes + +- [#2370](https://github.com/graphql/graphiql/pull/2370) [`7f695b10`](https://github.com/graphql/graphiql/commit/7f695b104f9b25ba8c6d36f7827c475b297b7482) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a context with provider component and hooks that manages the state related to the docs/explorer. diff --git a/packages/graphiql-react/LICENSE b/packages/graphiql-react/LICENSE new file mode 100644 index 00000000000..7802f239a32 --- /dev/null +++ b/packages/graphiql-react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 GraphQL Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/graphiql-react/README.md b/packages/graphiql-react/README.md new file mode 100644 index 00000000000..5bf7b262082 --- /dev/null +++ b/packages/graphiql-react/README.md @@ -0,0 +1,135 @@ +[Changelog](https://github.com/graphql/graphiql/blob/main/packages/graphiql-react/CHANGELOG.md) +| +[API Docs](https://graphiql-test.netlify.app/typedoc/modules/graphiql_react.html) +| [NPM](https://www.npmjs.com/package/@graphiql/react) + +# `@graphiql/react` + +A React SDK for building integrated GraphQL developer experiences for the web. + +## Purpose + +This package contains a set of building blocks that allow its users to build +GraphQL IDEs with ease. It's the set of components that make up Graph*i*QL, the +first and official GraphQL IDE, owned and maintained by the GraphQL Foundation. + +There are two kinds of building blocks that this package provides: Stateful +context providers for state management and simple UI components. + +## Getting started + +All the state for your GraphQL IDE lives in multiple contexts. The easiest way +to get started is by using the `GraphiQLProvider` component that renders all the +individual providers. + +There is one required prop called `fetcher`. This is a function that performs +GraphQL request against a given endpoint. You can easily create a fetcher using +the method `createGraphiQLFetcher` from the `@graphiql/toolkit` package. + +```jsx +import { GraphiQLProvider } from '@graphiql/react'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; + +const fetcher = createGraphiQLFetcher({ + url: 'https://my.graphql.api/graphql', +}); + +function MyGraphQLIDE() { + return ( + +
Hello GraphQL
+
+ ); +} +``` + +Inside the provider you can now use any UI component provided by +`@graphiql/react`. For example, you can render a query editor like this: + +```jsx +import { QueryEditor } from '@graphiql/react'; + +function MyGraphQLIDE() { + return ( + +
+ +
+
+ ); +} +``` + +The package also ships the necessary CSS that all its UI components need. You +can import them from `@graphiql/react/dist/style.css`. + +> **Note**: In order for these styles to apply, the UI components need to be +> rendered inside an element that has a class name `graphiql-container`. + +By default, the UI components will try to use the +[Roboto](https://fonts.google.com/specimen/Roboto) font for regular text and the +[Fira Code](https://fonts.google.com/specimen/Fira+Code) font for mono-space +text. If you want to use the default fonts you can load them using these files: + +- `@graphiql/react/font/roboto.css` +- `@graphiql/react/font/fira-code.css`. + +You can of course use any other method to load these fonts (for example loading +them from Google Fonts). + +Further details on how to use `@graphiql/react` can be found in the reference +implementation of a GraphQL IDE - Graph*i*QL - in the +[`graphiql` package](https://github.com/graphql/graphiql/blob/main/packages/graphiql/src/components/GraphiQL.tsx). + +## Available contexts + +There are multiple contexts that own different parts of the state that make up a +complete GraphQL IDE. For each context there is a provider component +(`ContextProvider`) that makes sure the context is initialized and managed +properly. These components contains all the logic related to state management. +In addition, for each context there is also a hook (`useContext`) that +allows you to consume its current value. + +Here is a list of all contexts that come with `@graphiql/react` + +- `StorageContext`: Provides a storage API that can be used to persist state in + the browser (by default using `localStorage`) +- `EditorContext`: Manages all the editors and tabs +- `SchemaContext`: Fetches, validates and stores the GraphQL schema +- `ExecutionContext`: Executes GraphQL requests +- `HistoryContext`: Persists executed requests in storage +- `ExplorerContext`: Handles the state for the docs explorer + +All context properties are documented using JSDoc comments. If you're using an +IDE like VSCode for development these descriptions will show up in auto-complete +tooltips. All these descriptions can also be found in the +[API Docs](https://graphiql-test.netlify.app/typedoc/modules/graphiql_react.html). + +## Theming + +All the components from `@graphiql/react` have been designed with customization +in mind. We achieve this using CSS variables. + +All variables that are available for customization can be found in the +[`root.css` file](https://github.com/graphql/graphiql/blob/main/packages/graphiql-react/src/style/root.css). + +### Colors + +Colors are defined using the +[HSL format](https://en.wikipedia.org/wiki/HSL_and_HSV). All CSS variables for +colors are defined as a list of the three values that make up HSL (hue, +saturation and lightness). + +This approach allows `@graphiql/react` to use transparent colors by passing the +value of the CSS variable in the `hsla` function. This enables us to provide +truly reusable UI elements where good contrasts are preserved regardless of the +elements background. + +## Development + +If you want to develop with `@graphiql/react` locally - in particular when +working on the `graphiql` package - all you need to do is run `yarn dev` in the +package folder in a separate terminal. This will build the package using Vite. +When using it in combination with `yarn start-graphiql` (running in the repo +root) this will give you auto-reloading when working on `graphiql` and +`@graphiql/react` simultaneously. diff --git a/packages/graphiql-react/__mocks__/svg.jsx b/packages/graphiql-react/__mocks__/svg.jsx new file mode 100644 index 00000000000..47d2425836d --- /dev/null +++ b/packages/graphiql-react/__mocks__/svg.jsx @@ -0,0 +1,7 @@ +export default function MockedIcon(props) { + return ( + + mocked icon + + ); +} diff --git a/packages/graphiql-react/font/fira-code.css b/packages/graphiql-react/font/fira-code.css new file mode 100644 index 00000000000..0f1bbffde92 --- /dev/null +++ b/packages/graphiql-react/font/fira-code.css @@ -0,0 +1,58 @@ +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} diff --git a/packages/graphiql-react/font/roboto.css b/packages/graphiql-react/font/roboto.css new file mode 100644 index 00000000000..f3053bdccff --- /dev/null +++ b/packages/graphiql-react/font/roboto.css @@ -0,0 +1,272 @@ +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAMwAA4AAAAABZgAAALdAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI4ghsLEAABNgIkAxwEIAWDCgcgG3YEyI7DdHsjE9IUV+CFDh74vPL9/MmgO0un0soqjWt7En2kQoCMtXsRxyxkMqP9iO6NfSiUaLJuoRIKnhI0+ImbcWOB5XOAFVmCgxZQQmuBJRhZtsUCXm/492Dyuk2YZJdkdApZeOzyEQgKOwDgRjASBEEBVmAlgACtOHEhpjLyyrACMAB0vaLa6cAw5bc5bvhA2uwO7zXAyKPmkYNnAJgBxLEMDxFLqVBPI6EQ/daTr/QOAgfCngRoZc4UZiL623qCkf/oHVsfRCOuAIbJyF4ajQQKQLmQhNBAA4aygH9b19Xw4iAC8DkKM6WrYw/ABMAOWEAamA7sgBWACgAUSlc3SCmlc95o45idYD92Qt/+5gF19v3FALtB9+7dq/h6/Ljyu/zzYfnngwdlHxO+k39nOcO/e7nPf2vCoo3HVlmNTdnWwW3JZffuVU6cQX14kb3qUGOOJ+mjP9iMeb1Nivq5gXpJUWm+cmVK56e6PjI2uce23hHlG48vyDvym5/5q+wbkjq90rN+z53D6zXqmVUPVshZoVtrZgc4vleS1NNrni6VR8I/vTrpzpPwu1+1Pel4xBIzK16W3KcLNnVGl2RGZHbPXBAvhw4M02Ci/t0BBfw/p79XS9V7CKAMF0++DK9rtI/7MXvGATjz0TEA4K4oef476t9dS555BAoLBYCA6ei/FSzVgvg/cIR45gpTaLWeLiB+oa4xJuTks7r7/xwCmCzlpoJKALCDQmkyEsCsN0mELUADghGsGgAF6c9IXkabDYyqg6WMkZd9z7BT5gaphhhqnOH66aOvkTQhggQLpsk0xBB9DNSLJttgPQTQJBtoIE0JEY2wb+1lhF6GG62XngKUGKLFECMNkW2kZgP10+M31GZUwfojwkU0uAcQkISKFNtqGMlau3vIjjRUjMANjYkDNKeouYh7CRBmuD4CHQgHG6GXET8oT7ZU6QqUStddiABBJPSv6P315AAA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAACI0AA4AAAAARUwAACHdAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGkAbjgwcgTAGYACDFBEMCuRQ1QQLg3oAATYCJAOHcAQgBYMKByAbkzqjoqTVgkfwlwk8kKE3XiIhIgKsVW3TdG3TuIGqASL+pV+AIzTjRTyFY3CirY+QZJZAWiOq0pPuOSAAB8KfMIQSSZFifPIIO/l5fm5/7rsLNmCMjRxIlGCMKgMcKRVKKZKKSCugKKmiCCqxUa3NEIYxUKGtQPsrZSV+bUCHM3spV9aR/gYPF58gHiGHOqvswcOM4QCgaB6oBCxHGn/sW4V2OQeoZB7buGiesCgBQbK8myPw+9aGzNnsXzlx3FqwaJHXPTUqsdLw6XWWreQvZbQ0s1rNxXZYO+NRiGucHouWi8p++v6W/PV3ec5wG+uI7d0ckfbAIeCiOaYuAFQh1ZlU6dKlaNOlTlOlqgFL4KLs2Ja0nIUzI0aIvLW+7FXLEx0r09XFKqaYYAqyTbK/7sgCgWHj3twHgcySFcSGHWQFZ0gUPqTKbwhCAGvAQGDxq9GxCOmEk9z9Qe/6zJT4OXJzSvTGyB3r0hJWCN1+Y0oCMCEMcsCaNxrBog8q0djtfyRgTNMGqn0Qk9Te3tOHXdJFZqWIsdGacrp7tNfbZseM4689XgPSt+aaPbDset2PZtscIfhjErts/Mycfp9stNX7Rqsfm9flBWADy+P62fmx+7oXbmbc2amrN4LiF0742hlps8f8QJq54BQnvGU/tNnTvrMRWawacTJR7rrxUqg6py2jZTfZ6X7PANbBrH0OSfW1iwkmSdOZ0VZfIPce6bzOjAwcm6mciHfRnREsG0iC3dDvwi7a5uV7PwcmIcneBDkexrjPTmYtG2saKJytFydegg/I7tdXb6T8Wf4qf/t/8YhDfQAJYydKjPU2iLNRvE0SJEqSLEWqNJttkS7DVttk2W6HbDly5cm3T7ESB5Qqx1elRp0GTVq0aXfIYUccdcxxJ5zUQahTF5HTBgwZMeayq6676ba77rnvgYceeeyJp/4zZcZLr73xznsffPTJZ198NesbxE4PBCBiwp61odB+ZcgeXgR01O5wKpLRVqWt5ujWozBpkSA4DNbpFuVrYJ+sKq+vr04izCDNINYHE4N4pgEs20Yl7+hGpGKWb5x1oJr9EtA+gGD59NGBsq7GiSyMQJoGZ78WKYTp4IBXRW5kJl2WYQCOrmWVgU9pmAbslKiaEC4xISYlFog77o7U7IZphWDUaGOWOJ15trsGu7PsAzVYneflEUsmEgZbaKp6XOcEyhlIYOjXrZNDICgg+eGnX35DCL36IKS6gcqwfJyJcQAZ9Ie6KYitTb/pC2KO0myj/xNgizTauJ9OPtvLGVCA5voU+AdumqsbaECPA/KwLqRBA+4KzfoNYCiKFDkvjZPYIaOEDJIN3ZgfRmEZbuETayM2dkR27I/SaAphfIo5QqVZtqCtQu1otZ19VfupoaHR6qhjOp3TN3tujoDWCVbohX6YhFW4h3+Ex3p3emN0GL+a0k6pHaWW0xe1WaNFe91ZvXOs24BaD1SM0UdduGtW7y7+67yOa76K+w3AsvbfP06KdT35yH2f+PPcFOA3L+TmiGZN3KMVJyzzHGfIDSrwe07oXmpfjsnR76U69Ro0atKsRStbS6r2uiy1zEX9hgwbMSpG7Gnio/fMcxMmnXfBgEHf+UMIEoiaszbA/wHxb+BJsOrjYN0fAebXQT4Aqgebvt1tHROxXyVYM4VgOQPHW8EuAxwFfk1rx8nRuTOrJCaSMEN5bRwUDVFw8GlWYPF9YlCR+DkugTVgKgS4BzKwNYdGe1M3DD0m6opugMxtISSWkNQN/UCO00gaBoiUqRfMS8GFyyUiIqkQNVTJrdykumzInD1PAjAJEaCASYOoXu96HSKyLEvLwhunbDdTr+m61ucWu1qXpp3VN6I5djsDX71TK7PzdywU6fzEQiJJBoIDOBtPiruuq6rSFfP4VtsvKVjW91Q1ETmvfGCUdnlliai+HolV5S0Ouqq0JEVKa2QtJVkaE/DS5i67LBqPrynvhwTHIWXyi+NxHnG6no9WDnbJGoz9vKC1bWP0mjtHmajkHJ4eQPdNCaM7mDNgjGweFh16r4eX5URS9D02cRidpbWkrslJmNtcfQiJjOZzUeWS2t6Tc3RkA9zaZeBcp2Mv1frJqxxCi4SJ65/HJ0c9aq+QQyzLZeX8lSCRBYl4vdhkufzdtMcRmSFuHijHtDDUlMFzC7FMAWYp5bW0jiWZmvpraDyBJqafib57n8M1rKV+PQpjLaigt/duufjArEeOnO9+x/rj7W/tNoKwbd7yNrImjLVByqAFO1rk31VuoNG2i2tXy7z7KaHliZI2jtLdYZv+/c2hehKcgVbNT+gw6LmNpJ+9wby3K56m9Lsob03z438br//j/gv/i3VO/6T5w7tLlvyt/+8V9L2r+7+Zv7Oz5RnszYFtq1BY03acdowIHtCSSdi/kKOGLQPSO4xD8S+g15HAYZ8daIseWbjcpKR85FTQ+oA7+tc20x8jWADGf9GjR3GGBMXLW2NN5WMGF6YuBhjzY22HGCxe3/lrdn5dcaC70NCdCXaq9Uea7x62eKofp7Tmz+aSgModOeVdLpHVNRXsAW6UuEAOHPQ9LGvypDdy4rKoSIex6Z85Ao41PtIctZFXtjPtu3LaGm/RdunnYVApOdepDjmlKUmzNNu553sHLHGXDfXlit1Pt3/3bY6cGVbkDHqHXO3I16QZi3l3/+b/rcKphd8erepj8ezsr4/0OCIIqK3Xrne5hPw8YhRnJrTqcyTeBnaUI6kZzFLZx6acFEHLDKhCy1A63Ue61Koh4xtiNihMS8pBVdJI+xUFT/ZkeSQF8o9MJyguKaxDqeije0aObL+qlpkHm8OEoQOD+jUbV1/WPrDd4ZDzAg6rfnoSPfa4q8xPMKqglQXZcK9NTqjNc91a88v1ZcM6c1zauXhAZte+Lrw93CpeHHznPdChcSlbZl7osHx5FnFFxfAGlh4sy6WvdCqkd2QLUXak7+17up1sfeDOlrf3ei8NrYkmZlCYN/agOaGk7LnzWfbS+CyWELD0jTwNRk2v/xuLhP0N1TiuTY7eVh9UokUudEXY77e/frurwDqXn/pfDxdxSbtN2UovOSMvai9/Gfl/d8NX4/8z5HsDB+CRd2YiOy8k59PSOMcsPhWZBh2jNawOh4dW5Gyc6Jqqxz7FFEkUlkuIZNCM2nKw8A0eifFubKyhjRx1UA8YZFITna8jXf8T41icY4ZWhYejqUVLgabcaytZbso628RnLIMtMvSl3Lp7epsh2h7b/HCDJu/dfCDxnjLI39pV6Y4FGRgs2iXP/ZzTC8VvR7RFu/QKF7dnx4HIRTP7F6nfCkzj5ccqHQn5PszGOZrbAFdWZUYtp1XfDq+Vgi2ttGkxs9xajtSlVqYI4zD0MKzxIhEch4cUYJxjb2J8ixlPDZR93NveZehQPM375c23VyLP1Mn0lpNl89uNOTcZxq7nQUoHZtzzOzd7HQ1lO+2ftJrv8qJcb1rR+GQXCAUD2bOvM5RwcFX3oHbEfcoV5RGvp6hEOjfNnMwOh+XrZNbHJdrGzQuYxHC0a9ucLrt2n2jti5ijBTcNydnMydDTLTDOg0+sYvIN4zaow2nHfHB/u5n8n5/WStYfArJwCEeHApkqm+e45aNk+lQTRmGFKAyD1a0sz5Ftl4w3C9tYZOHZ5crPMtrBVfamwYQDdZK8i7i0I/ED+QD2oXsw07nOCVsppKv4I1CmxFLGk4qol/RHS+e3PJ+8iny65ME+LCCN1JgeB1uZcWEmnILORCuFfprLwqUVW01RBUsqavMZuKtHXTijdZqew6juOFmGYSnRFBWEx1Rq83+8BJW6Pu87UWCbku+dmNerSPFPKWHAZx9wFl50iVFIOIVKiPHszA8SAsoWlwrRfGZNB3EZf3rFvH2Ovmd/2Q4spvxRmc9kFRFuw033DqLbpG3xtk4uKjUAw960xtEnOvd745NH0LsPSOKgLwarGeXeoM9SVa+xZ6/hC/jWM8lBMT09sSQRbcVHmlg5oN5897zflIM12DY0M/SltUjVT+cWsGrrVWqD1bn2gVaAUGa22WCo+bvjpUUu3+Jq4LD3ANOhKSg1fFEHc4CtPRoFcVIOcX3B+PSMLE+U8k8Ugzd7L3E1e/MPcjU5wz6yaV5qQG3qGL6Lv6lJzOL1Jrw8+aiwjhbmlIA8VPGgDO/EtwW7uLIvCTvyoODpAdxL+sHRnwu3w3F372h3D891EUzDxxnWML1QeKPUbCJGagxes+HAcCUzm5GVW1yAtQDuuZUu3yB2Pb6sUruA9YmWcfDsp6jdRD5xPXHjGHl7L9B2FpXmokJ0Ol86mV1+2b3cbKW6cq7cHA/3n/p/XTFRCJMpm0cpO8QgkVtfqYnFueA5zhpmyLPE8s8Gwyp1juBLFtLzH2pO8qSmcQlxe2vkf8xiev6js/TUx8zKPSeLsIB8U8hpoOc/gb6LuIN3TMX0awPVDGhty8YUeU/7tduEx6jTi3GkQeo80rxjVF3haYgY//Dwuf6dmlA58VoDOb9dV+F1rZZKLZlTtSQqY1al7pEyH37xt3L4W0Gr+1HJVd1rIIpX1S/f045L0CkhtYB2TOniTC9IBtDC1yStQaGoZI2Mhwgk1uSWXvGOR4exeIjRvEqR5K4wzrxTFIiqAy3d9f4rhGOijZIREm6ro+BlbjiqSVNccxQY0QWHLoVtIHahc4WrZqUr7Vk1+7+9LCzCR/CVx0cOA9qQnBeO9xHn7iv0G6zFPEra5t3gq8ZuLabdyM8iunF4dqyZiNkObazU7CIxrsCdk5TzC0TyRMnGulhUS8lsDfhqW1aH44jmXf5f4Av7Ep7SlJ1YyWyspU3syiPacd+4RA9hR7Gj+w7KlhZcy8cNeHdZ7CreunsJiH0tkWivM6qRhuUy25PawU9NUVhCupqVSYjx2j3aGe2SDtqq1+V/XCFvQmOR1oExCesONOIcfEqgWsRem58vxFFEeYzPAE7n9LCJkvW1G3ATTmv2/2RbVksuxb3fmbdBkd1TXH0GC1DpVdaZzUOiLaPersyiMqINp3dKRJJEzB4QwVS35JBNt97eW5eNGMfC8FkUVgfKUTZSd8XsytaGAmRvLytT5nIrV7lKalaspsIo/nzrKpchnugXQ/OX4h3LU7v7OKRjfkJi9tq3n64GxI/AVDezHUSg5GCrkLF7/0Ucg0qCOD6Czuu4CVfdYgu3jHRvHvMLZu2uJyJQ4w6FmK3Xe9JHpRJC09ehwziyTqJMUSQ5ZANKUbbKhQcbzuJKfPDKoUSbia1CW/yMm1/guRv17w/9w6iQZ9VV/HtfXIx3oYH9Qd+lyhmHBJIfSp85J1B4tM0ZRVFEECFYE3uBkUYN8ZTMyCyKwkXE4IRCDyzCFf4SJyNrJfxQ559vJ4GzPYVfgzU9oVeHkbhnsdjivQ+1j1Lyf087akFXz+GKLkDeG6JXoTDEM3xHc5EKy14QrHTWsKaKnEyOSq8Y9UwijqFnQ7i6G0JSN0VHoP2BoD5ut5g8rFQylNRoIE/x8NTcIM23k+VtRBurJfM21V1QKrmwmAzX4nbkDeJqXD7OOpN6TpTW52ZAcnbz4RH95A3NEvlyPf2h7hgsawL5Mhux2l2bMio2UYo0KaP625wgaespYb1SaGYqsQ3G9HU+7KTcIuycmTIV0wE4y99wjd02yW7tPnjND+fwVygdWOTHNFepVFUsAum2IOnazzcvM7jiiedHGhdJ1018OidjeG7i5iWwclQoVigpBpX/4aWxbgMccspRxTuJ6BPJFQTe2EaWiZJ0ipUcX1wAG5MgiBuuSgp/5agrbOYI6pfdW8bhWzqxTnhqZnSvvQUecm04zWtbtaD35YajpBkIN1q4heg8MxG+g7iGczLzWvk35oxSaZnShwPEE8vq7RO5Df/QRjXfRZH73GNrSCLSb/bCr5oXTA46Yw+6x0LTLa7Wyfg86Y/ufGn5UnAGuQx0JtTE//BpNj6IDh+n7aM1/O16OAGSAZKxARlBOBbtj2MEnGLJ8H93nEXxqDlQ073pcD/egU5sd33C3CO7+bwEb79UXE5WLAShWltXrlnhnvRlwgpHVO9ib7Xg/WXIaEuSDJZwDQq07TLfRBypNaujr921ju4VHQLzp71jUPCC6PJ82H99Uy5lWIEawKqpp3zcXYxWo1CtFs+ufVc3b6NcVQ1R16aYm3SU0/JNgi+fjf9ci2+yAlmEq5rDaJdCbhEx9ljtnNQa8Eq7dVra/1YbKzVn31nyXnxykNXJ1aOuYtWX0K7nb5+xbo8pGXH4cxyBiCM4bc/uJA5uqolBDXhLc8CXSuUU3IsDv+mSfKXiPEkd6E1rHHm6fRE3L1FkrNlnojlCc+ld9iVlWKt/BKYKbRwRNF5N8LraE1rrHu9L3jcvveLIp2rfBaUWL2lfxXwp3/DFp1g/ed8e/ejTvlA/tb4PlNlxrbaKec1LcmZ60uoqzBXyyi2yn4ogUF7I3IKVjl0U87H5Cva8yiSDAp1eZpi6Q4pUVIpYZlgoUi9IkvJPAiU5W/nqos7zuBlXTsr1Uu9g+bbzZytQ9Vqq1Xhx96kPbfsRYCjd0EKqx0mFElOL+/kLBphKdR+TPzo8WIcMI+Q1SsSdq9ISmNFSd4+DJ/sEencogqvcx962FPBCuQiJtYya3jMCoo24FKB1gMe9Y55DnEZwKsleeVg6Qm30mrPGkdqGVtKvWafPxjkogrGa5iWT03IA9E2PDdHuktjt587ykf1tlYNeCwrVr9Hu/GuXL2mXTpI7OXxBgExD5FTLN+p3qz6RihiG5ey9xI28lFlyDSme0655fchOrqGdmMY7KyNpKQWs7EbQclWxV15PWk8WuJec0ZdpkOfxyYPl98txH+mvni5i7QBn8vmKyTI8SPrN1fwrmwf6Ol6DOKNwpbRPBCvrgExZRstmddmVeCVtpDhQsrcV78bni1d9lynX0fxran6oYV964ya8jzQ2yRlLwA4SGZv3ReNN+ERJ8HfwjRbOe5AgvaWItb8SFK7dGr9AT8ySL6t//i9DQDzEXxnK988Maqv3nvgwluMbR1Rq6V0z4D99UPpQU10rmRbpeEwhLitvCNdg/n25nlkrepEa1/rF2a24M5gS6MfOAc6sjVRUqXxbn1iAfG7PO+i1YK/2bamoQtBJ89yJxEUB3xjlpsyKcpg+kIsvki9Qle/IZnRlraXFp+asJQ6TSxOWbN+65TadNHU5kmitsuD/gZC0JLrH+jCwcPjEKEVJhzsOVRJMeek40CYHCg/VE1LzmAnXZBgVCMyG70tmHS3NxltR6UGUUQqUgznYCXz8Je2AOeNvWPf5SPiNPdH5AJjmGSg4Z3uQb0pqAFqdsy3IPyV5nf/SNQu5nk4+YZb2C7heLiBP2HEzgyRWJ9ihTyuUcQZvgZ/nmijkQwjlc8Fm5qlkQubOMN3roqdG/oRafCZFclNWUShSeb7BDjUGqicBN3qutuZ2mXKvSXAbQOGHa2y0k0PQGp5zRISTY9hqP8dlOzTUG2OM1qrpVoJG90P5yvw4Gs2e7lTD2JBLFK0lvCm5TaqSzmDm/YNRN3EQs+flN+2maTeJaOymAsXajM3mnudDvwdejK+Q4CmW+UVcRqq1b1VrVqD1ujo36E5HQT6rib27Xj6rSu6k0lX5bxfIh/CFm1ThOaDERWZE4ARc1c7IsizGVz7Lg717JQS2HH+gLEC67H1L/i9PP3/Jd3rh3+EIbidBWwrCone4sEhsr21kybNnJsuuZHy/0N8lyAzs0x40UG2Pg/CuY4PJDQYKFHcvDVe6wF6WB3FoY7nk7k11uQlb9g1BhJlIZly4DtKJrpDgdlLifuCSRYvJw26dCR2Qjqo3rBiUjGMdFlOHAB7qujt56HF/1+McZUGja/8ljuBlz0T35NNDE12yEy85gjFyfxNHkMN4fJr0+HXb4w7tFouNDv2nlvTHOvQft+4/DP2RzOg1ZjS5O1tvu2lIylw52/+cQ283PwLcbqtKUslV1gUzF5G521oVWvlB0jJEZzdVyS98KTmb7CeiKAcDNDF/NvWkKLldaezytaMYyqwjrMUSd4wuKvMvMsP6OfyLBl/fQdvEdr20Dxz+aSh9ehFx+HdA8C1085n8fJAJy4LIj40oOcgRyaz2mzZHlp7lpCBYUcGaAb0wHHPDpW6/aefcyeuUbZbSD2uT2akT6Fv0ZWtwqUPk0G2RsVgdXOr2gD0P0zw4dy+6c46cQK4ombXODzZpiv8lKBfDJg3xXIKNX++iX9RkDTElWamk+RfVlHC186QvcjofpePAmJe4WaG91P9dkRvNed5ZkcoR9jZyDL1ovSBUJeeqKOcKX2d4Tu+B5jWR2hnuAvMNr7Xmj4ngOMvBkCU2ZF1SqRtTKrysUju248EfuE15/ZbZJ3trwZdPwaBY6Cir6wBVAzXMvTKZuyq24yAAkssjHypj50h5MlaZRnLiEbsjCm3UCNNQFJ0YyyeScOZJ2i4ua2QuZSSJGZFmgvx91nmR4tdsT9hHI7fg+BWkTWSlaXBsjHAN3iqfwfA5XjLvNvzZG8fhx4GuRfLYN1F29VOnqFhn3upQB8fwaCfHkGAfHslrmWZpzDK2lgOoUpbGBK7cxI5WzO9mJqtehKCUKjGHL07YcX189XVVX1f9eXrT/wd+z2dhYfntb2YqZ9vF0lG3hzj8weecRar8WbDlWT6TmLIUS+dmKnfDindVFmdnOHBLnkNY0HNLr/PDjLn7vYped9XOniV63ZeR8fClmYBok7noylWjSfZxjw74j6dj5/Czz8zlZEPDq7HUnYNj5fbbFz5wdP3OuwpvhJVQ7LulwOxoWiDN5q2UnBi6jdZVGPCSvvcW62QGW66uWnx3Xu2+jgr1vV8rzMtjJNb6eJPgmACfB+RPDKXxa+Bj5X8g15E/mMTed1dcrC8WYCcsYGaQZqBFCcmMiLzQUlQGmq33kphRkNCykYPRPRIv9SuDG5aUohohQjaNYw6tUlULCwCFXYLsDJTtY8Ju8Rgoo1hvj2sox+oo1xOQR6Et3AoePg9meAo6m1BNI7djpacWRehyhdrkD2CSRHZSirlFXawAW9ADy7Crx85A+gbj0eKr8ldRl85ngtjKMInV8EkKVZq4YyiIAV1a4VG8CMzIMLFa0JPJNUMVGiHo/mHPJWF61q7nJKzZghmExDKqPW+lZVSWUGIrq+vxgPw6AIhL9/gNzdPker4LtqO58YsVlqZU0wNEM68V7xwJqcD19jBXnKJl4gMhHbEevPz0tE3Ug+UFYZjGosNY1SlsCL6kPjx0l6MUVXUxCatV5wCbt0WdbbmF+8qw6ebSSo/H9BRt88NC6GmYhAqmX7JL0dN8SJl617APS6oQ+Z6UXHfs8kJ2YtXqhl21+aEbVFndK6zV+aSEGssr+GGV9zIOwQqV9wSu6FfpVVlknqJfVb0Kq8pNRT/0nWA75gNehQFbcAaSsIsxZ6DszK+YSZQCoBBSP4wVHouWRivct0VQ7+pJWNNwQtcKOWuipi7geYYayyQKgGXiFUBtkCyZfbTt6HuJvOnpT9jwhSh43kgSWEbm0LKw0S0SsZVhEJbIECmlS8s9MsPecjdJMu8VSQCQPfKQKBgu8UQsYrkKiGLexaCRF0ujbIcXw9BfoZQh3suq3IIOMGG3qAQEgKZJugfQxIeOEqaTgH+vL8Kc1VMh1UzXjxzF4sRhHdW+Oc39zJwokoSN2z1QuTz2bdgUDMMIIIoGJ0zJYoOjnDiZruXkQyHjmo9YCF3DW0FIee9Ig6JyYv2eYr4pAEDhkZGSmE9eeU5AYREmNE+KDbTUvkeehpa0s3XxszmjUpZdUUYuYTdyXTlcdmD79ohYw0O3oEp0fXRV7cRzsLG7AP+vuaOt+Mx1/zObev2/qbA6gHx0LmNar0aGsoY3Hh9Thmw/UXf/LPO+knd9SFq9mJ/zKk71Oi8WFopqTYdFkGxFBNiC/OZ34Fav2o75vTQ+4lhv8n8/saiaVXo870OVqg4Th0EzS0Cmv8BSqKuQlrNHfwAUo5r+UFWVhrWV/6vJoy2jwu0S+r3zCupg+sNvz5XmdcC8mCxov+9rMncYH+HWfdljG7eiqsz+uf7Aklv9IbKwkqjvm+qorOWgWXOZF5ukb4Xh4pR+hx7fUulU86I1ffx6DVut3uPRWByHMyCcrUwvzcYMs2tT+bZaGu7cXrUcDX2o6p3e4ekDwLe2Z4F4QhYt2UhbaAly1P3+eGp8EbLqN/1rEHGvx5IgvV5WmjKDY70a9X6Cr6HKkoeG/2w5cVmfg8NAvuevYrpOOkwjDWjV0J+4O/6GQr5k8Px6PS182Nx6nfcLoR5tcdP6qLbwtPSuXpmrWvmf2hGbQZNLwGEuItPIQjzfJ8q7HVcvbnFQaECjWq1nvU/xyBRbL6sxawqpV6PW3y5qxpQ4IVNlxEMopVUj1ODO5usi6HPwPpiPnS3kgL4M8Ovsh+1V2znm3Tjjb70F8lN9i/fA9ClF9f5u77BMtfrgE3MFwHzfvAK7Xu26gUCjWls757CurbNggP/uKQ6Kk+2j4dn6qx3tIx+MN6BRqxi3jd1xcVPUhUx9PzfGp15bGiq6UCLax8adelbk84rmOH0LLJ+QZTH4PpDPcEfHebklXlvYLkHT2cyR5ecPPQLa9uslK3yqt1ZmyT8klFcBwAd/luUC8E34/uaX1d9xmvsqqQg0BECA+Y5FCmDVjUwV/+IvAugVG9v5/8QXZQ3in6BvVh1VlNY12WaqlPzXoPvJ7KVsmx7X9EXPl7pk2TRuAnhG9XDpeQubbDM/jzncWWLHOwazy+HsqLfZW7lfkpvJY5ocThnHLfU4ZjRSelOPdxjGtHL5SYNbwriPWvpSz3SO7aj/fY4O3FaGlz5C+jNypp5qy5Tv4+LRVOl7yzQe/9fY71YFDacxBNiZyDqPc+uZzOMbboZYnFa0mhbtHsc8E+nEd6Y9lk87Wa5dIzYzreiJYvM+wfGvaCRNy6bOUJyyYv4UHFT07jGI5kCEdnWky9P2kYHmW6+BlX8A/P+d8ZGe++rr4KKP9axXWc6mj0EbFFDvp/FSClwzFL0b1JduVDMRc4t/NZUCZe1oSKIf/vTlZDPB0jzmcCur2bwgfdNFyBlSO12EfPbtAKfn9DzpcSTkHPmZLkLekTtoon98I2v2wO1UJe+dSfx4I4PrdBND7SCt0A9yDQ0h37RZacvGLY+hNGb7knwDgW1oDvoINNAhNEOpZzXw0OZ5ogOXaNpPigdJDE1DfzOFoH9oFVMAemVTAboNbALQLLQLYi5YM9AlUomph2nCdMAkwc3RC0FeUPflzDwOEPB/BygIRIYA1gINsRkKBKwiBoaSBuAqwMUQKWtkQo2LYRxb9kiKkek54FJ0tacrg7+beP+TJWcuaYNY66XRYMKIsTA1OEuMkx4vequuEkTiuvaKHN/oa81TWTfaHxwtxZZp3ChcvhJFTHKa64rsOvGVR43cf1SNVx7oJptqA3hCSDJ3pClLtgEe1dLseTGoNE0SG4aCpLtck5FkXTYal2IpYhnmoyUE76YqrjuV8jjy5OfxxUGUGsGgZqWIq9RBAAA=) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAB0wAA4AAAAAN9AAABzZAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmobmnocNgZgAIIEEQwKw1i2CQuCEAABNgIkA4QcBCAFgzIHIBv6LhXc9d0OQlLmtmQkQtg4gChsLYqSwfiU/X+9wI0hUv/ESljasdKOLTGMi44Ndgq6GqWg9LAyZSaQ1p2jO4gS3GO52RdM1zk/kVej1lvvb916njBD4+ETR2hyip0e/N39agQ2E4uSVEGghOwN6WYXpPWQqgRRjyha0wCtB/EaOgzLb9Pfu/Z2gDPJbgFAHz8PpANbQIyq/SvsAQrZCnUkaTL5UDx0hBQuWtrOtqcReJzBYjAGoQxOv0HSnf+5Fg+TUohWeR0q3kQ9Xiap+ObpzxX5eZrb+/dvcVuzkW1i0QoGPSIFiZZMqRKkVCpMjGZmYBZmYCEg1jDBJrQZ7OWgjSirppuMh67lD7df+KNVl3LJKjTepvzfWpntSoeoAgjCbWLjo3T1r05N/66uAe7XIZoFwNkwKiChowYCfEDgLutynkDoGHfenroNPE9TZ/PasmSEjKyMd5djvg7F/LDlMaaaXgSHm8Ya4L+51R3vQjmWFlJe/PwkCLK2ZIrao1UIT8JdOgs824sX1UVVRHw3Xqt23FhdSz4iQYIXwkPStQfxtJicUREbHtUNErA+XstdorxXhhhYQOwU4mZQLz8NoimLpbwszcvTK/f00Rv9MAVWD5hHoyHg/hM1M9mJs0WgvXv1d53w1MtvE76H5udu0FuuqwYoqA48EAPIkMRoo5z23dR7BEQaIAEAVZTcQn6kRdCesSro1vQjrGf0cVbFR8pNZlYwpjHK3tsuxjHGKNOAac5cyeYw1zNllJg1TkmoWGotdWCWP0W9omQsyZkZz0Hy2iDHMg8yr2S1szaynrEG2UqsHxJkyzkrwXcDIFjt7g8ZEAZmHbOmP2gzIzaOXD+slZWIT+mkOqGroajYAWm/ra+8xcyPglVJPHNXew50oO5nsx6bFd1Xn1ybYF0feLpL2M+nnkqOI256UcjrotQawk89RYYtoDPxnjgioWbbyctYjKeoqus0jPMfLCe7mjK6GPfaEguW1wYE0h7Qbq/1DexBJhQjoq4WpHG9Lg76FngorPD9NMndQbWkG59P0aJ3oPoW/emn6fuKrU5LX8A1xfdc12PaN2Daeic32Tp53hfEBkd25/b3slLKr9Cs2aqBqhosGijCdXnIbTxH821ua0erQbGbl06BWv7/hiiUipqGlo6egZGJmYWNnYOTi5uHl49fQFBIWBwGR6AxOLyMgqIz567duvPgkaCk4sWrNx9EVTV1TS0dPX0DYwg0iCaIIY8lnT2aJ0QkE9Yzrm9COjFINU8nQTfTIME02CG0cap8msYZspjzWVLY43m6FgoSCxIPkgySCpIOgvWOAAoajoxF6xdSiI2rZmlAi75/MDmatlr0YIKGdww5LGmyr26E+pRuzI0bSVKkC9YDAimg4chQ7BfSiE2o5mhEW2Sd9t0/YdI3bck2tAsaa3t6FooWI06SFOmCBRAiBTQcGYqKPRtii2mHHTrhYDHJuhAWBAwkBAYz/2EYhmE+wTAMwzB/Fn7BMP9hGK5/a9tW+ijKJCoIDY3eOvMq2C42YWsSktIUIEq+Vf00Rd5PAxah2YbAXvDC5YkKjpitlIq1ZaMStsFqD/TWysvgZfCuRQuFwDs+D1uVoIAlIpNw3i5QECwqrarrOk7l4QK0SRpbswXC9M5wJ1xonZ0sxTrpkVs+A7HcechSxdN40ccwLM3WtiRLpCgooJhZPR1N4zJg4GCg4YacYVILdUGFSYIsVBpDfD7NtSGUWX1oiGSJLeNCkhRpsbOEQEkDR4aiDWjZ7dHnj4myxpGH23bDN7BcojIurIu5cSFJinTB0hFAQklTmL5wmIEiDVr0+WMyPgvPkqdemj1qYw/Gz5eFe5IIL3CVsLCmNSJXMMmbjkU9BoynswKz2cRKkgZ3lLVpvPmyHYCPWLjc5A3TEc58tHC2LraxB2PlxXoAmXkmnUKdKTlYtT19MCecCf8okavYgh918qA6QHkiVS1tyG5GwLpRqVICNE6SCoR7fH0sm6dvg8eq4BbU27poGDYgW/V0vzqPIbN+eLrv8FJ/gSkucoHOe1X6yn+NTx9WYIvCuXz8YraAHLvTopyXSkJvA5ONt+3AlpvdVZxwGZxsooCrplZqYYAdetlhgE709NZDpK42lEtTHNhaPZTgUQiGdGKInZxNdZCsmJAniuVL/xHv4lqGI11JSAR+XBM9deUC929Y1sDT2/6fb9hW1X3DocK5fkpFsHH3A2qZ9TsItY/6IRthOn9VIHQddHGHEN5mAyiQQ3Lq4FLAulOKCBDtOvlRARAACPCAA1ygAQMAMNBBiAl8YOSbXjLphIFsXVhbFCYQECUAPVMREXYpmADBkjObjYEHmAIgJVgRIEBAonQafVPWJUI0cIqYFDGBDXROQhYhYAAnCLAkbGAAFA1QV139DHQNXUfXOVcHqKQw0VZMlo6tsDnQOmsOQJqzW8V3RE8AIP6TL/M9O3xlCIBI0H6nwzhA9OmcoAWtAwCkZUn/qBasCAhSLB9mlIRRKQfqyyBI/cyIXdwTmobs/VhPTAASSIPMjH08sjrSZugfZfkQwN9Lf/3LFCBs8wMAlN2pVCBtQXQEG9w8I0SxH/OqAq0SndVRr+b5YcmzB2bjq/c3z8Jqf3GO+MbqIqJiGuISklKa0lsGYoq44lgxp03zvnz78but5TvxZ2Lg1ONGHTfMiaxEqiggnlb9CEYfvBugRJBPux9NErA6DMgUC+F8jXRo+8/ovis1ZsGEVYfsNKnpcG4JjInf2oImukkG3hA5lR8mTwN8MaP0XJSCjW66AZlb18JeVmpEPvD+tscCG3PkbP2Xee8h1lYOBSluu0ocK8FDDtm9vN2Y72q2SJe7bivwfL4PXuBgwhQh/j9lNpchGJubnL707o1fp98RIwhiCy+ZkUPeK1Kd3MfQnwylwQY2w3rG3rsd/TD8Y9aoUPiufU7DihXZsOibVZ/0uAixK2Kx8+wb0SgBMcWKM2fqGh0PRsxhNWkf7IZK3tzHTshyS3DLSYM4AEJd7zM1Rz5oQ9/6udmdzSpyF87GmLCZ5V9WnukFDqUnAvqHe+/LCQMKKeWMLKdEnhTNtCQEXDxtJabVw3fU9lmDtK85hKC9V4l6fqVq2Ifb1mRIkR+ab7GNU6G3NadUxKih1UTbnAzVotmsxScIO+H+B39qgO68ZbdJZN4bu4upZc9TL8MD+GBCzDI2+sYV6Jy0OzxnT9hQumEV0wu0CqpQv1AS3tjJpNpK+PaIrYBonpXLUBOd6EuYiBTvvYE0zPTIRx+EUfHux/uMNDHsGxx2bCPTSXInDG3892+2OXkBV3Aa1unZgpiGVheZV7yBw7ZSCrCsRsfKhiCP7LVqOq53R5QYgmZG4ED/Pj8gciKpbFaB3JrG1exAceodolPsYsVEmkGY/hGrkteC680JxFcNIxctBiie7RSMgLjRFRvSF7UFsQigOhR6BooNbcEJqKyDBAoPwWm5R8WEXiHpKx08IEqDmhbf4W9WK5ElmJs769CAG7aHXSfK2BumZn0tQ991pkTauqMt1ccOiI+Y4bwNhe+6XdDI63ZCTwub+A8Fw2y0GYipqISboN2Z7EFAVTixA25TvgaQ2HYXDmfcqthuYF1/FZsB98gghDlwzcFdvnImQnDToJUWsH/7HqSYdXyb/GW2gHe2UeL2lHFKv8qxiod4c4CmAg5tbr8I6Z7ldudzykvuZ2sLKfy2NljsiY77yaD5wOZOM3+rdgSlxq/7C5DqTnTQXmmG73k627EPRnpi9T+HCKBDIwMCWQeACBfx7pYeIwLv8tEnSHREjGzD3mPRihpLVIKyfQJ07CBdddMElCETWZsCNyNm6yYje1ZcftBJyL1AuZIovkzKiBcumSouOeyw3ese9F7veVMd9/ImgfgRMk34ZWtG+afXQgubvTtpF9Plvt7rN/d1Dzjp3GDRCkQJPAEff7T8/JCxrzYGmvAkTpYzmn4zfUQB3eWrgIsCo+9UFSozAe7SM2jlxDM4fX/tqDzG8/a5z+fNxYz1Im6zI5x7lo0kzz1Bo4hwdf5eImBj32Fq9Vlaa5uNQFDQyTMFsBX3FzYA2Dj88grrOS7ebdJwJ7KkOsVZk7+WmZERoZbZNf7Ki3y8DwwswY6ioGx1sI0gi0TsSJSHokjiOtRxRQbhuuqB9bD7qgRbh02kyKawhIOBE8Z0zDRMmoZOot9RY6fxa+fUVOStpGDXK5qRht8wN6411LC30jfdpPNAk57HUUFAYwjL7LK/sJe93YBR8AoUjMHsjrf2bi/WLH3pC+Fm6a+vh+0R/mDIvy89BZ9h6Cp3v7B/NN5fM3w7PYt7Se/D6K7VbhcJyOrJ5yVwo/0zYjDj2BvI68jgRigdu08HAPSGp3pv3XmjuIa4XZg1Sm+jpdmsOGOmtGYn8Qj/YzI+/iS7cmqyiY3k0+/6H0UVzChG9LQDaSF+hALLbRpYza6xdT29RefKGv4FaZvutXV2DXZQI0upzE6pHOPfl47FBWfHBo/BVNngC5OB6UGpjPX2v0a/2thtfA0/+ERd/AncgdM4Eq9cLs6F2emXDrkcR/o8M7vb1/78H65ardykKQb9d1KuT4B+ZoAt/4JU5jNUEqJf4bKP+yMpoMPjLt2eBb6ieuJB6TIZo5teYOnaKhfru6v+DX6IQZsto+WbL6jhRPvv7eL2KDHjaImzjmSHBRCF+GxLzizqPXWo/E453kW+4ur8gHy1YDXm/y9hAP8SXBf2m/z6i1xTQZU7qgS53OTkyhRyDkBmYOAIt3lAxt00cFD3WgRMmdOTy5mi98zqrtxTcbl46syPphcFoL/0zsEHRuPQdFhteUEnrkNHpLQqxg7Fc0MdiOvk6ylKyCOcUboHx2YI0SOLW/u9s5AUX7gu2Oj1h+E/RRG92C1BxY5X9K6nQuW6pSw/xiKJC/yOryNuVkV8Zq+eJNzUTf9UtYK4iq/qK33mxmxnluSuiUftZEn1skKbsOfx6PvG47Rg/hkwTgpk2ft7AmeYfd5y+KrYzMG1r8FFYmohcWoodXUENWNLTmaH/Nbj+1rRV3uB6PQTg2LlZk5zi5rY0kGy97vBjua91XlO9uCoJVjbjr/UN+AadGVV0G9uO39nJ2O0rhFXo8srg39xWj5nkLFLi/yJXGJTn3grLbwkqiEMt2G/duMgbg7DGxZ4KYs2VDCuVxYR23BYRhgxIrB78giEKfmVO3A0tEV7nCOWcb5ak45ESUB9AFqOw4u830zLqcZZxPqT0DpVEKHjYn/Dj76fbBg/tRftRI9Ooo5BQJLFPhLknuq6khugam+jfsGXfoSMLmi/45FFSNHHK2jNACDfSH9fWJLpCOP4eLj8Gs1R5V+tqVSqeMeMj9QvOBzs/ZQ+Sfxz+USe8LQVio73LCZS7PUl5ilsH0MZiC/cMLVbNGuOne1CcxubMBuHZTkm9ou0L3LmY95Fi0DVF9TnGt0EvpXfH5he+EBVHO2oxOVobXtJL5C1OTbOrifAsWKgNngq8i9Iy6BSdlaJ15+tP7j+GHjhUldnkIxeoJ/fkCvCR2aj/yG5UzV44wpeLicprSQHJxENmll1Y/D5c3WvuYGk4anWGw/+lxReIHuE3kFLzdhnrrpmG/EQ/2WwBqvnfE1eTRbRQvbfnTf4HXSvfGCG03oKj+TjGtrBVt1G8MIbBFCN+7OirrFKBXctyR/a3OaBPaks9YZFM/8I+shA+Sszi5gbXkySySVXtzYUPQ5gC1ER6m0SFvCSUqtiMah62yUkxMvCpv+F1/Dfgs/yb1j8/4Em5SYk5Wq1W/Z8zOdD8zmXoN21vHRuTGp+PAY38cAru6hS1eXoEx78ofhAcmnM+XJxirj+JC2S2KNasN8s2RN0ry0EOX3pGHfT+0QA0bl5q3XM2OZ1ngCHewM188L+wxv4ZwjO8W+Z//+hMmjRzDe/Fg8zWngVL5sbm5LzLbi/jv5sFbXeOmokYMZSIt1rzWxTbpVPIbf5/YEF68kQzM5U6Ux6J1joYwNuizJ7kjJkzX3XXMxYpF8umt6t+jF0TVyorHr2aw6FWujtM/2nC4YZTkXrl7Hj2MEFKYkoGm1IEYT9AGZ2/dGx2Fr0khx7yD0iuEksi5geuJOewD5mMDjAXnAHwXv6qW+AI0tzolAhPlPCTVI5f1tp9gHQuQQO96UTuac6W3d8lvf4+HnmBLkg9cs6Y0Eb47/8s2jJisJC+vr+yV/kS/+VoPXw2jH1qcY7vTv7yorQjAV0hUumr5IXJdjkyzUrELDggt76wYa5pfNrBdv5PXt4NW7dSw4Qqw1PDRue3j7Uls7lrxFsP6Jk2LUDpJMvvjfCeqJtNVcaGGeoOUKFrejts1XPKZFQWHmzIRQLq3jJtUVJeAxhmGdnxpS380L44LtZ1M8i3qpj6i78Dn35pvTU+bLM+Qq/OLSURrsxOX8raP+Ucpvf7waATHZACbcihxflX5C+ycc9MLI5TfPxvODQBe9fLKyD0qzQaf/gFYyrvAv82+b/ZSj3wHCJyHjxsBBK9qzmZXOiE/MSMaiJyn0DDHrC8rFJ9MehH6jTV438tqfBosf0zsKqfKKJvHHf4vMf0L02wogk1pYdLMTVuLdDp+kHGL6TiAZxPdFfmDPKbKMts687YSTq3kI8xwTJGIBFo+I3JJ5L0Y/EBvH9aU5bucvg9Yj3bpvkqfnE79ZLw8sQTSpFU16aHL3A7zyVzaprvf4/fu1H4N+X6ka+5qXGV6bjUVgywahyVw1Mfjt+FN8UCR/Iy4xmvcQ1+GJ9wC9+ixhTkpnuOvXvZwULG9XEUX2MSM/iDq9J5qd6FrSuaSs+54YKXFxqWQF0Jwt6ZHi6H5FJrOsVrxNzaqLXgQ77vOUaaMLhU3ocmdupdbc8vJXCctFisunj5mvEtetGnO8QRiQ7MRe02y/yJL7uOQj35EurXawjiasA3sjsS1RPdtF8tQdh5qm4sJIRje2uJU+pnpwGfzxktnDd5lV+DSBiiGactYVhwrJmw/yv+8ud9w1X98uw2jfrkvXgH1HPtkynbcPVsx5jvm3mLv7YZCWYG6lCOgVnRc120LItwG5kbH7rA48Cohc9OYFbPyHb8MUefjk+LAdx5SbyMGjs6QIfFO3ItEl2s7eVoHQX3oIhYDf9OnAYpaNep8AVYGJr+aOw78jv4/Ydq8DDnUWSneX+e5H0hiT2mr4SzjHUBdtmS/YByxGqJ9sg4pzxu2vX14KX/OXZAYz0Vo09PM/QG7Bnmmo/1wince7RpqMbNz8ufkyhvD7UjjgfaN3gyFXjEbezba5nR6COCLYBePI8Z4B1ZK4PtT93mOrJ9dQ+0wTaFR42yFbN7+aw/107LQfUhtaOwm2+n43CxvIvx9NSCTdw0PTcMey55ZF94/pHxGG2b4Dy/hJ8qvCIFTOAST5aRddml12ON3j/157pO4PaX0VPjSm/Zqn9AFtGA9fHcoTan9NO9eQcPq/VicRjswUKsHTYLj5APrwP3Xwqd9zYecTEJdSOndNA8yLSFMI4w/8qDEi0BziMhQ41qOYu9oCdC6oH3vAnvDYuZCjDgUTisfkCz9vAnr/QwOP1fejFN/uY61nb8O1rL6me7Bna59SCVOYFPYRAlB/M8WK5OC9xxrASCuzZyaKKyxIJ7ld30J6A/PGAzrk6b1QQy/d4AcyEst4bYWlQhU/U+o7xWqYI17ag4bp6vAPfeknb9wLIAN8sD3yRFjjZE9S32jAKgxqhpPK4/ROt0dO4Bp+rDfrHb5OX371fUGcdOS2XKCTOF0Q8YJReBbdzAr0LFyPfqURseLE/kU1uP6O0kx5WEbYyFOcQW65Se2DhUssv/puHbOv69etI16Pu01xayABqPaPvwmBsr6urDfoGJmZXIRAVhcC087uJ2Z8q63fgdtR6V+50rkzxwOXzmxehhXyNM+5TizX78kckxpzcMqICRZUzM+jDnB+7O9R3dKhtHVHfSsLArsWoLFrk9QJY8eV77kWmErX4VPViGb9NpIZmmDyn9eIbr9D+5+GBaV44hmisndbhB+pbnTjFIY1gQ1ouyLkPe8mbh5jtrE0T76532DfNl/iYTrk8uplcKr68KJCR3KLeLVwaeiPP0tT6ISxBBYEcN2HVRgry1rbZd44sRK7P7IGLN156PWvd8DRwtSzNvv48glBeCMt5nZOLBwlG4oNq079W1u/EHaj5vtyJjMPDWcckenxlo8tRzJ255MEq9e1VqutHNNYr2xFMDGwVF1pFjVhH2c0c4DgwzGA2c5sHzi5arpkX+h7MbLKfbmw9/pmp+RBk3On2VGn2UJ0uWHv3Yiuux5vOsjroTvyt/eeb8Srcc45q3YkYobax9siFiEvkRVA+jBCbeAfkjmJTucGaZNhEqVvMXioe4d+Xjot8FNmZikNglbInIeX0qFcTF1lIRVrHnF8+qATGfUXyq/bZeai/djv5kLmSkd9+4ndUHVFF9KemXMYlP4Gell6YQWSi9WncMFHRSUeJyoDnwWesViqv/tCfyFa0Ej5m5d8mK2TAyK9eXoKWofVx8GGXDyqLFnq9BFZ8Re+t8FSiBp2r9Zfx2nQE3c3jn6tX4V5859WBF8EBWYtxDV73nfaczgGLRvKWP/7lj8+rby8UlBO0673HezW0dYkCeAH3HdcNO6y7rL59I9XfMBT1N/bv+EF5w2Yg0nUDDABggKpRZBUm0Sy1cXTTgYJkUkdvbwZr0SEgajbx2jxMA9OXxpCnQIrmpTkRg+6pBPzgwIQrLQ8POnwEyEnEkvOH7nZRQBEVKfsQbTqo/qw0l9zVXERJYm91fRXSv+SbXqCsbNsJlUZ/fOPqwqHrqQFlKTp1y5vufenFp/+qPfG/XwDAEJDHDguMALnrWDEBxKSSzj7gaYcFeEJMeEkZAVr+KwzvtGOq66S8QHkfvd40mNxjQE5wjnWhOka1Cirgh9FvYhVVE1os7brM2a8cSW8Y1VJxaZd0i6YT6ls0B3gF5TNYz+Jhbg+GID0pA9KxnrDojzGMVz/ewXBpuH/tIhfLPppZIkxqmHYDc17cXt+p9ad1Ph5mSFG0R3RG89d1sTn3c4yH28nS+sYRrQ8ahh0rx4orSofSBt8+AgBC9+1R/P4N5c/7Y+UHAADOv4qtAAD3h9frT+L/PpXzZCCAAgIAABAAI/FyACizZNCNuATQfv2lqlarpV4D+g1oxr0pXxiWqqgk+YPrGc65TOIPkyMM9/39ZSZaQgEY5ozufO9zs8bVWNGJsbmTBprjX3OSxSKx/Rg2qK2vfXTd6YMr053Z4PIU01kJxslgRrWKUT3RUJZiHo9+efwYbWPrq5p+PtOtN11x0no+x2lUFcNa0S8Z1rXN+dZ9+hXrwkkw9Vw0tX6q3jcYZZBuzeJ+DMzO05Ymik2y6SwJpTzp5dut14NAIcWU40snpX1ZL+mkiHIry3rNu6SsciQ+2E3qjqa8+8jlD/ftWEEPe5A+3R1EL0v6IP64UnHu3trn+2gdUwFezSvnWkV4ftMtFhihBL1bc5QeToGUx7UR0CTQA4U7VYVb1SMHVA7URqAX2Hk5gdxTYY7bGBAH3VAHqA2gh/qAbkiLEr78N3bBhvWbDwQAVVZR4IsWSNhbMSXmEDZkQjQMiKTW2BAwF4GKkLkEcCBnLoZJKgqSc2lgYBeh97PLv6qwov9Sr1iQXr4XT541HXO+uIGOiUSC4om+Ky9M+SSwYmIj74F8hmwEWHZmbl1bsVTCfBMfjTS9Y1yElVMtHyh1H7yHQxUI+x+/yVNebCwm8lMisZa5+IQE7+9jOiRLOZBrjFRVkO3WO2hNRlc9rFxmJap7Msle2acybJCNRUnB8AqPtIj4neykQB5QlZI+AAA=) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAANUAA4AAAAABbwAAAMBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI0ghgLEAABNgIkAxwEIAWDMgcgG5sECK4GbGM62A+KOMNGmZWUwcdhKI9l4Sh/WwYP/3af9w0W4ERa2bOg405uoSptTooGKkF8HniO5b+Iojvye4dReBbNtVHwcLQTG2gBzQfYOqjJ/XYU/jItwgxa4I3czM4Fj9LAAnlHz+dzgSO71Jqn2QML8H66dROj0qAFLYnRhtm0b89/erW/v8l/LA6we9gCizDBtQzSf4EtkcwDT6RtmgYEQXnDKGQslZyX/CkQSFgBAE4ERggEAgmwACwQgADMsONAJKVkFWEBgAJgwMz1NlLWec3G+jtZu+rXO1i7rx/sZi0AEwB5WVY28FUE1CORQAjvtSPftAwCQQjGAbTUfm4qwrvbNmDEf5pjR4JoxElAiYiMWjQyIAEy4EBGAA4UNKCgIMC7a5Cej2sCAA+SMEEyYA2AMQBWgCmQAObACrAAQAUAJCSDMEDmo7CztfXoRGu7SUeVdbvosOq6N6PHnZ2yf9l3eXPj/q2qXdkjBL+qrix1cYsqzItOvXfRPaMXkUvPeFWoxr7tZB8gfxIhMauBapmSUhO8d3O8wUt0MoI7UAxLzt0/zhCwJnVHrsPYXenm8suPeLYORWqn/3wwK6Qp+frDiYGvxHSXFzoXfpihfmlODl9oFbOqKa8nXbZgd6axNivh4JS8xEZKChij/nuDBPx/MrxQA/WBACCtK44947xa66g/k0YcALjxaesDuBuQP/7x/3bTwmQACVMkAAQYd/7HYBqK1H97hriqWIzlN7cD8Qu1mY6Ql7eR9v8qAcCY/apKqAgArEBCCmOEAExoJiOUENTgBAI3NSBhwSjIbLboV0Blo3PIiN06hxVFfmrr0WtMvzYtWg3SBPDjz58mVY8eLTrpNOm6NfKhidepk6ZAbgbym+oG6PoN0zXxUaBHgx6Demiy6Zq0GdIl3aB6ndo04r7WvSV0/Qa0Nd2+yKcNFCrSvh/6dNKO3xV33aBeEXxNZKTyQUaverfOR49+LZno1XUboBt4oSzpEiXLUSjZDgF8+JHBMIY0KQAA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAABU0AA4AAAAAJLgAABTeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmQbi3YcNgZgAIFkEQwKrkSlZwuBSAABNgIkA4MMBCAFgzIHIBueHrOiVpNataT4nwk2nboHhRIwDgpKyhjHLyLzQxmFwTYyDE5esZ3+2EabADRB2gAnegV3sg2h4vmn/cH/ujNn5kEfUoTVzJCo7tDcxAh1qBL7aK6c2RAfYY5oH5jywGzfVxj2dQKMqiNV1SGa2/3fsqgYgzZIg4jcRiiRIlUD6TaSLHVGBGIUGIlSIiAWaB/Nlf92N3lGYYsKSKjZnfSTB8DmMi27e2FKIBTaKlRVsztJrgQ/v1ar83g3J/7Bm3pohA6p0P68Qebt32Vvzv+J+e5iNnizRruQrw0imsSTJfEmoUCohFIvESLYkJkG86bdWhrvEfNUcXTtnhaEruXzgVaEu0VRWgYqCFQSqCJQjUANMogmzaJVj+izItbskHExWMtGIeDVV4+zjD3+RFc+yF6RlRIHstekRMaC7I2haQkgC2+4KiUBmJDOA0pVozaXNfBR9QCXV2CAnZZ/Pa939bym2tY015bSKkq/1bW5rl2W3bLb9zSVW4Drhr5Xrw/3s6jw6wK1JMm+D+n/woA6vO4yKdplbgIyweLmY2gZzWw+oG+f+/mW70DuJgYtfT7LzTxPyqddT+nC3/NdfLWlUjfjXEzmQ/hpKLyQ98ii2GeJyRwXTdK9mWCse91WkQMY68rJFB88T8t35mpaolV7x53YfELcGYe/k5e+Q8OkBTnHYqOSF4OEEujtXNjCIqJi4hKSUjJyiiqq1KhTr1m7bj36DRk1YdKUaTPmrFizRZJMikLoKiGpjpWa4NUnWmPomkLTHApWNF+toulu2I0Yi3nKgC9LYMKUrGeVRDIh1kjzTns2qSeP9MP0pJk8NMecFu5MvKMmX6zA/fX9Q5TOL5OXchlXyJRSLinno0o+qMoi3UyrVXFduLL6vNeQVxpzV1Mea84LjsgLhbwUIlcyZi3jNgFs8XbW2ZDJIg2tfzlzKEN1ZtUKbMD8DXNXQz5pzDQnsB/gtQLeJN4m5izUdKksg2nSRk5D9WyKQs/IZRNpGuhaSpjhGY1WObToSmatUWx1JnL5ZiO7F4xkJqXyAGWpz01EMiOaMnHN14SjHwXF8xU3i1ZZWLxpN73ceAqTchLyIBv2QRYchjzI1TkEbetj5cxPxG81MA2TYoHqf182swq5rkjT+39QyZjqzKjJ6TL4ACPwvPgGZpVcE6wV0i7YziJlYTFgz06wSoJTcyZeux6CfnM0C5WIWhExayJu64faUNggA4GImLpCRlmSyTJArnQhQdaTUlJopaw1sgZU7ypr6OEVYGgoYhCPTOddtBvLdjIHMufBjQi9q30D8MqGOGCoW0HhivaBxX30m1mMYRKTOyZX24T8t6yqO5dvKWY8MQzAsmM2BOifOGgAttxzR98dn3SWhwPAfk8fm+A/AFev2NuADZ8FqEOHuBI2prgBmrIZBgrWtzvfgonB94d6Td/a27u4n+rD/W5/2MfyH/R7xOPX9W29sx/qp/ut/qDq9O/Rf48AgdPYjW7/N/rfSMgHsINW4FzQnGsrQe1COnTqEn7aIocMixoxWnLsMePiJtgmJT7+OJkeb0rarDmOeQsWLVlGrVpTZUW1GrXq1GvQaP2LmZ7EKSRh4BXwgf9FYOwMVr0KLHcx4+QVV2Bww8AOyAZgR0TFTAKBMZhV3EvUu2AsNqQDS9LuB4/kVg9nIEAakUChYKh0Etsk91wOkcQ08QqFo2oYDIWCw0AMCzosvVYEqoQgyKYVaV4v0TbyETaLINHkqBSblnAxWVLyxFhZiRT0Sioxaa/G0+vRiXi6Zpzgqf6qMzwKSFfUSjihado5YLh79B8qKJo+FF/xdsZkMlr6To3QREwg/1Z5syFRpJPGSR1WRZchQqfBxXCvElCFwlTFk8zNkqOywH1Jozx2tXrde299rYZi3F/j8hyYUCJzj+MouoariaLpw5/zWB0WCylI6bQBtlJsuLccTCwFl1fCy8BJ66uZzMLZRmjB7AZshWCpiXFLqMjZ+pax70kYJ4g3vdADAy+STlWm6dCBArat+kIJvSkOqDI74f6iAA6NRLZV66doUoUfq975RbXQxEgnLi0r3ZerpoaNaNtv8/mYTGpIneZ0iko225hRgGG6ATv8jFaUUQFVCVL6ZPgE2AwMokMDZTmtsllFK0U39mkUrSheCG2eXAF9/PgHgEJfotR+I+o9dmaSuSLeJiIkgrGO+A9EKvYluMiT4dFRQ3pTajHWl9veBQLEMja6I+NcAZBPIQSUPOluNyL7529e9N4yW178bFRuj4sN7tkVOYyfugKg5w2paeMcad1xefLsQSWpM09kB4uLqzoNTXGmScx8wUOVlR8LTv706zKwnzRrdE29H0sexg7yeBbE9/nzNc3zNHXCm5409hjYGLDVoJ4MDuqTFBLMiY5L9ryuwp4SXqdQ+CuWGi42IIFQY6ro8cALgu77TvsSb6Jv7b9xxbjOkP/JQkGGdIzmAxbccBfRMaV17ab6OH+KR4NEzlTuvmgg55yjyo/ZiaWA7KO3jerpxRvkVdVjPk97M9g1R7fFn8Gek9FO5zVe6ONDwK8lVlcLslVyp3v09KACk89xQwUmt85+2eYA7GhJolY3o2BkbMODdnNr+lhgpjFOnbr1/OBYib21aZpysKN9OmVax6cxd/D5qSIpSPpukN+4CIbSDC6CzbQR2F1wtTFvzdtHjnInQ2MDSg0NJmd5k/L2KvwzFd3KPmtoB3g3lJ0pTcCObzcF8NQLDplpnvYEQRGUjJ/cURmn3HTKPmjU7Tj7EwD/mL8sMJCeAvsFbj96Z4hwh008elN4nYEWhV/w3sBFhqVETU68vNhzRDiiRwVkDedsHC0ISHPeZnOxPwqyNFzQ6a9AyDljFvXSpX5nd/S4c/VY4TBr5xSNeX+M7yuGg+ZVgBVfhZEbARbPLLLL+EQWvW+HSGAFEgjB2gc+3P3eJD018Wtmt/jHZ8XdYf5Agz4qPg8+grlb1CPMR4sx/kqh/bh06g3V6cWhBvfrKEjvzKbFUqP8UzdB/Ol3YMueVGqY9OlRHADQoV9l63ahR2W4mX5NvIs30mrXaAeqlhLLMhLLlumj4uXNgRnRgctAZ4k+Kl4C+ik3jrueOf4g05p2t3z/a1reILNNiQPUJsVUfoBaWoAt/Zp4iT9XEKRW4nqY+i0+YI/nQ4NoUPlJPo1N5rMPVs8bKEWOkFoCQnYtOlYoWsI34XKM3XayooVDte/gEwi45CVs9jrLKkqU/6F91E5pwmZsnN7JjJAANBde3pGpR5wiHi9+UAyHMG+pKt9AtnygvLe/DTABfzBuMx8Z/fjNGJFFygbKGVnUhISyRIwBAFMTEyep2yeWqF0Tx3gjYUDboDOLoq360uwh6wWnmKOjO7PmOgOk/D9zUFGT1x1A+hGsyk6txoL1w3O8YQXFg+seG97ljQCFQeCozGjZDT/VNsIqZLh+40/qbvrgXvxizVZYidysC/xB2fExFRMdkeePZqFdlzi92NCCyMYQuAv67jbcSM3E+4BTayTC4V8u3/guJcJ4AXCu3VljZ61nYGdrtc7GJsTGQZRpZG/NBUpX+DitrYH8Y+PIeDxfCtNUgu6C/tmETvY8+ajxE5pgU3w1Eue1TnB5jmH3HDRfM3N1a7/k5r7OxM31ULubE7g1mOo8OEe+ajznfNCx4eCaH9K2ynJANsrq3RXfnUBr7ODMYa1d3nq6Ng6hTCcrQ2hnw2U6W9no3xzdUNfWwUvPwQY4lkxU7+IfiX5NXARWHRPPsyXEgkWQNTxMTj0F1qNZx1QuHZUM96hDR4uylvFNuJT1ni3Kqf69hQfxT2viFZmz4s4U3SyCBzDjLO4c0R4fXd33EtiFG/+f+wtWTlhxj1oxVx0Tf6IbiQFIDfeoDPfSbdzGVa6Nw2KtfJWRAlC2dBaKm9m/P/5A7/CD+7gWleEPcu1K1r5m0jXXeSNV2v+A2dU/90j/OJiHq2mt/b8la/sxvP5l3sAb8v+S9z2tfQhI1/VCtcPLvTOsxpzBUkrhoT3EK+cMdWuZO7MGS2gF4iby2dPAkGVRKjtwVXoPf2lZ8Ffrh7n2d0mHjCWHjBeKzy3lp70Xl3w+5+pgQsPK/KSI7+O/gfw7deoD+sprsO4GJNpdfD3m3HOzYjQdU+95wFNa6d6c6q37SBtVlUnZKHPiiBqzpRM2wTedkVxOL0VoGEq8fx/ybr0HNobG+T/DZdihtMvY466f3ZBAH4qzifM2v3BkD3LkOe7oig2qnMEq1khpPjoE+dt1SwwcvPFIuF+qF1KMhlZ53FxVkQczMc0PJY6BlceunoBPHlP6qJdfpAWuDDyFTyOWlN5/nlCMNsFUL+HwHD29j57ReGU8TjI2GilMJUUTfH3jPWEw0pDPjCQcUXHyaECSO+roydQIv2pfTDGQOQFumkX//qfCUXQ7O+/9igz/zgEO5x1u++yQGIlFdutyrhSv3Yy4xljupLkmrjlSOqhexWM37f65UF4PK+GVsg2L1G3Mc8//NcvRHdRdS3E1fG10U1iOEM1AO8/KnaHmRZ4OVshCu05J9YNVmsTjk94X3eMQB8weyv478BDm+aGGGWAd4eDuh5R6EG1YmWLsfaA4dAQkFPMJTnlRbhtQf6SWT3VaIMQU7nvpkYtchh/7gR1WLLfvw9L4V9xTNHAj76Cpn7JjCHQkdr3qzIo5YO7Qv9NNLo3HCJCjUCv7tcSH2DQV7mUgyzdhl1TuOwrb4PZHrAvko4J58lW+izo1vxQthxE5hG2sBfJVYzDNPgGvYJBZF4K94oiulYLja8xJeAmCKeBMsOe+NDCWtuF0eg1zirwwCy24p3jnwBZ9NIwD5yyfQjd0lOwWDhSPGhMMyCtXO6MaN+nnnCSckWxkSwelgmAgCWR2/DwBV3fRSkzzRg1ZgHJ5l3YQkhwpHxMNN1+n8DgKKy/0NrW3tVFPvAbmE8+3qPnl7Aogu8keoCElQOVaLhh6uJtZS9oYUhQsV6z6us8EX4/xEvXFuuZvfmvlUBM609Kqb6XyLJkDiDUnbg2s9dEIroC++P2K117UlK8ELtty9oW5aLKxlk6o+gzjnC3H02FEZaivJfFIzjz7P6yXe24DSDOjJwTcdHCs33YPcxDemCFcR21xthRvnddLy2JMHwxJD8EsxJw3SCiCaWjzYU4LKW0FPokf64bGILXnpduBhqH7EXjzLf7IK4AJ58f7wBS07YJEh77c3LwwTr3VFFeHem4ZiHXNjKm2dqrTdWi9bXYesq6w5RFdQ+DEy0DQogHGdTV6w465hZJKWIVcqff7Td+uxP2lq/zaGKxDVwvkYXxwthBJQJsG5boSfGQwkYEZfFSEth4DluyswAhPKWcLcJVzxEs7CMlGsgaoO0IcnbgXtwG5b8Zx2zEuiItxUOF27OVUKg9boJwzDtb3kcZov/auX27bDfvQE2PEC2rxDeCnnldJ7t+0T/oNq3UvoTSgfEfSpngyOYcYllQaLJNUQk3r3roFKUPu10d+o9bIfPVcRZER3p0PbBjiDS8iA2hBVL0A63MMrJ8wJhmUNXLPH7ehkgcIuSqiV4h2OjFP8czC274WsrTwzrzwwVvuUxulJa+Zea+PBKvVaExUbZAciVcMVErWe+1y3243jRahGdZbLgdgc1pZuw3tvhvYEZyVZem7klEBzOyT629lFJILyQUrssdRAxG5kPUyuWfycSfcjOwSSUWUTD7EtcPBGWQs+JU2cFQRFjmTWGmqb6V/38DmomcyA8Zo+atUppDValRReG0IOowzUGInHNe5xaGeZp1/cb8F7oJtT5lDBobJUjRl5ttTLmvXrknyQQqdfEiuQDWVyJoyz6wMFiLtntKGl9UsUR3bXR1+cClQsafCLQXYMq6csDwAzW+ByM5iEUA7kUoTVdELcVwCGoPsE0lFl84+w+2CbbPYl/D/471khHss2BIU+gNPnJe+LupQYTKGzSZ9T8QG4HJ3SDXxZr5x3+EdVYmHCtCt0EhTdiegTziEIqVZmg2GI5ojf15NJok75AT9RUXrr+vo+WJFNZpN6187/P1vu2UCU6TcbSw34otto71ytIVMPtD2wAJT4G0AvLEi539dOSQgXGeK402BSFU3E7Mg1bwStUPpa/WtGCt+wfDyseGwgCOHPFoooIgSyqigihrqaO5o+Gv0pH8xQ3HmBL9wDWYmBRZ7YBaQYZZQFirGdFd/bLBBB7f5SuhHF3rD7iKaer/sXCd6bi9V57pCqtkg0PwS15zTpP/Xh53uZEOSf74EPNOsl0NdkC6gnptWCcrgFSMqadxvxPi0vaaNQKaHEWQ/0XjRFSVY01PJr91+7jWZMMQ0Qq8F45WkTAZ+gGRqUcAorIBw2zQNMD+E++aMzfTgjptQ3ESwC7QbZyTlSvAks5q+3wqS6LsC6sxsGUwreQJ0kvV/aOHuz0W+ta1zhcVMltnswAX1aBlryUxplHde/b9VfMh7BOt4vGjkv3HS6XXwojp3WsGXahpyMjEZUx8CbddNNpTrsksM098IMisB4L3fFgXAF+j946+e/0ZXZa5MRUgIwAJW3Pg/BcCqgzRJ/4cdAfBl7TxX9J0inGb5Cxj7p6s+yVU8Sxy1HZqJhlqok+Yo14TGKKcDqO70ovf1NVfqmi91PJOVrqWP2+tpvrPteVV87I+VL9EEy6pS8xMOB4HoaM7ACLAxZHO4RGA8blWJ8nKMmB2V0ocpqW7QWYOZ7D+JKlFzOcoX1kElsqpcXGuTUN7p6/+Y1xPrlZiR4morkeaSclGOFsd++qOXxYzl1B6eFe58Oltc5e+IT9CoTVQzSczYIjC04jc8RVsb8i7Q6rZqJ4hoN0hJgFZArskxuSVHtBu0S7Q79k7pzzmlQFdLpIzcToRA93ckLeCQ8oHQjByMh+dd6QADaxVwMQCmoZCNaYTqaRoj721xdhon6yvw5o871Tn+ARuXrjy7cezQkTu2WtVquom2IZeWKM7szzriwi7KPRjOwrOl6hbxfiaZvvGQ9B6K9aUdgrti24TU+di9cyON3naGdndX67WTWpiAb4EkdeEWaHudJm3evU2Wu1eZmJx3vnOlVVWHj0w1o65s632U9I3DYJdZWF2skW+D37gRfQZMmuOq4ucnVWNAvgGJsacFAA==) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAABk8AA4AAAAAMeQAABjlAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmobllYcNgZgAIIEEQwKvFCudguCEAABNgIkA4QcBCAFgnQHIBsFKRPuMGMcANsgD4qiYjAY/JcJ3BiCt0FdjAhHwWJRoioVqofQRAWsbcdwTFm4VHx7x170Z4aVJ4CJpSM09kkuD19r5euZ7pndAJE+GUSbimK0DOUJdFSEZVYuUQf/gOZ2v2AbOQatAoIgKJWjyqKqDZxgUqXQG2UOxPhRwwaUKqMwkjYw4J/4e2Ln75t5u0CpFnBBkkJAtNf/mqa7Uv9vV3uFpwBcAcoEEDXXqrQi6RPJxyQfIOEBsBN8zYds5+hm/L1wwAuo56ZGGuaybvxqbFuxZTAnS/sRUWKK/v/rLFvd+eNzxruVdjcECkLRJR12VNX6X7Klp28ZB/StIdKy7fAgVGHsCSpDCOn0KalpkqJqs1U2p09R1lEH4kj3W0SBhy50MQwQBdH3fCHt3Pp1dCIqInIRT9TM2ddeo9VlfSrbhII1+69FgsELwGYY3KRJQyhQglClCqFJE0KbLgTVAYhDDkHYsodw5AjhxR8iUBREjFwIBAYYAgyBAAkYZBdFuNVrDzmD3J+MxGiQ+5sYEgVy/wKSY0EOcmRfYiyQIXgJAiSgAioUVSC2IEDK8+CApWOshcOMwwwvT4zHW+EPE9n4O8R4YjyRc+wfj1/mMOPm8z/EQeO4zTFEkCJ+JCgTTAi+xBeEMsJVwiZxIZ9R18jhLPQE1MVJVGWrZxJziAVENnGEuE6cqhzx+/Q+kvMBhpgMOIC6I1IXiGI/AVN8lDHxtkVg5NXlVx29kzHyC9HfNU2febXXfdMGiHXGGOlYTZLlwZQGK5yhW7HicNFYFiz/Rm7fe4KmMxsrLhYbutMQq/FYm+9xKbHieyoxe9njc6TN73vdJ9SXHHMin96D/t6Cj01N3eor0kMf4IlPSjRwVNtipfVWOirsNjJyeSCuN9xREIdBkJ0zH8p0KrRL58eljZtOP966SHwllwdsk9dKbQMfCLBXDDZ/u4WuY/7Oly3mtNfrXYMVX2I835JLjXnLOgMbcQXEcoPy6UAji3rTGLWMUiwRASF2lxFZSXwp7s5d9akLR6PmioFRKE2stwzVDWr9J5AY2UnGLrLk7CZPwR57KVKiQpUadRo0adGmQ5ceKn0GTFiyYu2Ag2zYsuPEmRt33nz5CRAoSLBQESJFiREnXoJEyVKkyZAp2wlSdjZBtgkKrVPqG9Ve02qKfuMMW2LcOJPGmTXOvHEWjbNskHXj9jfuAGADO3Lm2kF9E9eE+NYlASkXTOu99JZkKjpWlK0pp2rlNolgZ31k6/xaDbLspTjwUF+STTwW3j/RewqtUuo71T7S0sqwlUiNCdoorijeo/SKcvuAP1avSAeRDDJZtb88QYp2Sq4NAwJMaV8ZTsiCKSqjWKY4PFFuL3HZ2QqZNshOgYkUlVJqDWpF0EQc/7k80pcJau8LeEMH8gTCFrwteCtwUe1deNI+3pIBClN8LPtgXx854ROESzA+iXhKuZMwn3TXlqMwSt+S6R3ZGcn3hoIiRT6+Up+Y9pkTBYHiPIrfw9wW1XiDRbzBayyyRTKAeQO+xL7gjVnAqS9kGXEXzG2NEP2WstLvDFtmrMikYAZzWJClQ9aF/XQAsIEdnCkJSKH0O5CJY8ghbFy6Lq0N2RzhGBBc1Df7UHqwNwisQnIEEqPkvkidlAGcuCAPgy4y7ZoNpmJyUjJBBSZmzGmk4ZKBbJyQHG6ifrIMaB+H9rj3gLgMUCEavWWF21r/k6MSlTiNVNwycGITgUFLUCLT1jhxmNZ6UsqetRCWsWDoNdv1USTyXaWFgrqBT9gVRs041Ev2TXDdNrn3BnZ3lFb3U30INxwjPL16c21//PufBCwKv0PxslWGfQSutdwzgCFPiAETpuTLbRdMVxsDWzSDD4taQ7xkZKMTR5CNDBzRq2CJEtEnU85mw7Ju0G35mcF3nQmRgwSPdMs2pO7Ddu1yFB60LfoMWT1fydP3ahn/QSGdCRsrYweltp8+6HhHuRAyMQlRDPyhNDYe/LHXGIzC8BNDw7AxM3gxDmQcCmXBQHVxUiQCQ2BjuLdKAkbgxY0HHgGoceBHxIdgleyyo0VLg/vwO4UgwggBQJx2OvDPGR5QyyH0QCxeWB0kn8wBACCTdB6THVEfCZ/R/IpsIuLCYQ/cJgQBN5vhjNNFAAEypNd1TI5JMGkmfVVpkFgXW09f5+upCB6UB0UDpOn0odY/hb4AVH/PMXnD637aWYPJwM4fDfwH2P++UIEU5CkgLyzMU10KNqzAceAYWIiOsyxHQfs4MHluVsmW2S775eLcMVM4tkCGm5dVs1W2z0WZucr1kVhDxvQ+/DN/aS4QhIduBi4/0iVedvImzWfb7X9+CnQrg8gJtnvvSb7td8CWcAEUb4EfPUIlynch+RZ4aYkMGTGWxIQpM+aSWdwSsmyyajrR5NBjHWU57Iij966Ri2NyZHOFVNqFia29wg1dGvbaboH2LBh8DqTjIG0CbIWswM24AJNgnOYs5qNZiREsx8okttlWK7DnvHVz2/fhIPFyVkLickBEfZBc4/N+CY/JOJtRWS5CwUZX2TDBpaz0awUQeeP9bY8lNubIafOXxWIP2PLD1G9ZQYrbLhwnT24t2+YrXm7MR1WbpXHCl7rWwPO2xRIHEyYP8a8wPDBmGLEp+fwyKLbNpSwijnJiVPRV74J1j6KBeE7q0KWje5YT6ecLbIkUz27p+rNl6/6jfxNaEHVaiMag54wjx4jioQjLMLmRQwzHuNDT7CBoIDmAJBosfost0e7f8LnyqhAl7l5J9U7ay42+DTqvdepWct6IdGKfLFYuK9xR05+i6UQ8LX0LqiJWcswFzi/o8pyKSzCdYvg9de9vb+CByFvsQFDLS/SYWE0p9JxJug4afNN9UgI2GUvEHGuQzOrsDcRGLkhTiM126adm7GYOrmQlf1zNyXBN4Sj3Rmn0CtHAjLpPJoTtyQNu9PCqsMhkJi915gvHU+PgfrG4LrAVBPVyxQ109zdYYePPpnm+2CK4ZjN/9jNGuaLnqXzZc5bVYISZo6UWcUzYh7mBa+l3lxxV4ZDppzseWWu5RufVQakjF7gsKeeO9XBsRFyLjp5HoXoccbS9Ws1iki+WL0PZXuWoMsLGhbdtBwciprdUuCjZL36RDJNaSZnmHQy7efi5/1uqyB5ZtIuly/aGFUYmVPlsxeSQS6qf/wIuHBQ4D1ZwxL0zqcWS+K/qSDI66UjCEvZzw8ddYgRcESv325ovZ4qWRVnS10/kHsX8vBFwb92iEJmoNHkbgEQeuy2AD0/5BK8W5GUjrsidxbQ/tWEdo9rlSlvia0fNf1m9uB4yju7D3KG+yOdIcxI4JuZ0F8/m83xpGEnTWuogpuVfTClRXpm0zCRl6qVjWWyvfeiqcyru7faGruoGE+2qDrg3Rt9fTly2dHEexPGMs8vkWrsQ5r84woqy5tT6YFoB0z4lVh6FJsuWW1vGg0V2ZNGW1q7KV0zneTpW9rAnsGHh7IQXPkbPiKaSkF5E1sRjB+SXFMI7I4vCUfhaULnG9OrRtvUOnqu994Ex2eqY07byfIQ0/J5cNJLDvYlDn9uwstcq5TEW2TPRWYlMxd7fT6/GUsz8f+Wu4Ol/g1A0Oxiyo7445MEQ8TUM6vAvpw/XKW3+owMpX51Y6cLlhYa9NJTutLOTHCanFs1oueVK6gUV2g6db/JYRZmSH75ocFqrKgOyVU5nLSmf5ZFvssuVtQynrXfvVdnPIZL+sXrsUUgSEsLf9U+JnBHNw6qyYiu8z6GFzZEpIp6mxkX2vrDqsBGE87jKoRCQxDJuySF3MbvkgFqNoz9kEq0tNDYSjPScGEnzteUpCsOwxM/Wgv6S6iBbu0J8y4bKAp+/0LfFinGJPTZkUTZJWS9jS8RJfNFuTYFE/dhUoERlbPF7vOId7q4H+XuAZ97DhngDnsBPs0xd4kp724hFfE4jPlgwGD8ceDrrgfR9Zpv0NPN+p9jSzzZoBzzz2bfvd9mhSTVBe1KkTt/Ovvfv5UfdNm7DkxfOZhIkjM9LH604Ep1+LrpwO9gcHxF/L7H5HaOdoJ03XKRBYlz7KIIRXhwQvdJSXXF7jO9P/rf7Ip0NF4u2XQcjTGMa7nltLeCZpXWTU2lgnw0DjS8a2YBnshNfJA5A2m9vEVRvMAcI45tfxudXnj9iHzl9jpZWUg4nQZzRcfur7xOPnRz9aECToyu9B3Eh5o57jFfvt0d9Hf6gHYvVpTumqij+Ol2+LLAvaZ8pNCK0Mi+T2kp0kScRE8WmnBcvX+NsKzSZ7kOwo4LdN8cEMRtRfyYkUNYwL+YvhOtRh3ijYku8a4NTxMWfrjUeF+hFZ2j06gJMMOxPoUwBntLPf7uTdaEgb07zVnozPD7zfDFEJ0zn7ezzx+OvYQdjoR6RfQnyWySH7NzrDY+7zrUD61OXS0BSYkJQbpA1yyGx4p5bavckC0tfLZd1I6/nuVV7SFu/KHZ+6JYUAIcEnglIrUo3Zv59VnB88pMQ1uY5tr7z3tnAU3bqpvFup8YoSUPxlU38JRK8hLxTF8AFpaIPJZRioo94ZkVHgWAX9ZbuNkO1sp+aRiZmTt0UCcVYLW3IToQXeMrVH/734kzhc7Laf5669M1X50qekdX+osSulvm8/OZnDzvbnuWdaZ0H0zf8P18rDdyPP0xCAb/QTkyLPzd4940sx23srerJ021OZXjH0ku5NROgulPyYLyjqD7DyTbJPvfVrWu3F3vLWIeyYwJDEtyszSPMBQ0vuTimuxV/uIrSHnrFM/xRnPfZ6MSIo87w4+rS2bkA4Wjpmd9lv8tmo6UDhGfgGy/f3b0Ptmm+DuZ5Jm3BXSHgG35wZ7B8jOgu5SHgcPFSio4+TLjjyh7q75PAA3jFJVsOLiwqC5RyZzMYJdzNpemVVgdt91vZ2liDOZ7SB6wNlDCPgT0ZTnKUEQjN37Qd7LekcD6sUclZ51/uxL75hpRXVxaVIflN5U0VZ5Ra+txBfV0k2AwY/8jnBgs0OVuYv4YteqmlthJ9wot8otZSMeb/0dm+Y2pFPMfgl4YfIKvPsUqAp4CYCe9Od5lLpwsR49oEb46gSI1PnKs7BnQSJ0388hprc7Jrqs8gICKjN5LGDox8jYHXvf3w8QVWqWakhsUXMKD7ZovLr6A+PzO58twZDBwIoZCZ9buvba7MY55NDoxA5elcRnuzwh024ClVdeHAlfYBXmCErTwKwgbC1JObCVH6uiLfYrbue/eRTy+wyuHZ8fQuyfgV1lVmZ1Xl5yHgnRDSHyIUygZMmk9EbDDPlGRsGOAF+iwfpHwTvMS9GRkAB2hVNVXsqubqyuVPW3evvaWlNaez0+toaW/uXpWgI0ugZ6GQ3Hb6fPblvHB28tFbb0PPrvMs3A3Jao5VAZetNzLv1ou/hp7oPcFOulGVV8sqTgcDXFfd9WJM+REw32DiHghUnAoUoDwQ7EKYgHdeFgqnnJ8n1AQKrtm8lNLs1Ujy8E9X97Jzx1d6YiPUg0/IukvitGdBJ1dCkgF8lRWczS2VPFwVdETmHuve9lby8pfgsq3gIle2bh9hTQf3LLx/MjK/2C8exgrb3j/zeejRzKe7wLkR0np85/m3ruwpwKFcJs5H8grfcUk49vfKLOaFHhek993TugkiQsyMNhj9/upOBcbDmIfXGLFS/o1mP39VoIvwy/Ry9FzCLj64j3x+jdkDeNELnm4yfgWKeedMs9w3plC6KHv5EGolsgW97iCsAf9GwOnJtusXixquPOJBlgzrDL+NCLAqWqpFrwwIL4pgPjI5Wwo0B4sH8zUwjLbvEpvi7yGmqc6ObeGoL1MgPBg/MuG9UTOGeVKoTWq3/9HSdewVtZ84RInFSoyR36+NAp6ppvE7h1FfAuJG/DWMUpBL+vt4nfyS/3zK8rOcogWS9Iany9/iH3vPiQZYG1cdiT+Xtf2MBEOOcVv0fEn71crT9TebyFcbhs6crR++d77hNtRSW+beV5Qc9Eh3kwwQTs31KV+ofaSyYKWenOhi2/R9T+kSTnUD9w80kxrXGlnUK0CrMLaNOscrQr6G0s9No0ZrRihMqaz8suFEyGZg1DFDm0FnaMrTn2kqPqRXwv3H2Cj7qGj/K19OmvJnUFqjHEpyDwmkhVjezv9yvaNvsqlyv1uGvUyPcU/5uyvs7tWbNbft8uIjIo8H2HpF2yahNYM9ONDMoaJUVEhSQwilosLw7PGpJywqaygjavDVJcKo2hcw0aRSWY3xQmX8whVLdNwBurkHyaab85/ACGyui2AtP1BRAaG3AtnCTrt2odRlAHRkZYRFZU2vTKOAoI2rjSxqCOhjGVEMlBFccRqCiHzjWrdc/o6i05bSvrfHtXYtjYndCrCQvIS2mW53uTkmtmHB5nt87lWW8Vs+tvnh0/16qp03j3dnUl/zFxlmnpgH0j0qi75KR+nH+WdbTJWhl3U6QzJ7eGoU6TdH9+NWFrMzJMVZIBRMpefRUfo5OovqbAJUEOz6J0+vGsJzdP4JkUXqZorYLWS6u7Hp6V3WUJPp76RKgfCESB/P2MQgBFzueW1HRc3KqCy6rmYl3NCZkP/XpU7cDCo64sr0SWm/Gxw5iVP9IVmVujlz+mzX0stWZmj+2dC087e4GiqqyniKy5ngEosTnCVyDE3x7OBcJNVl/Xt5umicROabx86iVBSV72qZF2c8f9DR+jzvbOs8GCRTqaxmkf+MR3zsMNnYusiy510oPD9oF+XvDnJhnGEZwSCniUpgMivuu2Fouy62d1QZOvCWKNKsw7yl0sMT4j1P+cnaYFGUUcW4hl6TAGtaUGkawYOJ80lrvRsY+wKzGyTqk3/M5pbdXJ4nXGESwgtOhtPOM0k1ZVVlpPqqy2C4Tq2RuIGZ6Cornei+iZltdBBuFhCsfstATOlOzqRDLdwTwrzdGgkCIcnhrg4JfoEALg0r59Fa6evYMWZF5Ryrd4hzhZNFZbXfN+8u69Mk4O8dRh/D3hYXt+gxfYWVhZfQS5paa6vPQHUKRoM9qGCmJYrl6FtfP5dH9ihoyjT+bGRRfxmgkGlaE1YQdtagGu3VZbHoPrW30Zo6lNXYhAv0jXR19o4Av5AAkXVx5pccJGgR8lhWMDYWBTxzWNYiIeEWSOd3FNSZnwmt4u/xpb0Dzt++gMvpH1avRqouU149q/iclD2cMZDTWnG+oO5wnEdFZmTI48xAelyHwNSHCmxi3sNjAzl3quhVjVkz5clgKPbLuIbzTmm9FxT7HCcHknVJGzE0d2rT9PyNRUwvDL2Q6b4/iPqb9LrL7j69Wya+Rn6Wseb1+uQDvEDz/+D3t1nlz+72C61d7eVfk+O/Mq937OTVRzDzEIDWNvcQM7Bkkvr2p6ifA4mwmVQofgXOsOEp8LlUKiupSqYUSVhAzE2Jk0v8ISWJJGhTe8VrHzXGzYiMR0p1xss4GB8jM4oUMGw23kNT35gwE2HiUqz7Ajn1AtCsv4cnW1+l6C8T9Hek1V3bkkI9ZqLrxxeIa03HLwTeen5/UnvZtU9Ms0CH+2FFW/niM/6DmtxWf78Az0Be2xJ0gNzTmrkF0onCjGlQbd9ra/X1PC5MnaBMnWj/ZaXtYdOXGW7FbW+5fBOWXYKPraXwD2wHzUYdSqcyta9LKm/s/aTDCzdtj88cqWncJT3gmxZTcj5nWz4Ta1SD/VN5wys+mkbe1z9L1Bb+HqyZmUoB1J9g6fr2rQvaWFe+8qNu1M4H6WC5F92gWj337/8eTB6Wfeey8sWurcxhYmYIy7btimHi80eAavaoIVx7fuwZg//EiR0AvFkeKgP+Io7/Nif/myapdpKALgxAAu3RAW7Q3WC1/D8gFjOno904eYKdP/WCMt/2mYdvXy1pk/fEXdpfSm5NJK3Fab9/t9FsqcuNvnlADYHeK4N3GsZTzBjyeVbkP5+if4p4zRF5I8Xv/KRwBgkfdyEvmqxnU/WJdHySdOwNnbsFezZY1qeY2oeh49IYbRfmcmm6OOpvc9umn/126dh2KktgcxU57bxrm6nifQrzzca8FOT7Refi0TdY6Xu3WyvKY6IFTIna4+XCTFG+UoSGzH3q1IyjmmmguEtqp1ZNq3HmyO8TwdOrn9hD2E1Xc+sUz08SV9sn9yOyEXxPzdJgKhMeHw/ziAbtvotpeCb+eTxZkKZTpPhD1bS7dGIV2UUmgdbkfEzjFRKBWOSza7DliSY70Ptd+AU2n7smuwanAuHt4A9VeaPnh5AIBKISq6Zws+6q+CGkST/H6qWN4MsVZQhwQyFhzvCs9HSZjTmCf6aOUFhI7gLbAXcwgpvvwRi8Ipdj18tx7WA8OekHc9iurpKXMxbzr11kNIoQJlwyKeofxqQmyNqiuF2PFnL4/WIFUSbTBdEZR7VMYlWIJFaJUlsFU15UnMBCshCpMCk5BZhwNRIliZCx3lDepkGHfpCVOjarKA3hzjuKR6VCLI2UDYpnCrIoRKo4iSFUKGILQ8TGpKSqPGQ/c5af4KElpRh/kCosgIgUbAIAAA==) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAALsAA4AAAAABWAAAAKbAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXsLEAABNgIkAxwEIAWCdAcgG0AEAB6HcYyyEjO2Dy0eKLv4XvfsrGs+wIhEBOHOERRRTI2158fc/aln0WYmSJq8uTRSIgUyIVMqpfa/7uYHCqzWDuHREj0f5UuuL+ZAokTaYgiIs5sF5aUutjO7QhBlgMaYvCAIIqqoCggoq0+HjRlX70MGclDLyR3Z8fb0q/ectzCv30obmLesvO5hBhRhcp7kToaLpaRXpL0htKmb5C3rIgzUIwA1fnqrhHSbqXhA3v+sK1wRtcWuhdyg9E5tGXERkaAhroCGeNqCnJxAm6m1Sb58SICvFhXFWnVAAWQoYRjYADJUQQqIYm0uSZKkfpYv1sv21dm9b7kWbV6i3BQ2Z/sOf/hl+ezXH88LRz75pnLuq4/MO/Zx+eyHc3x9VDn3yfx9n1ILyusq3ps75y90fVZ657PJ2iXgF+odHbvzv7Lrm+uTsPR0WJqYcelN7180rHDDnbeWbrx0QHht49uXjCzffOsd5RsvGvHe4yF5o+Ej97/ZMP62+Z+3Wz/08CtZ/FezhpdvG/nb6PMhC9vNvHFx3Du9X47etewROuONg4L0v2eI+L9X7dt0evq+gNihfvWttiuWK4f8VmxWBM/+WK8b8F6Y9evfLf57r9SjuA2URBAobPm/Smni3y3+n1TqgQEACsl5awAI/5AetjNp65A+/38vDAUXaayPL4CMKHYkEFC0DlfIlbAMegyqlmGU2eSTO58TTHX2xLyWvlczc/wY7eDo5WxlYenKyMvNg9Go5MAatqis2Jty2oytLaPupFxOlsgFObsjM05dBxMHVwcMbeFma4xFh8jZxUr2e62Th09I7Bd96I2RI3gzYzqKcsHjqZzGjsamlojTwdmCy9bKFNm7IBcudRU5BU09BQ5eTm5coMaMAw==) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAA2oAA4AAAAAHqAAAA1TAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGjQbhlocNgZgAIEAEQwKpzCiKguCFgABNgIkA4QoBCAFgnQHIBsPGqOiVnFWWRD8RUImd2GxGAljk2gcqPUJjX6sRnWJIw3uCR6ILv03uzO7gQrfXeBCSq30KiEFfa2TEv5Mbw7wtEszkukgZUI6op2o/++etP84lubf8X9FzbJCVahWuCRlnD6ISTaXVKgpMU2KIFDiUma3cM5CAO9TYmtx0+R5cq20u5dkNv+cR87kv6onZPvCFF2VuMve8aZED8QKiF2Fq6okYMcadRWgdLWuFVrja5ge0Jp+eZyjhlmj1Dj6/FaEwCAIAIiChEl6BEDIiCgIcdQhEBhAABCAAATgRxQaMFSs7OYHSm0HE6mg1LEPngJK3Vpnp4MSSNf2RDrwgBBEegAQgAEYpMUI0BoBCFKRQKDI6pIgIa0gCov/+IGCT1qA6lfABv0x1N1O17/1r1GluCv6q17tAeI7Oj6jQYbBQ79pLm8ttupnyKl18VD9gdtyVL/0H+V9vVrv15/0StKCEEg8uuhjiDGmmGOJNbbY4wgZhMz6Cwa+xKEOkMvpM5CHYBhprq9DOMnoQhBrcogNeVVtqWIS5U10RjuioKoP4IvNd5i/7BJL4OYmMKEbYOaFDyZGoC/2OyDICAUSApCchNKV5IPMwfkO85cHBGBZDUxFmIHrUjERmrVs/cKQEpACckBumhzQPxetj27KCaIVBWqx0gdEaNjYvE4HAzAmKaxbwJ17lFDbkww2wgjbYoEXOtiLDQgDWQEgi6tVwpABTeTkTG8rB8JAt9ufER5QLGGKNEJVJIlVYtX13fXT9W/YFq1BGCJEqIhEsVKsuFa6frh+xc9JxwLa9J72DvB2fj7reannM54+yd7KIikOgX5KPllaE0zyFIy4cKAUYNwF2QBQPQDTAQDKLE3YYfYUw8ID0ZOAhRo/dr1wkebt8zGRjuUoNGOLCbZWTAeXBdla1qLxQ+/rW9IMTMKvlWQJBkIZgjL86fO/PdTzpEf8xB+r+duvefnrH4yiETPKkEGeJxsYe37P/vFSk7t6Qni4EPrdJftzKewFwtWCacRnOedfdRMNmxAKNTsn6Na43kdvRIwa3sfoex3ZZ3JPALnMPgp2pSAkVbFKbIeyQHwmbNpwVwiqjh7/ceslqcxrF6rXojf+leic8KIihlLCGavY91EOU86D3May+x/+2j/+38b6ii9C2Bh5VLNppQKHqegUdR01i7DQRIsPDLrnPKtp/rSPhT4MdtlwqxInVbaj6gANEgS6jm/c0h69hiqF8HYzKblTWlWVadWIMlVnPjrEOoNgs6zF9O5yV+0mOkODdf1rRElraARrybSCtdlnmXA1YhT7b/lD/h+hXTls/Zq+xnfW16W4zAshCUiV8nTXsswQDadaM1XchmKDvU2MP7cushlqHGCTlzHUULp8J/fIdXPT0aQdLDzMcNZ+bG+cR/hNG3hryBYiabqUjJJsvkqsPFj5WPCFUGd/94Ph4UIJe34vN7jyMmaQu9TMz3HmRZ9CeU6ZeAtgtNOMqTTgg3/ey1UmkjgJCTcpeX1Ym9qiMxGnPRvlbntO78ry9e+NlDbGBsrHy5aB8swZvnJrIHnHUJ5j1Jk9d31GaXvGs8g6O9tEnOt8Y1Y5v81bV9hmZ9jcPiLQq+kP7ruY3vjW9f8bruSUM0GkVKqtW73PZdTDYNmv2QTy/NmRB8u3LY9NLC4N36HdraEPHoS2nSV9LDQod5dioxZ0ev+nwLn2wQqh+JQ47Vt3FG1j9OyeqXOQ8n5Pw9YUIiuWFptA9+7TfbTxgJ0rKebEj3nRjUN+JTVeEhyR8GRWg7ON+0ZDRPS/H3MfPZI+2iAZi80+lB41xw99KvDPAWv3ggsTPF7LPtVbuFjbc4ka6R6lC/sRsWpI6qPpo6+8z2C6PzZHdh2d0maiZ/5yvQJrLqbte6HXgnHe2a4g5qSJ/dAw2Sz5rCtX924lIUWpKRASs2LYnyeTZ9wLyecNXD7ov2dTZ98NyZea7LO5/lbStKm7Z3dtvJs0eeYW+Ud17Vp6aduek5w6lnzw+7lblZbxJxf38DmI+2SOM9kKPm8X+CiiYsD8dC07ucq2i+ueOSr3BdKd4Zm/4jyqnbp+6PrTiKAW3xQjywKf3uTevaYVGjdXs2GKWQq1x1g23wLrzFxLzrf7AmX9tmz9uHhxpNViDHXG3SrZagv8PmySrmQ4bF7m0dNZRHuXPST12ZQZFyZOxuwybUd1y1/JX2XynNDyoX+eTpp5P0jv/wPPurNpU6dvJ4fs3Xhr6pQjN/z9uNbHr9WkjpHLnmvH/Ss589O8kaGK+f+/lTq/Zu5pbx9BHT1o8v68RGPtRYUIR0I30Gn3xa9v3lznXB/Ht+BeaI6/O3htO8fUnPwFWHUPZ8zDnQz6rx91G0ILi9/dqtRWR/zyfEOtroMawiP7uk3DQ3MUrZALlVP3WVhNVnLWaqZU3eo8ry++oWXN2m5sVObELzsPprNravGCYrTUqntD1sRa/2Ldvca1SlZN8LAq1PT+4p6n2yMa/W5huHVs4/K54eP5w2En54wmCra7enrTMm8XR8NVb68GjSfEiXvprzafSoaz38TNeOhwEZVlzU3hFaYxhI6iBVY1r1pum11oWwbf+SaNn2NPvCrtTrQ16l5ZxZnorJG2jLu1jdrQSkqhJR01PUz3/UVrjnVAY50nYmXWWOookdhuWLVU1UquFoXPhVBUFS2XyVlipeU9s8O9vF6d4hWsQHJFb3evzJlQM8Z3dxtVLVMl4SQLJ/m6uBMxswHVNCJ+xNRLX92d7Kgz6lcp8uCcWHxswbGRS/bLb1huyMnEK+Mtill3UqgsSv3z9clfafiZ+M+7tLfFw+epGDEwADbZ+CqKsIiD9CEAU7RDlxQYEiQRkCBLMAeFmcwrWWtaSOdkFUT7868oLPiQJAFg8HUpEuQYKl1G5pTvBcacsoMQGs4RoVVmEd7pX2QRnBCWgRHdbBbJSSEeGNn9DYvihGDyj+p2fftiEeOUMNK7jRjEeqhm0bwWmiyaFv1P9zBaMCwthvcjZ4d0MNpjSXGUY1GwFmtXSwq1WNuajoKxv+QgfoKL7dooYU65R/gwp6wihDpoFViZhaOZdCycZmEWGN7kXxZBu3AOjGhhs0g6hHJgZOIbFkW74POPanGd2zC9U9g1ogJsCRoBU5LTjGtHCLJpLnBJol1mCqyCG4g7bJA5WIkAkAfLISswp+IRTswpmwih4TwTOpkW4W06gZjJK2ENeXQdEDN5LSQhj64jZDamQhYOug6IefobYaJXBdgJDAGh6HTintAVwmxXXLKov6i1qD93mFNxiHLMKTsJoQ6eCMMyC0dX6ahLsQJXRAb034KFyHtAvMBbsJQhrwQmeIHQCBEi2slVYSdEIS1WlyzqLyot6s8t5lSoqMecsl2nUge3BVZm4ej8zVGXYtX/cAI1iBXsCL6ENAndlphT7hIYc0oXeITj+wB8QY5wCU5OO6OlxZhBfiU/Vuh2ADBSL/AxXjQHoJw2F91187W6qfeDMcTOrZeB0Up9IEl/kvO2HLX6k3lXvSUY5EHbCCFvddNjAQ7vaiWpVunuXW2+lh55IX2DReV1R8LlQas56YC+IEN14LV/sLVX3M6jTZVxt408LEC7+lBJ7j42HjabECTxIC/k2qW6ySbvVokpD4no/UXWwoDtM1j3sMbB3G7qk88b+0IVuWo162+YdFGnpIHJPiPtv7Kls7WXPOw32rqy7nZ5PQv2g/jn4EtAPLEqWePdIkqVh/HyeCJRnWLAGsUaSs3TpYH04LGO7UNYd7Oovpb2sSK61UyCzPe4PiXq0sCnFF9rL4pHebSpMu520WALaO87ZOv2jY5oC1GhJFZvsXc1toyxd1GQXCVps5xXoTQpx7wrzd4rSF9rUTHEkrTtVkRxq0/wuIfVC2phdQ97F2OLhL2r0+VMgnGfcketktGrTI80e28RXVARyj1W6i1u72W5aAECMCLTflw7uEUkd8nfPll8AODUtzS5AbgtfH79N/bntq+ODwXAFwMAAXY3bwD4VhVhbzU+Nl+UTjEbaQdY/P9LUkWRkI1sMjTZpcoZoPLSKM8TbC5FGoMxlSGkybG4ZSnCxXemyVaay87UmqfIaFQyVJ7FLf5jiSoFl7NprmaSJL8wyTzKJjOZCvM4Q4E/LYE/Rc1uZpiTjDY/0MP8qVvKIDqbv+hsrmC0Ocxoc5KxKhxmbby8AebR+8VvvYyX5vo4WWRtCIdq0PHA+8LbbiNi/W1MOkXGe8p7Y6TCCfGJ8f3l/WsNpYSx6VMytbftRXOfrKBa0T6w9rVl2NkYbhBgCjPYUPxgvFYIAgMjCiYE4EMHUIT0BVoCjgoCaEkNgujS1Yx3lUAVMeRTCwfDlxpEA+hUIINMCiBIIoFEspFBDx10vWgZyGQYkKSCJ3QmnVi07LYROXWVT7KTwtrxsACHINc1jEMLHzKIcXI2F1VMIIdUooVyQDQBhSRnemlZq0wfY8yVdDfO04PmwIsbh4JMzND2QJ5dS2DPHO2xIn0cLTIgSNiSSlIsCSdd55lQ0MYNZ+xxxANfHNHUkaUDyoLpLsShAA==) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAChwAA4AAAAATeAAACgaAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoFOG5JCHDYGYACCWBEMCvI82x4Lg1oAATYCJAOHMAQgBYMAByAbcT9FB2LYOAAQlrxDFMHGgYhg7wv+LxPMMdTZwdcAokVZdtu6RLW2UUDAMvAbzZ4j0u2S99aGde5X9nYZLo8RBVE8cz/ziI9IIx2hsU9yf6C5/bvdgpElUiKlIGkMA6ENkDRIGSmVI0aPDP0gFj1qoiBp0GVi0dYXJuYUHnju5981VVmCjIc7w3k0B1KTz2Y/Cgf0o2mPp/+Wsb87U/V613FQAqHQIQuFClkirPwW+afv362q6gMtVf/DsOf2cg0vvM3O4NPdzA4j3mvSUAnMZjCdnkUeRGKpRucwnAmqcD3gCWVZxcs/tQMPwPr2Toq7D0ZhBA+fWm5pLolxQRiTsrNzhdLu/v/ZTNsd76xPmzX9ECsMPVdARctFOfu1b6TZ0Qr2zs9a7YHAJCkso86kM+kMVIWLhlmS7ehCzFWK3kWXdCna1C1wmaJt0sbWSrOImtKwHO4R5x9/Su4Fx+oN7ec3pBJ8N1JXHSbD5btBxdL64RmbEBAY3Hq/9fdh7HIECcLYaYizzkJYsIKwYQtlxx7CBRnCjRvEFd4QAYIhwoRDRIqGiBMHkSgFIlMWRJ48iAIFEFddhfhPKUSZMoibbkJUqoaga4RgeAPx3nuIFasQ6z5CIDAAOAEIw0DYuAAAoZeanZz9sN0XZ6xB/jMlyAfkvwe5eYP8n8shfiAPWX0N8gNeCG6CIFtiqJtf9GvxXgISaYUFoBbxXMhQubGvc726uLHg5rjExJR0Tx3ZrOKw5Wn/QhIIl5GeLXqGlHXOU+EEm1DHutZHMAYTy4QF+DDhMBH8epbUgFiWLMcX9MywrBWln49cqDPvQ4V3wayqvCnfluUTUl0J7HbL755hb8JZNZvW55+vesv6HJ231QTzFndzWbOdc8i2zl2YaW7Qf5NqnzZydd7kCi/4mZFannpkiTG74hVPfJrDMXEFG0XiGV61ZftA1KS6oDHeeAP3jKIKTrQnWVM/au+s0gpuLGx6JGRpNknnE/R87HG7/X3q08E1N5tZM1rsYm4z4/l9NPux8A3c1CCHpdjQ7GTZ6Lb13GlycjkCAkpX5OMRbE4ySW9DY+dXaipDaJs3ojPG4jQ/aul0PNNO51SvCq6551maBRVcYsmllFGX/glWV19TjO7W3L3u11JrD3rUY4OGjJkwacq0GbPmvPDaG8tWrCEgeZ6Fl3mRjOJz+b4qtOU62xDRPocXYTmKlaIsl2epAu8rtRw7L/FFcIsiuSjuRVssxZY8dyswUqnarhsKj2STBSYvm/IxFWK6bhORl6dRzBZloWj9pVgrLy4FcbpuoTJbEKXehkPylYVNXj6Wb9t1n8Lw8kmoR3TWRE4W8wgJf3vfKTaK9qJs3V3zptL4Qpy1mTyS2OS5Z8GxKIkvxOTlXpzcKkQXpWTHE/MpxWrZvMuXX6GGromqNB7X5SGirfclgrSaKMJaUd6UZ7oCYbzulpx2Vfj0rZF6IkS4yRViSjiVE/o2lcf6/ifqxImwExxRu+P52JE0d9ZMFobyQsa5E8tBMibGQEbJ/86R+2jx8unUVlZtz6lB4/101XTo1O3hfeW83xYwNOkYEHAcMEwBdQr4nQYiJyBwAS5k4OEK7NyBnSewCwIuwcAjBRAZwCcTuGQBjyrgVw1E9cCtAXg1AocmILoLXJqBx33AaAG8VsB4AHgdgNMp2cYr2CoT4PIYeAwCYghQY4CaAIJJEDYFRNMgbAaIZkHYHBC9AE6vQcgb4PMesJZB0AoIWZPsJRtbDaN3CDgTY2BxI3zm40jcJ2+Agh52HAmVLY5u0AJ1mAYevFW9Hk5cWVXWGnpmBBLiEKpMwhTCt8CtbQ8RAdLHwZ9a7CAeIc2s4OtgYDG2Pjpxwqk1ijOjkDHF0R8pTV6VVGVVWSnLGhvATnDnaPTa7RscwG2qCZBqXEJvuR+HcK9aeg4AjD+aG4NunCsw8A/AfZUcIA05AgBsu4wM0lAHMzYpiIoxYEMGQpb77cLCRF3iH0poycnN1KYpHZnI07zLdhEcbwX2DsAuQk5AIpOa/NwKPc3pzGSe5X2+F4Pj2zvgzzPwZwYA/BkCfx6DP8vgzzvwJwsAQhaAHAAtegAuAXABQANQDIAO4AiSZRUqmVQTrBfltWpcdOk3unyJA0dOv7a+s8u15o7o6rhy487DmvX64r/wssZM/16UaG+9qzZPLQZVrDjxEiRK8sqiZDQpunXVnvIneqRKo5Ofeia9dv1wN3yQ7bmPbrgJgcEGEwR4AAB8AgDIC4AFwF0EQp8Azk0kx9snDfPj2QmX1DwUzSr3I4rZnsxV4KazY0KQuDQbrywA7HwxcI2zw1xZJWHD5VmoyqDaKJyscpqjkz68f7LUJy6TZMjXsyGBTFpTFyxonNXoVAXBK+0RqSefAlovCIp7zRt82uqT0UeNC68eabzREGvrdZ4TXocmmhWkYD1RsgYezAYhPBKxSIn4L5uSmEH33PYFeM6NZWmoZWzp0TlTuLIqS+esrdvL7Nr7to4j9KKuj2+9hmHQ2OKiv3OXFts0bnPXvEqCGte/dZxZlK2+x2IMVoKF7B+O5qvBIc79qe2ZIEetij/Rwrm+btakPVN9/M1ilf/npsR0YlRrBCW4YSK+CmBFQujrC3m+S8Ju4LHpH4nkYnJysgUVZxSJlOEfwx0uD7/GUZVIIPF5RdEjGmu8ReZm/0Af7uv5obkxNwuXvMKEb9rW1YbViRmrKxkPVLHPjRCrUuB8wyfx31SJC6Nswq2GEtXJdqucBTyVVflWFI9zuqybkrG4M4ci584piF0xKvC7dDZutTg/3uCJCYrLhUseQJkfkHC2z5f4odJxAoxLNLxC90Y6jrVmk8BeFvnl7t3h02X1SWGkYoNSa9v6o4H4GMjKTE/0XLrT4JTxJ63l9bQdeBsVy3Qi6aWJAGq/sGaSew6pnQIp0OzUgzA0ZmkKQKmtrRNiMBEVtmfeMNGBreSPDRm+vvA2zXhCBe2aS5P7KP6IJJSe6LBqz5Ei56TaOnWHeMhXMl445QWnFZOTK803ANrivZFmoBgL63JZ9voy6IknS+56R+f1DWvsvzpzWB19DIVc8mhfy6E5YI9dnpv9XEuRKw5QatQBLigNO8rTPRAhL1ec03hBwiMZFPTqL6H1E8/2X26SPWgBVUSts8n7TTMBJnmS17rjY3dML++JaWooj3xhV5mDb/e6xR3zRy5FfTvPH36NYQnfQbWiBzQOhBQ5NNFlU3ZY8czbQpnpgWi8Bxd3AwmPyNunMbt7pGj8G3WPuemhnnQlaZ/XfHpFTPbEoXsrmVvI0fu0cbgtWw41hmEIFPMty575POf9RhrpscIm4jKmFha8ldjdERqNKyPqlpb5Yx5lYIPBpkfcNt06HruzrseKVty0SzgorGALbNwvz73l6DSgh9lhy2KT0YjMaVMpauc79mWKtENlDTy3TB2zK78JVdAuz2w0NxmcWeZ0qlUa9vL2OCOdWSGZlmkf3HPSIYY7a0S3/otI0hwP2NMc3nI11Yw9k91we3kEECrWpHCdgDlKgVPNtLWLhKGF7ZcohA1gH5q3RQuqQ9w7NZqlbv+7Q/1JSsRXVky4J1YD2CPfs4lhm3aRb+QksBZc9Vpr2pq+7e74y7VGwdNegL6iDqZspLMjt1Jnr8RJxqWejmg8fkGF2cv10t+bZuJfdfXPvbXIcnSO+jdgneHNNkGGrihbmX3tuFWAEnFZT8yqnElEyFDQS3jJ53msXUKaLu4COb31KjLUCrih9oZ+oCV2U1jMFR+7uoOwQr9Bt92PkKHU0+XtBzRHBaRjrQ8Ozo1y3CQFhrEGQiXh6c+Yk3OS0PGjp1kWoJsDDYDyY76UIooOLWxMbUjT5MpGtDmhdDPZeE/yZN6kAJsENoaioZ5z9T6yMnd4KpCjOCpsYhmKimZZ+fN/YMfwcGHb1NT++2n6XSxcXVa/7cv+z7yc67dNKC1uT3ly6Y4N2FzcuokbcsdWvL64c91urT0+S6b5Y9NoJtq1FUS2QwazKM5dkkAXKnwc2dalH0j3pZVp7m0ibj1VOxm7aGk9cUJ1swGfbRL3K1/xsqijM9l37rdPcj1YUsMhGj22xTLFtjLevfZzfUhAaH1sl06a5+KxUWpZ5NA6lwq5AYkMHJNyzWTEcMzt9QSBF4I/CnlM8mQnAD0w0wsUUvbYpS5zi9z53h46FDv09lxT+YJVojc2chBiJIEjP9H1EnHf9yVWXllTdsCXgLOYk7njJJRI7JaqdR+PaAxBj4Ixj3iVnFNCGAC5ZsgD8e2siOrkW3FY9TOPfWXUmyzb8TLyQhRynZg28M31dCzs9s3yYP161d7Nj6uDvmW1UuX/42VRsAIlj+oMsGJZnUf7cGq0+lWhln14YqScT09o6NNdhLFMLPs6Rt/oMIJoYsJ+05ZQ0851tewu+ahpupMSENXDo1YamhshBb24benKkLp/2j7Bhwb5F8LHMN5mGnOeJedx7kuL1Sk58BTb1HRQH8Xjjccj/qw26c1yh6jVaDNjR3aTh/qjFmumg2K/pX94qWuvDJo1ip02Q2eQ02g6RRnbLeCtwrRLt2ZpjZJWHntwl3JkNfTJtiRwpF2S2XLbrM26mbBffNrpp+pyqeXm21xNN9Lt9yvk83Yn4ZYadaZZaBh5yyzmagub0aLuwO0yDo5dK/mrhwGp878QcWE8cXe0tM5dntMa6UQkrkSHFYGqUlwYKhXuHOL24SIK3ADReAvoQTmilsrUuhnkg3XH9oLaiObS8RGrr9mvNYY7Ww4Zegzpa24s529xTe+Qx1uq9GD2CEH4GR3bxE15VZk5T4U1CO8QjVBO8RXNKNgUNy6YLDxnJxCQCAWZYem0Lu+Z7QMtFGGZPvsoB8V9FtqJWcSe87O7a6ap2WYfFcU+wDH6UDd7wBH4EgzD/ucIX7qNIg6piAMKN4wTzh65pEwDw+6X0AhennNwVN1KK9SSIOvGWJINZbCRJatm7MDs7guh9X3YX41sFTkHMEOpE3lHeGvvbe7FiXxh8V3PT8+uZHxF1uM/1fwoLypKFiiF40Hpto87R9oAx7g7dj/fFizigJWSkfIXcIy/jhmOLLjJAhyDBbv7GeIG9uJa9sanxm9F48WXXVrE5y6Lxr1N+X8ZsHjfvFCgx19/765gffEJmLKcLzbkr3flpxfpwhwLu9WK1FS0AfLB+msHrqrm/s53p7HLA8t/lnvGEkGx4I46l9yD6SeLCoeFjgjJ9yy2TcuB31+zu6KSiddE/4lKFlwTA/Qfh2FwRE35eHtaA7T9X2Rs7eDqbOVlqcu8GFoycj7m4buHmPr1fEVbPkyjCdXw91hiSoqDrZG9JRxusAv3Qs+uoK6hjcNuoUvEvajYD4Li8pOtt7jWFdQ+LNw+LJYODQoMaj2Yyf1eU+2t9wpXZgIeXnH4+yS2PvygvrVZSW0LLTJImtCLLwqL7YALAmuSsluSd6L/vcvKWPwqhnHpZU++Xhpe7UlLiNZ1fnaFXf+ma2QGb/QkP4ESGA3CvX1haa2XsOm9zI4AZ3vHfON4HBPwwAQz+Zsx/5ZSC1/yirGvs92K/LOcVrzCr/Zvi606ret76qP2isxHlPCMLoD5cTL3KUEbOc6ngQuB3DZypoKc8N3u5SIqvvzahfez9mbXjL29nriZrL1InzYecPO2Gnr6Yfr6rvr6YXr6Q2rCf1dBq5Kz6UYThAZAArfV9wdWslrajLf9NN6rcv0SAsNXLdQ9KOIpYOYs+Dfjlu6ZeSsaY7Dp+o3PdRuPjO0c3S/YBV3Q2+TPZ7X1v/FLSqANInOfMR/THrClXy2jpV058sSk0vDQ1ImDcW2kFNLIdJ8HEu5odNLeTKN5jUxN46H2SQb6UCCBSWKCNNZ8WWDfd6mSyN/PM5Nh/gt8TqWzp2TfCrdNlz+rZVZmeGxajyhwyzY8iz+4Rcw/gAIHWlapTaXyTaXUVr1TJkmmJnogn7zz5aHSn6OysajSDlKFy1PKRLwMsfcb8TfohyzfWmYBjnEdtHr0E4Rzuqs3//7GbAurbYuGsUL/FxY5gH7bYf2D69lPYkV8WMBF+vjvj4gg7yhzSkSQ4w84qdt7Ui9L2e5xjjAp/lEx8+jf/bytoxSzi46BZ04cdTrlNdgwPY0pOBFt6+4Sf0FvqxRtH50n3AVtOVJivnjVeAX2nb/Al4j3AlhJbU6xCeYUuptdA4ifmeuOEjoJYL4VUh7CCqG7BuvstiK01GjYOZU5s5yLLzip363aLUAkwcG+PS4FwbG+eUF2rPDE9g33rN+Cz/vI4ZXeByhKcfTYvn2rv0t++kZ3R7EcS+MiaHdi3KKy/dLrhu5wwkkcQ6/zXArfuH4EueHcPOONYy0/FNPgJrjIdibf0B0JsiU4eqktEKd2DcHN1j0/xaTut6lcIt9964FDBoOP+eyz04yUkpMTBLOVUp6nY7cVGTiOFVibYE1Bekzo1cZypWoQnU1UvvXZN2o4eUzwxxdEpdmf059flOKy04P9MmKjEPB4JlBWnFxwnb6EW8CMYQhPGUu3Mgsz+MpYIp/lCFv3eKrzD8FY1GT2YY5qxs99WKE10JoNWwjbIg2BvsW9+HvMe3E/m5XdNazwSt9qgmqZtcHbNUqWqKe2Kuig/Ca2EWZ72nU7ijYZo9GjloHXvLb0Qi9cuuhpqW9uZ+jc2HT/DpKk52Bqec7X7OhWzv+t7cNvykEDS9oibc1UT3/91QRWXVQ9k8RkeCs37afhqjWPwkkDEokZpiEQwc9D/8Q4DcOC5uwm9cRlgXH4pyyI8qiRmGNKo5XKk1NMkgbwMVsqW5gkZm9lLxOOoRQnCpNi96QB3jK9HIQ8X2/MDZ5hngnzvOzjQhbmZEL8uy/J/XbulX7VH4d7YYnE3OXw+aL7hQpXRxsAaYEMm1BP8xXX4MZhj6BX7CossdKIPy9T8qIG3X3bQ1ccQsNs3WOucaRa11hxJcZkg48QA1n4+XlmxacioGJjcuvLPPIXG+oe7+gVGBeOItgQnwTyZV8qBQXHOVIzPH7+snvQKcsta7Rt7lVvE7MpyMrbyMrNO6jpW1OQnbf5qUuj7yMoa5FkD/3oxSyPNzYszzxCv5Aa6xo1mZqyMhXUz3aurhdtXDxtERDTN29h7y6SYCupcz7Nb9NfsY9u9H5A3lZv3jnfGUtofT/2Zz3hVr4mZvh+pqv54kUElAksov9mnnx7h7Ys451CQ+xeiolF10UR06Kz/C6Ge+DMlzFu4U3D5JBZzF+BlzcGmCQmHFanU+nv6MHZtXhpN8a2NI6Bl/Kwqv4BS8IOIr0idh7CP8QLSWvi90k/ynt/knGiZFEyVLt78t8zzZXIqv0NvKcH5a/S99a1qKn8HhOrmp+Q0/vvR2gJca8yZ/QR7hBhkpifQndfAONyxb/o12fYp8EsHyQu1C/H85IFy56aE+KLiQlg+WDe/nrBE5myHBi6XjMNCc3IeN/0KKfgi29CL/t5u2eQgXvMu0B1CAxEDmBub1WoUJx8MVEdSZ6FMsrQ73yb5HrZndrlS1aLSFqJSqkzYGL1gsXmBQVgovylE4+s185AEQMKtMimNUwS83mlwLNvQi/7eLtnkf57W/UdfRCi+huk5CrjmOQVuWtQ6DP7REtA9B3ffRy2//rZ1ta1KRiy91Vdi2uJCrdbESqNkV6OnAiE1Gg3pnraYBovUf9mfskku5DwVUER4gQE/z0aZOQl0S7y6kdFlrlzmO2eZyfri7cbpw7GoC7eObrncuMPFLUg/jE1tFug7RNmfqKQkFdb9J4d5c8rmeIQFioWFGYfB4sgRrFqBl/tNR3MmMN8kb5A4+r5svtyq+V/wrMuwot7n9mxB282LxMXu4jPHmyAmfztaNZSauELflH2DWf6Pl5NK1oSUEG++3gn5fGkIjwpiflXXl1JKuSJB574pEJwThcPFPdb+q5VV1oc+RhZELVC5KOEk3y+Se1lcMF7XwFnAWdK90WZSX034Uct0rKVw7zlkrPCy6Q/VO+FPGfIuix1gLomyxuEkbCR46OMH13gQNCGLCdFgYWbiP8WLus8cDlCNunb5JnBRFaknCpOjy52exLM5F+82tsl6dfm+1DylcIi38vX8g8lvNt8Oi7vj72L5hcsdl+8fzXh4l1zSec2ZzPp83eLEm0azKQ928DckDGx+QteCS9+/T21FFgWWLY08f82Oie9uMWaHHNyy4oTiHPLclL3a0nYToGggFhP6bv0PU3GKk324alfgp6evDTZVx/3GnIPmfmJLUToWuzzrPVQdwpvBP0K446XyzD6c2x2taXfOdclt6d55g3ah46/XO3sNb0UEr0dbRmif87BH7xGPo2A1yBtoWeVyFbu1LRrlSZnlSb7+HSbkKcnb0pdJ9J31l98MnIeWanvqqMBa5E2QLkU2xJrsCoOqrGiDqORZoUfpebJkD/uM1I7Rr/4mjJFoKQcJNk2WPJ7Mmtedwm0Nj/faXAT5sKYV5qlZmRfSZRG/HmRmh/d7+7XEbZiF0y5EBjfVbPrdkyHP3INLj2WrjOOla29f7zpbZY03ShWjj7sIUM3iZeltxnWLxXK0U9TpWpBtUiaygD4LAveDHgFosJCX17JpvJ6Xjm4OywdlGgKESASBoo2r5K6oYjkb6EP0kXCFvokfyjqTgLVb0zrII+HwR7WAaryaqpyaouC1sEeDk4h7jaB6vqq++XUjL/bhLg7OGVkByV7eVUt/MUSJ1RVZDnGroqYpPZpi5NVZS9YZotbXpei0gqadBools6GzmjFnW6KxWClThJfRs9EuVw0MmHorFocedIodeKavr7coNpsEG9eMwYGeweVl5ACQ12DfuWD6G6kwOCkUa8yKGvjZDG+wwMcrl5WM7NZln9PwD6dK7Gbn3ygVb5J/p1+EhJGofmQU4oiDtJ/6t0/FZaTGYMcYqmZFwXF+pJBH8P/zbfYi+Ln4hF+QTug+UoIwgTci7dE3yvxbQNv5fGbuDtx3RFFupFvT8YUG/F6RfqSL7jLnA8FH+LtGlkdDUFOohIT2hNTmnuQSGu2Lgo/fJzksPkVU0QKt+js8ISeGSRh3bBoOhdfUpxtNsAkDTGnO0isEJ/lOLHf5+RG+cZFX0b1iXW/+K/83yFxNzA1IOkgNoe0n9YdaC5tPl+/RdpinB8sHVSYaAIdl4CGANan533zrhn15IPMNsnvaqCF1EfVb4UV96UyfJSaVFLw1Ro6ICZgmeHo0ev9ORabHgLCKnvP9TmEhRYXABb6J2N6U8oLZy3HM92BKKB7pzCGsA/7+rL9Q3rW659MfYiCZ7ZHQkVxSewIM6wqjEnKBIcAoTfNRgVGDzr3NdRoYx4ON0Xvfnsrc8495m1329MX+GZ12rsRg9Gvn7TaerZ08QPyHcN2AlcCRZNc51yMb2cT5xud6BesHRpvw5lc/o58bcrh3JV9J7F6ky846CPMUwVRplX/jcaczC58H9nZslFY3PVvPHw2ruAM74XNbHq4t4tLbZT3UZq6Bin8CojOfXLue9h3WTZ+lbXMEFBeczoAfPfCt3t7e1+2VEUwIwoEMIsnVUFknjGHXDU7bOSL3Vcu500ki1YP1fN91EnEn/ixfGUb92sDXo/DNtPLgAubXp7Rwt89CYxzW+egLl6So5yvsoGTCUl5Gx6/qdiMJ64iy5N/J0NYUvzjWwXHHouo2ljtO1oiUjVLb2nNVGos2EW4WQZsMmTjJE/tkZGF7rt1hmp9egpPVaTu+fhItf33qDC76RU8FZgT+y0wJRMvkfy4oLbI44BkH36rMzbcqMadljj6+ZX8oqiw1wglAwoD2AI78obYB96101gMXZfcUfzFxbP/Gzwh+iMUCxwbjDk3Kna+b3B2aK9NCdplXf/GCBkOy0xKZ2tcaI/TRrdJBcRCGTGxMX8Bt/6gu7/WkME1oHM8quNarBcUORARJLHR24uC5vbHVYa53A99dKIfry2pnw1QEOrT9Qk+5f3k5jEJRg3I6TmZpk1h37z+f6y6WFNDrb++0pS/CFvc/Zyva1qqvf0hHPi27DeWB3cojEGR5xs9/eJrHzLeucc8TGQ50WI9KTlU18JrSXmZ9XBAP8ytLxNKwrtGRBfWH/UIbXxMW/KIfBjPdE5N8oksiPUq/i+hIKcODpNLhYbi512+7HNw7GzqmOCfDxjNKbxSdF5qaEh6bgQGgj7tZs1OCP76gNESYq2edkC807DRiKn0M4nT25IOe0cRA3R2688oxmwYrxyTkxYSmpVHAXDgYl/S7i13Dddj3kXMznrqByPxrWgN2n1i7pPwBdVWTAJSHf3zXVImoNatV5pH299g2Rcbzhl5JAZTH4/foNSGZRkE4vRh5fJ4dT4k+oROc9mNu/4C3MzY6j/y9nEscpZNx0TTFQlsQe9U/p/Rtthl5WHEHamh/HielF6F3q0i1B73i4rxADXej8h5s4uIUzaGihbp1nzanywSy4aOrm92lWFuBhASTGLvrCJdPW1oYvHoDq5HcARZqjzYZNp2AFcHxXbQM5ELcUH+H4WEMT2qXzCYl8NvltzeG2GItPF6MvnpxVMJZw4fCiOYlDMwjKTAmKQQaC6B5ncz2aeuWJKl0MfSS+Fkrwv5N+rNGDpIj1xnvZvHc2ujhDP2h2JwZlUNkGBd1Qu6IUs3RaS4iM7729JKkVMjQRQ2j9fcu3a9zjawPE0+4Ue9h1ahHbpPv+9yUxxA3JAq6u83iZm9/Y+7QT04hMjvxitczazHWCHx0Rvwbh4szpENL7jfRK+h908MfhIyP8DARCEl/isDUTE9A93QBucqGQa2Z5yO+yMxzWhlTXyWmkd9f0fL7kB7HrH17FCX9IvGiqHGgPrtDkYHk8TsZnQzZxELCzcjB4RciclFG0+MfxSzV36IODf0JaaGEvgToUOwXrC0RASp52n6T0K4rOFNyoXjD5L175T1rXZBa+/6jWgkIQkTjCnUGt2WZ/Cfh/NIetzYhi9cbDyHGOghRuH87h8lMhAL9OZ0U8vabrWfklejfr1Lz+90OqnS5XIkPSi9q0K6pOAhSGot9YzHjfdQrPtl/h+4Tm6LQ8FY0Fmb5wVEC8INezN6rXitLciGDohLIiYYzT9R9nFflGgMHh39utkT1okPBPWqW2vMf7SGOEdWQmY3xvMWl+56318u21C1+EqXftUXxKu/PNPbw/9evBMSnVsbRH6u2Tr0qOyOP2jMpJTRy0DPvz5gANOuGXXeh0itYTM35i4mZI0Rh/wvXzIrMgrg6tc5Ft2MA/k547d9f+C/pfFj+uNHfx+9fXM4ip832R9/5o3vN1k36+h1HtfHbpV+B+oU2/TWdDm9/NFQ38IfNrAl+W1OjNHHBlmD8/R5JtUnvf3M//lW5xp9rXSrtI/eJ+XFXSbh/CX7lDgcay5KKSz8r/BWigrj6cExAXLqXGZlctEBFNAOfFq0d+EfsudKbiGdnsDbxjlMHidz87VlAsiDAgAowG5EAjkOBMBi43YGxC5VC8LVHSYDTSF72TR4B98KQFUNnBu9bWDVqLqBBlM2A5tJtQyUpnGps1TIwDyjygbWkR40UBuiiNgqNapBBppK2QxsBtUy0GTKbuDmqKaBXXalLQPcqlBapxzRDqjYlCvArZ0ykckejp0LfoNytNdMgBmEIaBoYP2oRgCNyGPwIBMROUaopwpSWFOEW+jpLdGVnfdUwaAwNhuAcrTjaPmqfPAOkr9zyzlAcGTntoaHhZ0KjZec8vHAjSBlI0LkZd3Nbsxu5BiGzXpSdphKitsIviMHKc+yEKfZQAS+5PAgEuEixbxUcUowoJPwK3g7JDgpNl4PwhNSJaISZqO8EMgji2CEQASJ5XOxrQiUI6fNsG4GqkJQFFaQk1JNsY6o0w/LyLKlagbkUI52BDcmR1DjxkOjmqimjokeBBCSNCUQCQZtv7eEnEH0sGLQRUcJTL1NhXV+LFXSYZrTBiJ6sIEkcsCcbgS3AKLK2QbCQw+O8GBCYB/HyQorBMRou3LDnttx7iHJ9XbFWIaUWeVzOJ87eVak2sZtlSobxyQ9aNwGNGmVQFUMn2jURsfnXUuje922d73Cg8CcLrdHb2Wiz9U0kRvPoemdRYvLEwCFF7WLSw6tb5HlPid8ldxxOAbJfgdzPySlycbOlRw9PaSQvCQ0Mk+UiCyRIgokmzQQp/KK6FC5qHlBmYuaFfQV60CKvpf1pa7k6HMyqHWdThqL+6bnHZ91TtcCTsdGqAhhKTJ68UEDgJsEzS/ZUhXeFtivYe1NgK10irns4O4aM+736WHfPqYXKbHtdfbSOfty1ofj+ch4OH5uC4Kc/qkM0pfTfARJuY4c70kYELZrD0mAn/T5UuFfJa6zJFzan84/XSUNM2Jsf98BoV8Gkx1MUs4p3AG2t/awSoYjtmeL/bGS89LFzp8xj0d23Fcj1nvEdH9O7BJxlkv3dcxupbgk/iMawOZ6Wx5CIJqxPbrvT5VcGDDXc0w4YV2R9g2J2aiF1yneO8jmEmWRPNdxZ0f2xyzOR5zXt+dCGxdDF1EbU49O/b07sgH2Fa2dAHrpI6UAP1jskAMdd0a/W0fxACpXSRhl2NN3nFP3zZB80c+3ojSRQyRZnMW7X/jSb1f79uhllIyYoQD0fwCc96dwYs9CAGCaT8+yPv3NeI7+YxO7AwBA3zvfMwCA+ZDlf7/l/p9/2N+DARBhAAAggLC+OAGIKypwncREdW9XnyKZXD1G5AqQE4la4e8R7qEpbJPCQ0/5QmaC5t23l1TKSylvEaLWLkWNeZLs1KdZJRAl2WLjP0CfSZyRZA7nS6UreX+fJ0wOcTk56uIZLfSUYgpYnNhQpaUzCDdIx5lzh5mvO4SzwLQ1CltLpexwpGmyS4DcnuN9XpI8YSQj7GyuocVPTkrIDNo3v4p2btsTd07x9L3vFstU6pgLiMd+uxRdGwRo5QSJy/PLntBTPweVzWdxXZXw0FC+fsmJNMXzK81Gckoq84rjReXyDMtQ6hgI8TC5+u45xT47fAHL3SrB+t8opVL/LVd5dpQVdhcazmOogMLQRGdLaaRR7xKEZ5Zkx+b37bec7pebOtlTRKsVjo3iDoUruaZ6QY99loyVzjbqKPPIjss9QilGpJY6lQaQ72/ZecWpIeISLKQ0SSNHOL17tDJyEyF7FKl0N5k2KU0q6mgrrDjaoiqcCDlNZZEqdvb0DhmkdTbh/e5BKSGkSgDL2eQ5ixzHytEqOpAoJjkuZD2kN2V011+Fc0N4seCQ/WxKJ9PdDGojfkyp9DiZs11uFZXe7rE/eDejhQSiYI17g52PezDzhzd3LHDeEU9EDzHEeUFEERvEAkWIMOLJvzmCiDSiin1DFPGdF+dNIHaIFf9G7BFrPvd8iygiXogn4t7nNyKLGFbML6XjL0dPUH8QT54F8Uec+dygDuVK2Ll5Z0xgf22w3/foXorBbtQ71C3UkzuAAPgkhzAzOKEETlaCacHf74qNOxQSJQKAI4ClbRHiHLfF4BZRi6ZrsbQtjjyawEOrf6zcrA3Q5y8ARRAvHjyFkKZBjboJSjPmzwA+3HZsyg+ZqjjpEJ+4ZbYMFoVbX3ATJKx4rlQdz5/Lk4T40s4mS15C+eYIj4nn43KM2AaDBPOSfiBE9VRNh+hg9T9kun8VZFYLAUgOGDW8oOqygCrI1J7dqPIXxEP4REtkbvyQRfCz3hmm9BkyY9VJFYi8GlTvmHaWXAE=) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAMAAA4AAAAABWwAAAKuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXkLEAABNgIkAxwEIAWDAAcgG0oEAB6D426JQgSiDJGrY+EepR5ejwf4/fWd+/C1EBKYZDS7sRFxHTf9uCJn/m9Of4qsOwRQBbqEex0QSbKziM9Pj42dA85/tYTLU84Cj+f+PIAlq3AtV5GCrQWUqr11TNFedSEUjKs7rSju46fX7RWCSHFAeYQcQRBEKIqiAgIKlGZBdO5a3w4akEBWj6orkgSzThrq5iF0WjfiKGe7e/0dAHkwOR8nW+GblHR72hyEGmzEl02NcDPu9oBKt35NVVBcoyEuIJNhau72SE3EHkhapkdqCiZGhBhliQWUJVETSCQCNfr8o/boWoBjI3miLHqQC4ojH22AaUBxFAUpIBJlJeIVGIvLFI6PlFi4hGYVs0brZ4ZZlT0rbz1SLT+50xlW3X269vh2x+CpO/n7bw02ebvIys0wMkpteMHUIq4PGfxCRBdKjxXGaDRIc42rK+a/qgeebsfBvjGMiQ14cnJjW8fSe6fHlr2NIrgbeH2jS+k9X+md9WJP/5IvZ8LRg1cQ3gz+dJMePnr2/6ZSiy3c9rHc87Zj4tqOx0WLe1U0VR2OOEt9kq4gV/r/NBEyVbPvpL70poCoTunu3LVVZ4nW3xWV8gAKP5VqBMD10Pruq+7/52x5c4B8EQjkzs5oyJ/1JzxT0mgEACA3XjUZACFDut7UuAEqPZepikCuTcprJBVAcSJREzIBeaYSC4kSGAs2BJU5IFLcQjt+sxNAqr55kwOx947iBrvVCRYwpBuDQusVLFWyFCmCVcEwCg8JVsPPK1GwEjxesNZJv6dyHtID6dYP8UnUCvPAemHBGiA+jD6CVgilD8+tWyfSPRiYXwVJDNNkydPUzvrRmeBZvFdArqSTDSCJ3ALcvDp0JBHWjTK8pb0Qvx7N35CkXo0yFRq1qZAgVaJkYiA7H3AA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} diff --git a/packages/graphiql-react/jest.config.js b/packages/graphiql-react/jest.config.js new file mode 100644 index 00000000000..22b3f1af305 --- /dev/null +++ b/packages/graphiql-react/jest.config.js @@ -0,0 +1,9 @@ +const base = require('../../jest.config.base')(__dirname); + +module.exports = { + ...base, + moduleNameMapper: { + '\\.svg$': `${__dirname}/__mocks__/svg`, + ...base.moduleNameMapper, + }, +}; diff --git a/packages/graphiql-react/package.json b/packages/graphiql-react/package.json new file mode 100644 index 00000000000..63b97d45346 --- /dev/null +++ b/packages/graphiql-react/package.json @@ -0,0 +1,79 @@ +{ + "name": "@graphiql/react", + "version": "0.19.0", + "repository": { + "type": "git", + "url": "https://github.com/graphql/graphiql", + "directory": "packages/graphiql-react" + }, + "homepage": "http://github.com/graphql/graphiql/tree/master/packages/graphiql-react#readme", + "bugs": { + "url": "https://github.com/graphql/graphiql/issues?q=issue+label:@graphiql/react" + }, + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "exports": { + "./package.json": "./package.json", + ".": { + "import": "./dist/index.mjs", + "require": "./dist/index.js", + "types": "./types/index.d.ts" + }, + "./font/roboto.css": "./font/roboto.css", + "./font/fira-code.css": "./font/fira-code.css", + "./dist/style.css": "./dist/style.css" + }, + "types": "types/index.d.ts", + "keywords": [ + "react", + "graphql", + "sdk", + "codemirror" + ], + "files": [ + "dist", + "font", + "src", + "types" + ], + "scripts": { + "dev": "concurrently 'tsc --emitDeclarationOnly --watch' 'vite build --watch'", + "build": "tsc --emitDeclarationOnly && vite build" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + }, + "dependencies": { + "@types/codemirror": "^5.60.8", + "@headlessui/react": "^1.7.15", + "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-visually-hidden": "^1.0.3", + "@radix-ui/react-tooltip": "^1.0.6", + "@radix-ui/react-dropdown-menu": "^2.0.5", + "@graphiql/toolkit": "^0.8.4", + "clsx": "^1.2.1", + "codemirror": "^5.65.3", + "codemirror-graphql": "^2.0.9", + "copy-to-clipboard": "^3.2.0", + "graphql-language-service": "^5.1.7", + "markdown-it": "^12.2.0", + "framer-motion": "^6.5.1", + "set-value": "^4.1.0" + }, + "devDependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@testing-library/react": "14.0.0", + "@types/set-value": "^4.0.1", + "@vitejs/plugin-react": "^4.0.1", + "graphql": "^16.4.0", + "postcss-nesting": "^10.1.7", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "typescript": "^4.6.3", + "vite": "^4.3.9", + "vite-plugin-svgr": "^3.2.0" + } +} diff --git a/packages/graphiql-react/postcss.config.js b/packages/graphiql-react/postcss.config.js new file mode 100644 index 00000000000..1ab3f9d65bf --- /dev/null +++ b/packages/graphiql-react/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require('postcss-nesting')], +}; diff --git a/packages/graphiql-react/src/editor/__tests__/common.spec.ts b/packages/graphiql-react/src/editor/__tests__/common.spec.ts new file mode 100644 index 00000000000..f811b62c4f0 --- /dev/null +++ b/packages/graphiql-react/src/editor/__tests__/common.spec.ts @@ -0,0 +1,12 @@ +import { importCodeMirror } from '../common'; + +describe('importCodeMirror', () => { + it('should dynamically load codemirror module', async () => { + const CodeMirror = await importCodeMirror([]); + expect(typeof CodeMirror === 'function').toBeTruthy(); + }); + it('should dynamically load codemirror module without common addons', async () => { + const CodeMirror = await importCodeMirror([], { useCommonAddons: false }); + expect(typeof CodeMirror === 'function').toBeTruthy(); + }); +}); diff --git a/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts new file mode 100644 index 00000000000..0314d220f9d --- /dev/null +++ b/packages/graphiql-react/src/editor/__tests__/tabs.spec.ts @@ -0,0 +1,193 @@ +import { StorageAPI } from '@graphiql/toolkit'; +import { + createTab, + fuzzyExtractOperationName, + getDefaultTabState, + clearHeadersFromTabs, + STORAGE_KEY, +} from '../tabs'; + +describe('createTab', () => { + it('creates with default title', () => { + expect(createTab({})).toEqual( + expect.objectContaining({ + id: expect.any(String), + hash: expect.any(String), + title: '', + }), + ); + }); + + it('creates with title from query', () => { + expect(createTab({ query: 'query Foo {}' })).toEqual( + expect.objectContaining({ + id: expect.any(String), + hash: expect.any(String), + title: 'Foo', + }), + ); + }); +}); + +describe('fuzzyExtractionOperationTitle', () => { + describe('without prefix', () => { + it('should extract query names', () => { + expect(fuzzyExtractOperationName('query MyExampleQuery() {}')).toEqual( + 'MyExampleQuery', + ); + }); + it('should extract query names with special characters', () => { + expect(fuzzyExtractOperationName('query My_ExampleQuery() {}')).toEqual( + 'My_ExampleQuery', + ); + }); + it('should extract query names with numbers', () => { + expect(fuzzyExtractOperationName('query My_3ExampleQuery() {}')).toEqual( + 'My_3ExampleQuery', + ); + }); + it('should extract mutation names with numbers', () => { + expect( + fuzzyExtractOperationName('mutation My_3ExampleQuery() {}'), + ).toEqual('My_3ExampleQuery'); + }); + }); + describe('with space prefix', () => { + it('should extract query names', () => { + expect(fuzzyExtractOperationName(' query MyExampleQuery() {}')).toEqual( + 'MyExampleQuery', + ); + }); + it('should extract query names with special characters', () => { + expect(fuzzyExtractOperationName(' query My_ExampleQuery() {}')).toEqual( + 'My_ExampleQuery', + ); + }); + it('should extract query names with numbers', () => { + expect(fuzzyExtractOperationName(' query My_3ExampleQuery() {}')).toEqual( + 'My_3ExampleQuery', + ); + }); + it('should extract mutation names with numbers', () => { + expect( + fuzzyExtractOperationName(' mutation My_3ExampleQuery() {}'), + ).toEqual('My_3ExampleQuery'); + }); + }); + + it('should return null for anonymous queries', () => { + expect(fuzzyExtractOperationName('{}')).toBeNull(); + }); + + describe('comment line handling', () => { + it('should not extract query names within commented out lines', () => { + expect( + fuzzyExtractOperationName('# query My_3ExampleQuery() {}'), + ).toBeNull(); + }); + it('should extract query names when there is a single leading comment line', () => { + expect( + fuzzyExtractOperationName( + '# comment line 1 \n query MyExampleQueryWithSingleCommentLine() {}', + ), + ).toEqual('MyExampleQueryWithSingleCommentLine'); + }); + it('should extract query names when there are more than one leading comment lines', () => { + expect( + fuzzyExtractOperationName( + '# comment line 1 \n # comment line 2 \n query MyExampleQueryWithMultipleCommentLines() {}', + ), + ).toEqual('MyExampleQueryWithMultipleCommentLines'); + }); + }); +}); + +describe('getDefaultTabState', () => { + it('returns default tab', () => { + expect( + getDefaultTabState({ + defaultQuery: '# Default', + headers: null, + query: null, + variables: null, + storage: null, + }), + ).toEqual({ + activeTabIndex: 0, + tabs: [ + expect.objectContaining({ + query: '# Default', + title: '', + }), + ], + }); + }); + + it('returns initial tabs', () => { + expect( + getDefaultTabState({ + defaultQuery: '# Default', + headers: null, + defaultTabs: [ + { + headers: null, + query: 'query Person { person { name } }', + variables: '{"id":"foo"}', + }, + { + headers: '{"x-header":"foo"}', + query: 'query Image { image }', + variables: null, + }, + ], + query: null, + variables: null, + storage: null, + }), + ).toEqual({ + activeTabIndex: 0, + tabs: [ + expect.objectContaining({ + query: 'query Person { person { name } }', + title: 'Person', + variables: '{"id":"foo"}', + }), + expect.objectContaining({ + headers: '{"x-header":"foo"}', + query: 'query Image { image }', + title: 'Image', + }), + ], + }); + }); +}); + +describe('clearHeadersFromTabs', () => { + const createMockStorage = () => { + const mockStorage = new Map(); + return mockStorage as unknown as StorageAPI; + }; + + it('preserves tab state except for headers', () => { + const storage = createMockStorage(); + const stateWithoutHeaders = { + operationName: 'test', + query: 'query test {\n test {\n id\n }\n}', + test: { + a: 'test', + }, + }; + const stateWithHeaders = { + ...stateWithoutHeaders, + headers: '{ "authorization": "secret" }', + }; + storage.set(STORAGE_KEY, JSON.stringify(stateWithHeaders)); + + clearHeadersFromTabs(storage); + + expect(JSON.parse(storage.get(STORAGE_KEY)!)).toEqual({ + ...stateWithoutHeaders, + headers: null, + }); + }); +}); diff --git a/packages/graphiql-react/src/editor/__tests__/whitespace.spec.ts b/packages/graphiql-react/src/editor/__tests__/whitespace.spec.ts new file mode 100644 index 00000000000..4a44b6aab21 --- /dev/null +++ b/packages/graphiql-react/src/editor/__tests__/whitespace.spec.ts @@ -0,0 +1,8 @@ +import { invalidCharacters, normalizeWhitespace } from '../whitespace'; + +describe('normalizeWhitespace', () => { + it('removes unicode characters', () => { + const result = normalizeWhitespace(invalidCharacters.join('')); + expect(result).toEqual(' '.repeat(invalidCharacters.length)); + }); +}); diff --git a/packages/graphiql-react/src/editor/common.ts b/packages/graphiql-react/src/editor/common.ts new file mode 100644 index 00000000000..720f427b101 --- /dev/null +++ b/packages/graphiql-react/src/editor/common.ts @@ -0,0 +1,57 @@ +import { KeyMap } from './types'; + +export const DEFAULT_EDITOR_THEME = 'graphiql'; +export const DEFAULT_KEY_MAP: KeyMap = 'sublime'; + +let isMacOs = false; + +if (typeof window === 'object') { + isMacOs = window.navigator.platform.toLowerCase().indexOf('mac') === 0; +} + +export const commonKeys = { + // Persistent search box in Query Editor + [isMacOs ? 'Cmd-F' : 'Ctrl-F']: 'findPersistent', + 'Cmd-G': 'findPersistent', + 'Ctrl-G': 'findPersistent', + + // Editor improvements + 'Ctrl-Left': 'goSubwordLeft', + 'Ctrl-Right': 'goSubwordRight', + 'Alt-Left': 'goGroupLeft', + 'Alt-Right': 'goGroupRight', +}; + +/** + * Dynamically import codemirror and dependencies + * This works for codemirror 5, not sure if the same imports work for 6 + */ +export async function importCodeMirror( + addons: Promise[], + options?: { useCommonAddons?: boolean }, +) { + const CodeMirror = await import('codemirror').then(c => + // Depending on bundler and settings the dynamic import either returns a + // function (e.g. parcel) or an object containing a `default` property + typeof c === 'function' ? c : c.default, + ); + await Promise.all( + options?.useCommonAddons === false + ? addons + : [ + import('codemirror/addon/hint/show-hint'), + import('codemirror/addon/edit/matchbrackets'), + import('codemirror/addon/edit/closebrackets'), + import('codemirror/addon/fold/brace-fold'), + import('codemirror/addon/fold/foldgutter'), + import('codemirror/addon/lint/lint'), + import('codemirror/addon/search/searchcursor'), + import('codemirror/addon/search/jump-to-line'), + import('codemirror/addon/dialog/dialog'), + // @ts-expect-error + import('codemirror/keymap/sublime'), + ...addons, + ], + ); + return CodeMirror; +} diff --git a/packages/graphiql-react/src/editor/completion.ts b/packages/graphiql-react/src/editor/completion.ts new file mode 100644 index 00000000000..231a53851ec --- /dev/null +++ b/packages/graphiql-react/src/editor/completion.ts @@ -0,0 +1,264 @@ +import type { Editor, EditorChange } from 'codemirror'; +import type { IHint } from 'codemirror-graphql/hint'; +import { + GraphQLNamedType, + GraphQLSchema, + GraphQLType, + isListType, + isNonNullType, +} from 'graphql'; + +import { ExplorerContextType } from '../explorer'; +import { markdown } from '../markdown'; +import { DOC_EXPLORER_PLUGIN, PluginContextType } from '../plugin'; +import { importCodeMirror } from './common'; + +/** + * Render a custom UI for CodeMirror's hint which includes additional info + * about the type and description for the selected context. + */ +export function onHasCompletion( + _cm: Editor, + data: EditorChange | undefined, + schema: GraphQLSchema | null | undefined, + explorer: ExplorerContextType | null, + plugin: PluginContextType | null, + callback?: (type: GraphQLNamedType) => void, +): void { + void importCodeMirror([], { useCommonAddons: false }).then(CodeMirror => { + let information: HTMLDivElement | null; + let fieldName: HTMLSpanElement | null; + let typeNamePill: HTMLSpanElement | null; + let typeNamePrefix: HTMLSpanElement | null; + let typeName: HTMLAnchorElement | null; + let typeNameSuffix: HTMLSpanElement | null; + let description: HTMLDivElement | null; + let deprecation: HTMLDivElement | null; + let deprecationReason: HTMLDivElement | null; + CodeMirror.on( + data, + 'select', + // @ts-expect-error + (ctx: IHint, el: HTMLDivElement) => { + // Only the first time (usually when the hint UI is first displayed) + // do we create the information nodes. + if (!information) { + const hintsUl = el.parentNode as HTMLUListElement & ParentNode; + + // This "information" node will contain the additional info about the + // highlighted typeahead option. + information = document.createElement('div'); + information.className = 'CodeMirror-hint-information'; + hintsUl.append(information); + + const header = document.createElement('header'); + header.className = 'CodeMirror-hint-information-header'; + information.append(header); + + fieldName = document.createElement('span'); + fieldName.className = 'CodeMirror-hint-information-field-name'; + header.append(fieldName); + + typeNamePill = document.createElement('span'); + typeNamePill.className = 'CodeMirror-hint-information-type-name-pill'; + header.append(typeNamePill); + + typeNamePrefix = document.createElement('span'); + typeNamePill.append(typeNamePrefix); + + typeName = document.createElement('a'); + typeName.className = 'CodeMirror-hint-information-type-name'; + typeName.href = 'javascript:void 0'; // eslint-disable-line no-script-url + typeName.addEventListener('click', onClickHintInformation); + typeNamePill.append(typeName); + + typeNameSuffix = document.createElement('span'); + typeNamePill.append(typeNameSuffix); + + description = document.createElement('div'); + description.className = 'CodeMirror-hint-information-description'; + information.append(description); + + deprecation = document.createElement('div'); + deprecation.className = 'CodeMirror-hint-information-deprecation'; + information.append(deprecation); + + const deprecationLabel = document.createElement('span'); + deprecationLabel.className = + 'CodeMirror-hint-information-deprecation-label'; + deprecationLabel.textContent = 'Deprecated'; + deprecation.append(deprecationLabel); + + deprecationReason = document.createElement('div'); + deprecationReason.className = + 'CodeMirror-hint-information-deprecation-reason'; + deprecation.append(deprecationReason); + + /** + * This is a bit hacky: By default, codemirror renders all hints + * inside a single container element. The only possibility to add + * something into this list is to add to the container element (which + * is a `ul` element). + * + * However, in the UI we want to have a two-column layout for the + * hints: + * - The first column contains the actual hints, i.e. the things that + * are returned from the `hint` module from `codemirror-graphql`. + * - The second column contains the description and optionally the + * deprecation reason for the given field. + * + * We solve this with a CSS grid layout that has an auto number of + * rows and two columns. All the hints go in the first column, and + * the description container (which is the `information` element + * here) goes in the second column. To make the hints scrollable, the + * container element has `overflow-y: auto`. + * + * Now here comes the crux: When scrolling down the list of hints we + * still want the description to be "sticky" to the top. We can't + * solve this with `position: sticky` as the container element itself + * is already positioned absolutely. + * + * There are two things to the solution here: + * - We add a `max-height` and another `overflow: auto` to the + * `information` element. This makes it scrollable on its own + * if the description or deprecation reason is higher that the + * container element. + * - We add an `onscroll` handler to the container element. When the + * user scrolls here we dynamically adjust the top padding and the + * max-height of the information element such that it looks like + * it's sticking to the top. (Since the `information` element has + * some padding by default we also have to make sure to use this + * as baseline for the total padding.) + * Note that we need to also adjust the max-height because we + * default to using `border-box` for box sizing. When using + * `content-box` this would not be necessary. + */ + const defaultInformationPadding = + parseInt( + window + .getComputedStyle(information) + .paddingBottom.replace(/px$/, ''), + 10, + ) || 0; + const defaultInformationMaxHeight = + parseInt( + window.getComputedStyle(information).maxHeight.replace(/px$/, ''), + 10, + ) || 0; + const handleScroll = () => { + if (information) { + information.style.paddingTop = + hintsUl.scrollTop + defaultInformationPadding + 'px'; + information.style.maxHeight = + hintsUl.scrollTop + defaultInformationMaxHeight + 'px'; + } + }; + hintsUl.addEventListener('scroll', handleScroll); + + // When CodeMirror attempts to remove the hint UI, we detect that it was + // removed and in turn remove the information nodes. + let onRemoveFn: EventListener | null; + hintsUl.addEventListener( + 'DOMNodeRemoved', + (onRemoveFn = (event: Event) => { + if (event.target !== hintsUl) { + return; + } + hintsUl.removeEventListener('scroll', handleScroll); + hintsUl.removeEventListener('DOMNodeRemoved', onRemoveFn); + if (information) { + information.removeEventListener( + 'click', + onClickHintInformation, + ); + } + information = null; + fieldName = null; + typeNamePill = null; + typeNamePrefix = null; + typeName = null; + typeNameSuffix = null; + description = null; + deprecation = null; + deprecationReason = null; + onRemoveFn = null; + }), + ); + } + + if (fieldName) { + fieldName.textContent = ctx.text; + } + + if (typeNamePill && typeNamePrefix && typeName && typeNameSuffix) { + if (ctx.type) { + typeNamePill.style.display = 'inline'; + + const renderType = (type: GraphQLType) => { + if (isNonNullType(type)) { + typeNameSuffix!.textContent = '!' + typeNameSuffix!.textContent; + renderType(type.ofType); + } else if (isListType(type)) { + typeNamePrefix!.textContent += '['; + typeNameSuffix!.textContent = ']' + typeNameSuffix!.textContent; + renderType(type.ofType); + } else { + typeName!.textContent = type.name; + } + }; + + typeNamePrefix.textContent = ''; + typeNameSuffix.textContent = ''; + renderType(ctx.type); + } else { + typeNamePrefix.textContent = ''; + typeName.textContent = ''; + typeNameSuffix.textContent = ''; + typeNamePill.style.display = 'none'; + } + } + + if (description) { + if (ctx.description) { + description.style.display = 'block'; + description.innerHTML = markdown.render(ctx.description); + } else { + description.style.display = 'none'; + description.innerHTML = ''; + } + } + + if (deprecation && deprecationReason) { + if (ctx.deprecationReason) { + deprecation.style.display = 'block'; + deprecationReason.innerHTML = markdown.render( + ctx.deprecationReason, + ); + } else { + deprecation.style.display = 'none'; + deprecationReason.innerHTML = ''; + } + } + }, + ); + }); + + function onClickHintInformation(event: Event) { + if ( + !schema || + !explorer || + !plugin || + !(event.currentTarget instanceof HTMLElement) + ) { + return; + } + + const typeName = event.currentTarget.textContent || ''; + const type = schema.getType(typeName); + if (type) { + plugin.setVisiblePlugin(DOC_EXPLORER_PLUGIN); + explorer.push({ name: type.name, def: type }); + callback?.(type); + } + } +} diff --git a/packages/graphiql-react/src/editor/components/header-editor.tsx b/packages/graphiql-react/src/editor/components/header-editor.tsx new file mode 100644 index 00000000000..672a2b759a2 --- /dev/null +++ b/packages/graphiql-react/src/editor/components/header-editor.tsx @@ -0,0 +1,35 @@ +import { useEffect } from 'react'; +import { clsx } from 'clsx'; + +import { useEditorContext } from '../context'; +import { useHeaderEditor, UseHeaderEditorArgs } from '../header-editor'; + +import '../style/codemirror.css'; +import '../style/fold.css'; +import '../style/editor.css'; + +type HeaderEditorProps = UseHeaderEditorArgs & { + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; +}; + +export function HeaderEditor({ isHidden, ...hookArgs }: HeaderEditorProps) { + const { headerEditor } = useEditorContext({ + nonNull: true, + caller: HeaderEditor, + }); + const ref = useHeaderEditor(hookArgs, HeaderEditor); + + useEffect(() => { + if (!isHidden) { + headerEditor?.refresh(); + } + }, [headerEditor, isHidden]); + + return ( +
+ ); +} diff --git a/packages/graphiql-react/src/editor/components/image-preview.tsx b/packages/graphiql-react/src/editor/components/image-preview.tsx new file mode 100644 index 00000000000..b330f7bb01a --- /dev/null +++ b/packages/graphiql-react/src/editor/components/image-preview.tsx @@ -0,0 +1,88 @@ +import type { Token } from 'codemirror'; +import { useEffect, useRef, useState } from 'react'; + +type ImagePreviewProps = { token: Token }; + +type Dimensions = { + width: number | null; + height: number | null; +}; + +export function ImagePreview(props: ImagePreviewProps) { + const [dimensions, setDimensions] = useState({ + width: null, + height: null, + }); + const [mime, setMime] = useState(null); + + const ref = useRef(null); + + const src = tokenToURL(props.token)?.href; + + useEffect(() => { + if (!ref.current) { + return; + } + if (!src) { + setDimensions({ width: null, height: null }); + setMime(null); + return; + } + + fetch(src, { method: 'HEAD' }) + .then(response => { + setMime(response.headers.get('Content-Type')); + }) + .catch(() => { + setMime(null); + }); + }, [src]); + + const dims = + dimensions.width !== null && dimensions.height !== null ? ( +
+ {dimensions.width}x{dimensions.height} + {mime === null ? null : ' ' + mime} +
+ ) : null; + + return ( +
+ { + setDimensions({ + width: ref.current?.naturalWidth ?? null, + height: ref.current?.naturalHeight ?? null, + }); + }} + ref={ref} + src={src} + /> + {dims} +
+ ); +} + +ImagePreview.shouldRender = function shouldRender(token: Token) { + const url = tokenToURL(token); + return url ? isImageURL(url) : false; +}; + +function tokenToURL(token: Token) { + if (token.type !== 'string') { + return; + } + + const value = token.string.slice(1).slice(0, -1).trim(); + + try { + const { location } = window; + return new URL(value, location.protocol + '//' + location.host); + } catch { + return; + } +} + +function isImageURL(url: URL) { + return /(bmp|gif|jpeg|jpg|png|svg)$/.test(url.pathname); +} diff --git a/packages/graphiql-react/src/editor/components/index.ts b/packages/graphiql-react/src/editor/components/index.ts new file mode 100644 index 00000000000..9fbe6db2a47 --- /dev/null +++ b/packages/graphiql-react/src/editor/components/index.ts @@ -0,0 +1,5 @@ +export { HeaderEditor } from './header-editor'; +export { ImagePreview } from './image-preview'; +export { QueryEditor } from './query-editor'; +export { ResponseEditor } from './response-editor'; +export { VariableEditor } from './variable-editor'; diff --git a/packages/graphiql-react/src/editor/components/query-editor.tsx b/packages/graphiql-react/src/editor/components/query-editor.tsx new file mode 100644 index 00000000000..79d6f2b4646 --- /dev/null +++ b/packages/graphiql-react/src/editor/components/query-editor.tsx @@ -0,0 +1,15 @@ +import { useQueryEditor, UseQueryEditorArgs } from '../query-editor'; + +import '../style/codemirror.css'; +import '../style/fold.css'; +import '../style/lint.css'; +import '../style/hint.css'; +import '../style/info.css'; +import '../style/jump.css'; +import '../style/auto-insertion.css'; +import '../style/editor.css'; + +export function QueryEditor(props: UseQueryEditorArgs) { + const ref = useQueryEditor(props, QueryEditor); + return
; +} diff --git a/packages/graphiql-react/src/editor/components/response-editor.tsx b/packages/graphiql-react/src/editor/components/response-editor.tsx new file mode 100644 index 00000000000..ae8150ee2e9 --- /dev/null +++ b/packages/graphiql-react/src/editor/components/response-editor.tsx @@ -0,0 +1,19 @@ +import { useResponseEditor, UseResponseEditorArgs } from '../response-editor'; + +import '../style/codemirror.css'; +import '../style/fold.css'; +import '../style/info.css'; +import '../style/editor.css'; + +export function ResponseEditor(props: UseResponseEditorArgs) { + const ref = useResponseEditor(props, ResponseEditor); + return ( +
+ ); +} diff --git a/packages/graphiql-react/src/editor/components/variable-editor.tsx b/packages/graphiql-react/src/editor/components/variable-editor.tsx new file mode 100644 index 00000000000..3d354157d7e --- /dev/null +++ b/packages/graphiql-react/src/editor/components/variable-editor.tsx @@ -0,0 +1,37 @@ +import { useEffect } from 'react'; +import { clsx } from 'clsx'; + +import { useEditorContext } from '../context'; +import { useVariableEditor, UseVariableEditorArgs } from '../variable-editor'; + +import '../style/codemirror.css'; +import '../style/fold.css'; +import '../style/lint.css'; +import '../style/hint.css'; +import '../style/editor.css'; + +type VariableEditorProps = UseVariableEditorArgs & { + /** + * Visually hide the header editor. + * @default false + */ + isHidden?: boolean; +}; + +export function VariableEditor({ isHidden, ...hookArgs }: VariableEditorProps) { + const { variableEditor } = useEditorContext({ + nonNull: true, + caller: VariableEditor, + }); + const ref = useVariableEditor(hookArgs, VariableEditor); + + useEffect(() => { + if (variableEditor && !isHidden) { + variableEditor.refresh(); + } + }, [variableEditor, isHidden]); + + return ( +
+ ); +} diff --git a/packages/graphiql-react/src/editor/context.tsx b/packages/graphiql-react/src/editor/context.tsx new file mode 100644 index 00000000000..0168c3285bf --- /dev/null +++ b/packages/graphiql-react/src/editor/context.tsx @@ -0,0 +1,589 @@ +import { + DocumentNode, + FragmentDefinitionNode, + OperationDefinitionNode, + parse, + ValidationRule, + visit, +} from 'graphql'; +import { VariableToType } from 'graphql-language-service'; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import { useStorageContext } from '../storage'; +import { createContextHook, createNullableContext } from '../utility/context'; +import { STORAGE_KEY as STORAGE_KEY_HEADERS } from './header-editor'; +import { useSynchronizeValue } from './hooks'; +import { STORAGE_KEY_QUERY } from './query-editor'; +import { + createTab, + getDefaultTabState, + setPropertiesInActiveTab, + TabDefinition, + TabsState, + TabState, + useSetEditorValues, + useStoreTabs, + useSynchronizeActiveTabValues, + clearHeadersFromTabs, + serializeTabState, + STORAGE_KEY as STORAGE_KEY_TABS, +} from './tabs'; +import { CodeMirrorEditor } from './types'; +import { STORAGE_KEY as STORAGE_KEY_VARIABLES } from './variable-editor'; + +export type CodeMirrorEditorWithOperationFacts = CodeMirrorEditor & { + documentAST: DocumentNode | null; + operationName: string | null; + operations: OperationDefinitionNode[] | null; + variableToType: VariableToType | null; +}; + +export type EditorContextType = TabsState & { + /** + * Add a new tab. + */ + addTab(): void; + /** + * Switch to a different tab. + * @param index The index of the tab that should be switched to. + */ + changeTab(index: number): void; + /** + * Move a tab to a new spot. + * @param newOrder The new order for the tabs. + */ + moveTab(newOrder: TabState[]): void; + /** + * Close a tab. If the currently active tab is closed, the tab before it will + * become active. If there is no tab before the closed one, the tab after it + * will become active. + * @param index The index of the tab that should be closed. + */ + closeTab(index: number): void; + /** + * Update the state for the tab that is currently active. This will be + * reflected in the `tabs` object and the state will be persisted in storage + * (if available). + * @param partialTab A partial tab state object that will override the + * current values. The properties `id`, `hash` and `title` cannot be changed. + */ + updateActiveTabValues( + partialTab: Partial>, + ): void; + + /** + * The CodeMirror editor instance for the headers editor. + */ + headerEditor: CodeMirrorEditor | null; + /** + * The CodeMirror editor instance for the query editor. This editor also + * stores the operation facts that are derived from the current editor + * contents. + */ + queryEditor: CodeMirrorEditorWithOperationFacts | null; + /** + * The CodeMirror editor instance for the response editor. + */ + responseEditor: CodeMirrorEditor | null; + /** + * The CodeMirror editor instance for the variables editor. + */ + variableEditor: CodeMirrorEditor | null; + /** + * Set the CodeMirror editor instance for the headers editor. + */ + setHeaderEditor(newEditor: CodeMirrorEditor): void; + /** + * Set the CodeMirror editor instance for the query editor. + */ + setQueryEditor(newEditor: CodeMirrorEditorWithOperationFacts): void; + /** + * Set the CodeMirror editor instance for the response editor. + */ + setResponseEditor(newEditor: CodeMirrorEditor): void; + /** + * Set the CodeMirror editor instance for the variables editor. + */ + setVariableEditor(newEditor: CodeMirrorEditor): void; + + /** + * Changes the operation name and invokes the `onEditOperationName` callback. + */ + setOperationName(operationName: string): void; + + /** + * The contents of the headers editor when initially rendering the provider + * component. + */ + initialHeaders: string; + /** + * The contents of the query editor when initially rendering the provider + * component. + */ + initialQuery: string; + /** + * The contents of the response editor when initially rendering the provider + * component. + */ + initialResponse: string; + /** + * The contents of the variables editor when initially rendering the provider + * component. + */ + initialVariables: string; + + /** + * A map of fragment definitions using the fragment name as key which are + * made available to include in the query. + */ + externalFragments: Map; + /** + * A list of custom validation rules that are run in addition to the rules + * provided by the GraphQL spec. + */ + validationRules: ValidationRule[]; + + /** + * If the contents of the headers editor are persisted in storage. + */ + shouldPersistHeaders: boolean; + /** + * Changes if headers should be persisted. + */ + setShouldPersistHeaders(persist: boolean): void; +}; + +export const EditorContext = + createNullableContext('EditorContext'); + +export type EditorContextProviderProps = { + children: ReactNode; + /** + * The initial contents of the query editor when loading GraphiQL and there + * is no other source for the editor state. Other sources can be: + * - The `query` prop + * - The value persisted in storage + * These default contents will only be used for the first tab. When opening + * more tabs the query editor will start out empty. + */ + defaultQuery?: string; + /** + * With this prop you can pass so-called "external" fragments that will be + * included in the query document (depending on usage). You can either pass + * the fragments using SDL (passing a string) or you can pass a list of + * `FragmentDefinitionNode` objects. + */ + externalFragments?: string | FragmentDefinitionNode[]; + /** + * This prop can be used to set the contents of the headers editor. Every + * time this prop changes, the contents of the headers editor are replaced. + * Note that the editor contents can be changed in between these updates by + * typing in the editor. + */ + headers?: string; + /** + * This prop can be used to define the default set of tabs, with their + * queries, variables, and headers. It will be used as default only if + * there is no tab state persisted in storage. + * + * @example + * ```tsx + * + *``` + */ + defaultTabs?: TabDefinition[]; + /** + * Invoked when the operation name changes. Possible triggers are: + * - Editing the contents of the query editor + * - Selecting a operation for execution in a document that contains multiple + * operation definitions + * @param operationName The operation name after it has been changed. + */ + onEditOperationName?(operationName: string): void; + /** + * Invoked when the state of the tabs changes. Possible triggers are: + * - Updating any editor contents inside the currently active tab + * - Adding a tab + * - Switching to a different tab + * - Closing a tab + * @param tabState The tabs state after it has been updated. + */ + onTabChange?(tabState: TabsState): void; + /** + * This prop can be used to set the contents of the query editor. Every time + * this prop changes, the contents of the query editor are replaced. Note + * that the editor contents can be changed in between these updates by typing + * in the editor. + */ + query?: string; + /** + * This prop can be used to set the contents of the response editor. Every + * time this prop changes, the contents of the response editor are replaced. + * Note that the editor contents can change in between these updates by + * executing queries that will show a response. + */ + response?: string; + /** + * This prop toggles if the contents of the headers editor are persisted in + * storage. + * @default false + */ + shouldPersistHeaders?: boolean; + /** + * This prop accepts custom validation rules for GraphQL documents that are + * run against the contents of the query editor (in addition to the rules + * that are specified in the GraphQL spec). + */ + validationRules?: ValidationRule[]; + /** + * This prop can be used to set the contents of the variables editor. Every + * time this prop changes, the contents of the variables editor are replaced. + * Note that the editor contents can be changed in between these updates by + * typing in the editor. + */ + variables?: string; + + /** + * Headers to be set when opening a new tab + */ + defaultHeaders?: string; +}; + +export function EditorContextProvider(props: EditorContextProviderProps) { + const storage = useStorageContext(); + const [headerEditor, setHeaderEditor] = useState( + null, + ); + const [queryEditor, setQueryEditor] = + useState(null); + const [responseEditor, setResponseEditor] = useState( + null, + ); + const [variableEditor, setVariableEditor] = useState( + null, + ); + + const [shouldPersistHeaders, setShouldPersistHeadersInternal] = useState( + () => { + const isStored = storage?.get(PERSIST_HEADERS_STORAGE_KEY) !== null; + return props.shouldPersistHeaders !== false && isStored + ? storage?.get(PERSIST_HEADERS_STORAGE_KEY) === 'true' + : Boolean(props.shouldPersistHeaders); + }, + ); + + useSynchronizeValue(headerEditor, props.headers); + useSynchronizeValue(queryEditor, props.query); + useSynchronizeValue(responseEditor, props.response); + useSynchronizeValue(variableEditor, props.variables); + + const storeTabs = useStoreTabs({ + storage, + shouldPersistHeaders, + }); + + // We store this in state but never update it. By passing a function we only + // need to compute it lazily during the initial render. + const [initialState] = useState(() => { + const query = props.query ?? storage?.get(STORAGE_KEY_QUERY) ?? null; + const variables = + props.variables ?? storage?.get(STORAGE_KEY_VARIABLES) ?? null; + const headers = props.headers ?? storage?.get(STORAGE_KEY_HEADERS) ?? null; + const response = props.response ?? ''; + + const tabState = getDefaultTabState({ + query, + variables, + headers, + defaultTabs: props.defaultTabs, + defaultQuery: props.defaultQuery || DEFAULT_QUERY, + defaultHeaders: props.defaultHeaders, + storage, + }); + storeTabs(tabState); + + return { + query: + query ?? + (tabState.activeTabIndex === 0 ? tabState.tabs[0].query : null) ?? + '', + variables: variables ?? '', + headers: headers ?? props.defaultHeaders ?? '', + response, + tabState, + }; + }); + + const [tabState, setTabState] = useState(initialState.tabState); + + const setShouldPersistHeaders = useCallback( + (persist: boolean) => { + if (persist) { + storage?.set(STORAGE_KEY_HEADERS, headerEditor?.getValue() ?? ''); + const serializedTabs = serializeTabState(tabState, true); + storage?.set(STORAGE_KEY_TABS, serializedTabs); + } else { + storage?.set(STORAGE_KEY_HEADERS, ''); + clearHeadersFromTabs(storage); + } + setShouldPersistHeadersInternal(persist); + storage?.set(PERSIST_HEADERS_STORAGE_KEY, persist.toString()); + }, + [storage, tabState, headerEditor], + ); + + const lastShouldPersistHeadersProp = useRef(); + useEffect(() => { + const propValue = Boolean(props.shouldPersistHeaders); + if (lastShouldPersistHeadersProp.current !== propValue) { + setShouldPersistHeaders(propValue); + lastShouldPersistHeadersProp.current = propValue; + } + }, [props.shouldPersistHeaders, setShouldPersistHeaders]); + + const synchronizeActiveTabValues = useSynchronizeActiveTabValues({ + queryEditor, + variableEditor, + headerEditor, + responseEditor, + }); + const setEditorValues = useSetEditorValues({ + queryEditor, + variableEditor, + headerEditor, + responseEditor, + }); + const { onTabChange, defaultHeaders, children } = props; + + const addTab = useCallback(() => { + setTabState(current => { + // Make sure the current tab stores the latest values + const updatedValues = synchronizeActiveTabValues(current); + const updated = { + tabs: [...updatedValues.tabs, createTab({ headers: defaultHeaders })], + activeTabIndex: updatedValues.tabs.length, + }; + storeTabs(updated); + setEditorValues(updated.tabs[updated.activeTabIndex]); + onTabChange?.(updated); + return updated; + }); + }, [ + defaultHeaders, + onTabChange, + setEditorValues, + storeTabs, + synchronizeActiveTabValues, + ]); + + const changeTab = useCallback( + index => { + setTabState(current => { + const updated = { + ...current, + activeTabIndex: index, + }; + storeTabs(updated); + setEditorValues(updated.tabs[updated.activeTabIndex]); + onTabChange?.(updated); + return updated; + }); + }, + [onTabChange, setEditorValues, storeTabs], + ); + + const moveTab = useCallback( + newOrder => { + setTabState(current => { + const activeTab = current.tabs[current.activeTabIndex]; + const updated = { + tabs: newOrder, + activeTabIndex: newOrder.indexOf(activeTab), + }; + storeTabs(updated); + setEditorValues(updated.tabs[updated.activeTabIndex]); + onTabChange?.(updated); + return updated; + }); + }, + [onTabChange, setEditorValues, storeTabs], + ); + + const closeTab = useCallback( + index => { + setTabState(current => { + const updated = { + tabs: current.tabs.filter((_tab, i) => index !== i), + activeTabIndex: Math.max(current.activeTabIndex - 1, 0), + }; + storeTabs(updated); + setEditorValues(updated.tabs[updated.activeTabIndex]); + onTabChange?.(updated); + return updated; + }); + }, + [onTabChange, setEditorValues, storeTabs], + ); + + const updateActiveTabValues = useCallback< + EditorContextType['updateActiveTabValues'] + >( + partialTab => { + setTabState(current => { + const updated = setPropertiesInActiveTab(current, partialTab); + storeTabs(updated); + onTabChange?.(updated); + return updated; + }); + }, + [onTabChange, storeTabs], + ); + + const { onEditOperationName } = props; + const setOperationName = useCallback( + operationName => { + if (!queryEditor) { + return; + } + + queryEditor.operationName = operationName; + updateActiveTabValues({ operationName }); + onEditOperationName?.(operationName); + }, + [onEditOperationName, queryEditor, updateActiveTabValues], + ); + + const externalFragments = useMemo(() => { + const map = new Map(); + if (Array.isArray(props.externalFragments)) { + for (const fragment of props.externalFragments) { + map.set(fragment.name.value, fragment); + } + } else if (typeof props.externalFragments === 'string') { + visit(parse(props.externalFragments, {}), { + FragmentDefinition(fragment) { + map.set(fragment.name.value, fragment); + }, + }); + } else if (props.externalFragments) { + throw new Error( + 'The `externalFragments` prop must either be a string that contains the fragment definitions in SDL or a list of FragmentDefinitionNode objects.', + ); + } + return map; + }, [props.externalFragments]); + + const validationRules = useMemo( + () => props.validationRules || [], + [props.validationRules], + ); + + const value = useMemo( + () => ({ + ...tabState, + addTab, + changeTab, + moveTab, + closeTab, + updateActiveTabValues, + + headerEditor, + queryEditor, + responseEditor, + variableEditor, + setHeaderEditor, + setQueryEditor, + setResponseEditor, + setVariableEditor, + + setOperationName, + + initialQuery: initialState.query, + initialVariables: initialState.variables, + initialHeaders: initialState.headers, + initialResponse: initialState.response, + + externalFragments, + validationRules, + + shouldPersistHeaders, + setShouldPersistHeaders, + }), + [ + tabState, + addTab, + changeTab, + moveTab, + closeTab, + updateActiveTabValues, + + headerEditor, + queryEditor, + responseEditor, + variableEditor, + + setOperationName, + + initialState, + + externalFragments, + validationRules, + + shouldPersistHeaders, + setShouldPersistHeaders, + ], + ); + + return ( + {children} + ); +} + +export const useEditorContext = createContextHook(EditorContext); + +const PERSIST_HEADERS_STORAGE_KEY = 'shouldPersistHeaders'; + +const DEFAULT_QUERY = `# Welcome to GraphiQL +# +# GraphiQL is an in-browser tool for writing, validating, and +# testing GraphQL queries. +# +# Type queries into this side of the screen, and you will see intelligent +# typeaheads aware of the current GraphQL type schema and live syntax and +# validation errors highlighted within the text. +# +# GraphQL queries typically start with a "{" character. Lines that start +# with a # are ignored. +# +# An example GraphQL query might look like: +# +# { +# field(arg: "value") { +# subField +# } +# } +# +# Keyboard shortcuts: +# +# Prettify query: Shift-Ctrl-P (or press the prettify button) +# +# Merge fragments: Shift-Ctrl-M (or press the merge button) +# +# Run Query: Ctrl-Enter (or press the play button) +# +# Auto Complete: Ctrl-Space (or just start typing) +# + +`; diff --git a/packages/graphiql-react/src/editor/header-editor.ts b/packages/graphiql-react/src/editor/header-editor.ts new file mode 100644 index 00000000000..db700f56fea --- /dev/null +++ b/packages/graphiql-react/src/editor/header-editor.ts @@ -0,0 +1,132 @@ +import { useEffect, useRef } from 'react'; + +import { useExecutionContext } from '../execution'; +import { + commonKeys, + DEFAULT_EDITOR_THEME, + DEFAULT_KEY_MAP, + importCodeMirror, +} from './common'; +import { useEditorContext } from './context'; +import { + useChangeHandler, + useKeyMap, + useMergeQuery, + usePrettifyEditors, + useSynchronizeOption, +} from './hooks'; +import { WriteableEditorProps } from './types'; + +export type UseHeaderEditorArgs = WriteableEditorProps & { + /** + * Invoked when the contents of the headers editor change. + * @param value The new contents of the editor. + */ + onEdit?(value: string): void; +}; + +export function useHeaderEditor( + { + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + onEdit, + readOnly = false, + }: UseHeaderEditorArgs = {}, + caller?: Function, +) { + const { + initialHeaders, + headerEditor, + setHeaderEditor, + shouldPersistHeaders, + } = useEditorContext({ + nonNull: true, + caller: caller || useHeaderEditor, + }); + const executionContext = useExecutionContext(); + const merge = useMergeQuery({ caller: caller || useHeaderEditor }); + const prettify = usePrettifyEditors({ caller: caller || useHeaderEditor }); + const ref = useRef(null); + + useEffect(() => { + let isActive = true; + + void importCodeMirror([ + // @ts-expect-error + import('codemirror/mode/javascript/javascript'), + ]).then(CodeMirror => { + // Don't continue if the effect has already been cleaned up + if (!isActive) { + return; + } + + const container = ref.current; + if (!container) { + return; + } + + const newEditor = CodeMirror(container, { + value: initialHeaders, + lineNumbers: true, + tabSize: 2, + mode: { name: 'javascript', json: true }, + theme: editorTheme, + autoCloseBrackets: true, + matchBrackets: true, + showCursorWhenSelecting: true, + readOnly: readOnly ? 'nocursor' : false, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + extraKeys: commonKeys, + }); + + newEditor.addKeyMap({ + 'Cmd-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Ctrl-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Alt-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Shift-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + }); + + newEditor.on('keyup', (editorInstance, event) => { + const { code, key, shiftKey } = event; + const isLetter = code.startsWith('Key'); + const isNumber = !shiftKey && code.startsWith('Digit'); + if (isLetter || isNumber || key === '_' || key === '"') { + editorInstance.execCommand('autocomplete'); + } + }); + + setHeaderEditor(newEditor); + }); + + return () => { + isActive = false; + }; + }, [editorTheme, initialHeaders, readOnly, setHeaderEditor]); + + useSynchronizeOption(headerEditor, 'keyMap', keyMap); + + useChangeHandler( + headerEditor, + onEdit, + shouldPersistHeaders ? STORAGE_KEY : null, + 'headers', + useHeaderEditor, + ); + + useKeyMap(headerEditor, ['Cmd-Enter', 'Ctrl-Enter'], executionContext?.run); + useKeyMap(headerEditor, ['Shift-Ctrl-P'], prettify); + useKeyMap(headerEditor, ['Shift-Ctrl-M'], merge); + + return ref; +} + +export const STORAGE_KEY = 'headers'; diff --git a/packages/graphiql-react/src/editor/hooks.ts b/packages/graphiql-react/src/editor/hooks.ts new file mode 100644 index 00000000000..d86d2837e91 --- /dev/null +++ b/packages/graphiql-react/src/editor/hooks.ts @@ -0,0 +1,334 @@ +import { fillLeafs, GetDefaultFieldNamesFn, mergeAst } from '@graphiql/toolkit'; +import type { EditorChange, EditorConfiguration } from 'codemirror'; +import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; +import copyToClipboard from 'copy-to-clipboard'; +import { parse, print } from 'graphql'; +import { useCallback, useEffect } from 'react'; + +import { useExplorerContext } from '../explorer'; +import { usePluginContext } from '../plugin'; +import { useSchemaContext } from '../schema'; +import { useStorageContext } from '../storage'; +import debounce from '../utility/debounce'; +import { onHasCompletion } from './completion'; +import { useEditorContext } from './context'; +import { CodeMirrorEditor } from './types'; + +export function useSynchronizeValue( + editor: CodeMirrorEditor | null, + value: string | undefined, +) { + useEffect(() => { + if (editor && typeof value === 'string' && value !== editor.getValue()) { + editor.setValue(value); + } + }, [editor, value]); +} + +export function useSynchronizeOption( + editor: CodeMirrorEditor | null, + option: K, + value: EditorConfiguration[K], +) { + useEffect(() => { + if (editor) { + editor.setOption(option, value); + } + }, [editor, option, value]); +} + +export function useChangeHandler( + editor: CodeMirrorEditor | null, + callback: ((value: string) => void) | undefined, + storageKey: string | null, + tabProperty: 'variables' | 'headers', + caller: Function, +) { + const { updateActiveTabValues } = useEditorContext({ nonNull: true, caller }); + const storage = useStorageContext(); + + useEffect(() => { + if (!editor) { + return; + } + + const store = debounce(500, (value: string) => { + if (!storage || storageKey === null) { + return; + } + storage.set(storageKey, value); + }); + + const updateTab = debounce(100, (value: string) => { + updateActiveTabValues({ [tabProperty]: value }); + }); + + const handleChange = ( + editorInstance: CodeMirrorEditor, + changeObj: EditorChange | undefined, + ) => { + // When we signal a change manually without actually changing anything + // we don't want to invoke the callback. + if (!changeObj) { + return; + } + + const newValue = editorInstance.getValue(); + store(newValue); + updateTab(newValue); + callback?.(newValue); + }; + editor.on('change', handleChange); + return () => editor.off('change', handleChange); + }, [ + callback, + editor, + storage, + storageKey, + tabProperty, + updateActiveTabValues, + ]); +} + +export function useCompletion( + editor: CodeMirrorEditor | null, + callback: ((reference: SchemaReference) => void) | null, + caller: Function, +) { + const { schema } = useSchemaContext({ nonNull: true, caller }); + const explorer = useExplorerContext(); + const plugin = usePluginContext(); + useEffect(() => { + if (!editor) { + return; + } + + const handleCompletion = ( + instance: CodeMirrorEditor, + changeObj?: EditorChange, + ) => { + onHasCompletion(instance, changeObj, schema, explorer, plugin, type => { + callback?.({ kind: 'Type', type, schema: schema || undefined }); + }); + }; + editor.on( + // @ts-expect-error @TODO additional args for hasCompletion event + 'hasCompletion', + handleCompletion, + ); + return () => + editor.off( + // @ts-expect-error @TODO additional args for hasCompletion event + 'hasCompletion', + handleCompletion, + ); + }, [callback, editor, explorer, plugin, schema]); +} + +type EmptyCallback = () => void; + +export function useKeyMap( + editor: CodeMirrorEditor | null, + keys: string[], + callback: EmptyCallback | undefined, +) { + useEffect(() => { + if (!editor) { + return; + } + for (const key of keys) { + editor.removeKeyMap(key); + } + + if (callback) { + const keyMap: Record = {}; + for (const key of keys) { + keyMap[key] = () => callback(); + } + editor.addKeyMap(keyMap); + } + }, [editor, keys, callback]); +} + +export type UseCopyQueryArgs = { + /** + * This is only meant to be used internally in `@graphiql/react`. + */ + caller?: Function; + /** + * Invoked when the current contents of the query editor are copied to the + * clipboard. + * @param query The content that has been copied. + */ + onCopyQuery?: (query: string) => void; +}; + +export function useCopyQuery({ caller, onCopyQuery }: UseCopyQueryArgs = {}) { + const { queryEditor } = useEditorContext({ + nonNull: true, + caller: caller || useCopyQuery, + }); + return useCallback(() => { + if (!queryEditor) { + return; + } + + const query = queryEditor.getValue(); + copyToClipboard(query); + + onCopyQuery?.(query); + }, [queryEditor, onCopyQuery]); +} + +type UseMergeQueryArgs = { + /** + * This is only meant to be used internally in `@graphiql/react`. + */ + caller?: Function; +}; + +export function useMergeQuery({ caller }: UseMergeQueryArgs = {}) { + const { queryEditor } = useEditorContext({ + nonNull: true, + caller: caller || useMergeQuery, + }); + const { schema } = useSchemaContext({ nonNull: true, caller: useMergeQuery }); + return useCallback(() => { + const documentAST = queryEditor?.documentAST; + const query = queryEditor?.getValue(); + if (!documentAST || !query) { + return; + } + + queryEditor.setValue(print(mergeAst(documentAST, schema))); + }, [queryEditor, schema]); +} + +type UsePrettifyEditorsArgs = { + /** + * This is only meant to be used internally in `@graphiql/react`. + */ + caller?: Function; +}; + +export function usePrettifyEditors({ caller }: UsePrettifyEditorsArgs = {}) { + const { queryEditor, headerEditor, variableEditor } = useEditorContext({ + nonNull: true, + caller: caller || usePrettifyEditors, + }); + return useCallback(() => { + if (variableEditor) { + const variableEditorContent = variableEditor.getValue(); + try { + const prettifiedVariableEditorContent = JSON.stringify( + JSON.parse(variableEditorContent), + null, + 2, + ); + if (prettifiedVariableEditorContent !== variableEditorContent) { + variableEditor.setValue(prettifiedVariableEditorContent); + } + } catch { + /* Parsing JSON failed, skip prettification */ + } + } + + if (headerEditor) { + const headerEditorContent = headerEditor.getValue(); + + try { + const prettifiedHeaderEditorContent = JSON.stringify( + JSON.parse(headerEditorContent), + null, + 2, + ); + if (prettifiedHeaderEditorContent !== headerEditorContent) { + headerEditor.setValue(prettifiedHeaderEditorContent); + } + } catch { + /* Parsing JSON failed, skip prettification */ + } + } + + if (queryEditor) { + const editorContent = queryEditor.getValue(); + const prettifiedEditorContent = print(parse(editorContent)); + + if (prettifiedEditorContent !== editorContent) { + queryEditor.setValue(prettifiedEditorContent); + } + } + }, [queryEditor, variableEditor, headerEditor]); +} + +export type UseAutoCompleteLeafsArgs = { + /** + * A function to determine which field leafs are automatically added when + * trying to execute a query with missing selection sets. It will be called + * with the `GraphQLType` for which fields need to be added. + */ + getDefaultFieldNames?: GetDefaultFieldNamesFn; + /** + * This is only meant to be used internally in `@graphiql/react`. + */ + caller?: Function; +}; + +export function useAutoCompleteLeafs({ + getDefaultFieldNames, + caller, +}: UseAutoCompleteLeafsArgs = {}) { + const { schema } = useSchemaContext({ + nonNull: true, + caller: caller || useAutoCompleteLeafs, + }); + const { queryEditor } = useEditorContext({ + nonNull: true, + caller: caller || useAutoCompleteLeafs, + }); + return useCallback(() => { + if (!queryEditor) { + return; + } + + const query = queryEditor.getValue(); + const { insertions, result } = fillLeafs( + schema, + query, + getDefaultFieldNames, + ); + if (insertions && insertions.length > 0) { + queryEditor.operation(() => { + const cursor = queryEditor.getCursor(); + const cursorIndex = queryEditor.indexFromPos(cursor); + queryEditor.setValue(result || ''); + let added = 0; + const markers = insertions.map(({ index, string }) => + queryEditor.markText( + queryEditor.posFromIndex(index + added), + queryEditor.posFromIndex(index + (added += string.length)), + { + className: 'auto-inserted-leaf', + clearOnEnter: true, + title: 'Automatically added leaf fields', + }, + ), + ); + setTimeout(() => { + for (const marker of markers) { + marker.clear(); + } + }, 7000); + let newCursorIndex = cursorIndex; + for (const { index, string } of insertions) { + if (index < cursorIndex) { + newCursorIndex += string.length; + } + } + queryEditor.setCursor(queryEditor.posFromIndex(newCursorIndex)); + }); + } + + return result; + }, [getDefaultFieldNames, queryEditor, schema]); +} diff --git a/packages/graphiql-react/src/editor/index.ts b/packages/graphiql-react/src/editor/index.ts new file mode 100644 index 00000000000..a62201b24a5 --- /dev/null +++ b/packages/graphiql-react/src/editor/index.ts @@ -0,0 +1,34 @@ +export { + HeaderEditor, + ImagePreview, + QueryEditor, + ResponseEditor, + VariableEditor, +} from './components'; +export { + EditorContext, + EditorContextProvider, + useEditorContext, +} from './context'; +export { useHeaderEditor } from './header-editor'; +export { + useAutoCompleteLeafs, + useCopyQuery, + useMergeQuery, + usePrettifyEditors, +} from './hooks'; +export { useQueryEditor } from './query-editor'; +export { useResponseEditor } from './response-editor'; +export { useVariableEditor } from './variable-editor'; + +export type { EditorContextType, EditorContextProviderProps } from './context'; +export type { UseHeaderEditorArgs } from './header-editor'; +export type { UseQueryEditorArgs } from './query-editor'; +export type { + ResponseTooltipType, + UseResponseEditorArgs, +} from './response-editor'; +export type { TabsState } from './tabs'; +export type { UseVariableEditorArgs } from './variable-editor'; + +export type { CommonEditorProps, KeyMap, WriteableEditorProps } from './types'; diff --git a/packages/graphiql-react/src/editor/query-editor.ts b/packages/graphiql-react/src/editor/query-editor.ts new file mode 100644 index 00000000000..33147427dd2 --- /dev/null +++ b/packages/graphiql-react/src/editor/query-editor.ts @@ -0,0 +1,511 @@ +import { getSelectedOperationName } from '@graphiql/toolkit'; +import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; +import type { + DocumentNode, + FragmentDefinitionNode, + GraphQLSchema, + ValidationRule, +} from 'graphql'; +import { getOperationFacts } from 'graphql-language-service'; +import { + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; + +import { useExecutionContext } from '../execution'; +import { useExplorerContext } from '../explorer'; +import { markdown } from '../markdown'; +import { DOC_EXPLORER_PLUGIN, usePluginContext } from '../plugin'; +import { useSchemaContext } from '../schema'; +import { useStorageContext } from '../storage'; +import debounce from '../utility/debounce'; +import { + commonKeys, + DEFAULT_EDITOR_THEME, + DEFAULT_KEY_MAP, + importCodeMirror, +} from './common'; +import { + CodeMirrorEditorWithOperationFacts, + useEditorContext, +} from './context'; +import { + useCompletion, + useCopyQuery, + UseCopyQueryArgs, + useKeyMap, + useMergeQuery, + usePrettifyEditors, + useSynchronizeOption, +} from './hooks'; +import { + CodeMirrorEditor, + CodeMirrorType, + WriteableEditorProps, +} from './types'; +import { normalizeWhitespace } from './whitespace'; + +export type UseQueryEditorArgs = WriteableEditorProps & + Pick & { + /** + * Invoked when a reference to the GraphQL schema (type or field) is clicked + * as part of the editor or one of its tooltips. + * @param reference The reference that has been clicked. + */ + onClickReference?(reference: SchemaReference): void; + /** + * Invoked when the contents of the query editor change. + * @param value The new contents of the editor. + * @param documentAST The editor contents parsed into a GraphQL document. + */ + onEdit?(value: string, documentAST?: DocumentNode): void; + }; + +export function useQueryEditor( + { + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + onClickReference, + onCopyQuery, + onEdit, + readOnly = false, + }: UseQueryEditorArgs = {}, + caller?: Function, +) { + const { schema } = useSchemaContext({ + nonNull: true, + caller: caller || useQueryEditor, + }); + const { + externalFragments, + initialQuery, + queryEditor, + setOperationName, + setQueryEditor, + validationRules, + variableEditor, + updateActiveTabValues, + } = useEditorContext({ + nonNull: true, + caller: caller || useQueryEditor, + }); + const executionContext = useExecutionContext(); + const storage = useStorageContext(); + const explorer = useExplorerContext(); + const plugin = usePluginContext(); + const copy = useCopyQuery({ caller: caller || useQueryEditor, onCopyQuery }); + const merge = useMergeQuery({ caller: caller || useQueryEditor }); + const prettify = usePrettifyEditors({ caller: caller || useQueryEditor }); + const ref = useRef(null); + const codeMirrorRef = useRef(); + + const onClickReferenceRef = useRef< + NonNullable + >(() => {}); + useEffect(() => { + onClickReferenceRef.current = reference => { + if (!explorer || !plugin) { + return; + } + plugin.setVisiblePlugin(DOC_EXPLORER_PLUGIN); + switch (reference.kind) { + case 'Type': { + explorer.push({ name: reference.type.name, def: reference.type }); + break; + } + case 'Field': { + explorer.push({ name: reference.field.name, def: reference.field }); + break; + } + case 'Argument': { + if (reference.field) { + explorer.push({ name: reference.field.name, def: reference.field }); + } + break; + } + case 'EnumValue': { + if (reference.type) { + explorer.push({ name: reference.type.name, def: reference.type }); + } + break; + } + } + onClickReference?.(reference); + }; + }, [explorer, onClickReference, plugin]); + + useEffect(() => { + let isActive = true; + + void importCodeMirror([ + import('codemirror/addon/comment/comment'), + import('codemirror/addon/search/search'), + import('codemirror-graphql/esm/hint'), + import('codemirror-graphql/esm/lint'), + import('codemirror-graphql/esm/info'), + import('codemirror-graphql/esm/jump'), + import('codemirror-graphql/esm/mode'), + ]).then(CodeMirror => { + // Don't continue if the effect has already been cleaned up + if (!isActive) { + return; + } + + codeMirrorRef.current = CodeMirror; + + const container = ref.current; + if (!container) { + return; + } + + const newEditor = CodeMirror(container, { + value: initialQuery, + lineNumbers: true, + tabSize: 2, + foldGutter: true, + mode: 'graphql', + theme: editorTheme, + autoCloseBrackets: true, + matchBrackets: true, + showCursorWhenSelecting: true, + readOnly: readOnly ? 'nocursor' : false, + lint: { + // @ts-expect-error + schema: undefined, + validationRules: null, + // linting accepts string or FragmentDefinitionNode[] + externalFragments: undefined, + }, + hintOptions: { + // @ts-expect-error + schema: undefined, + closeOnUnfocus: false, + completeSingle: false, + container, + externalFragments: undefined, + }, + info: { + schema: undefined, + renderDescription: (text: string) => markdown.render(text), + onClick(reference: SchemaReference) { + onClickReferenceRef.current(reference); + }, + }, + jump: { + schema: undefined, + onClick(reference: SchemaReference) { + onClickReferenceRef.current(reference); + }, + }, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + extraKeys: { + ...commonKeys, + 'Cmd-S'() { + // empty + }, + 'Ctrl-S'() { + // empty + }, + }, + }) as CodeMirrorEditorWithOperationFacts; + + newEditor.addKeyMap({ + 'Cmd-Space'() { + newEditor.showHint({ completeSingle: true, container }); + }, + 'Ctrl-Space'() { + newEditor.showHint({ completeSingle: true, container }); + }, + 'Alt-Space'() { + newEditor.showHint({ completeSingle: true, container }); + }, + 'Shift-Space'() { + newEditor.showHint({ completeSingle: true, container }); + }, + 'Shift-Alt-Space'() { + newEditor.showHint({ completeSingle: true, container }); + }, + }); + + newEditor.on('keyup', (editorInstance, event) => { + if (AUTO_COMPLETE_AFTER_KEY.test(event.key)) { + editorInstance.execCommand('autocomplete'); + } + }); + + let showingHints = false; + + // fired whenever a hint dialog opens + newEditor.on('startCompletion', () => { + showingHints = true; + }); + + // the codemirror hint extension fires this anytime the dialog is closed + // via any method (e.g. focus blur, escape key, ...) + newEditor.on('endCompletion', () => { + showingHints = false; + }); + + newEditor.on('keydown', (editorInstance, event) => { + if (event.key === 'Escape' && showingHints) { + event.stopPropagation(); + } + }); + + newEditor.on('beforeChange', (editorInstance, change) => { + // The update function is only present on non-redo, non-undo events. + if (change.origin === 'paste') { + const text = change.text.map(normalizeWhitespace); + change.update?.(change.from, change.to, text); + } + }); + + newEditor.documentAST = null; + newEditor.operationName = null; + newEditor.operations = null; + newEditor.variableToType = null; + + setQueryEditor(newEditor); + }); + + return () => { + isActive = false; + }; + }, [editorTheme, initialQuery, readOnly, setQueryEditor]); + + useSynchronizeOption(queryEditor, 'keyMap', keyMap); + + /** + * We don't use the generic `useChangeHandler` hook here because we want to + * have additional logic that updates the operation facts that we store as + * properties on the editor. + */ + useEffect(() => { + if (!queryEditor) { + return; + } + + function getAndUpdateOperationFacts( + editorInstance: CodeMirrorEditorWithOperationFacts, + ) { + const operationFacts = getOperationFacts( + schema, + editorInstance.getValue(), + ); + + // Update operation name should any query names change. + const operationName = getSelectedOperationName( + editorInstance.operations ?? undefined, + editorInstance.operationName ?? undefined, + operationFacts?.operations, + ); + + // Store the operation facts on editor properties + editorInstance.documentAST = operationFacts?.documentAST ?? null; + editorInstance.operationName = operationName ?? null; + editorInstance.operations = operationFacts?.operations ?? null; + + // Update variable types for the variable editor + if (variableEditor) { + variableEditor.state.lint.linterOptions.variableToType = + operationFacts?.variableToType; + variableEditor.options.lint.variableToType = + operationFacts?.variableToType; + variableEditor.options.hintOptions.variableToType = + operationFacts?.variableToType; + codeMirrorRef.current?.signal(variableEditor, 'change', variableEditor); + } + + return operationFacts ? { ...operationFacts, operationName } : null; + } + + const handleChange = debounce( + 100, + (editorInstance: CodeMirrorEditorWithOperationFacts) => { + const query = editorInstance.getValue(); + storage?.set(STORAGE_KEY_QUERY, query); + + const currentOperationName = editorInstance.operationName; + const operationFacts = getAndUpdateOperationFacts(editorInstance); + if (operationFacts?.operationName !== undefined) { + storage?.set( + STORAGE_KEY_OPERATION_NAME, + operationFacts.operationName, + ); + } + + // Invoke callback props only after the operation facts have been updated + onEdit?.(query, operationFacts?.documentAST); + if ( + operationFacts?.operationName && + currentOperationName !== operationFacts.operationName + ) { + setOperationName(operationFacts.operationName); + } + + updateActiveTabValues({ + query, + operationName: operationFacts?.operationName ?? null, + }); + }, + ) as (editorInstance: CodeMirrorEditor) => void; + + // Call once to initially update the values + getAndUpdateOperationFacts(queryEditor); + + queryEditor.on('change', handleChange); + return () => queryEditor.off('change', handleChange); + }, [ + onEdit, + queryEditor, + schema, + setOperationName, + storage, + variableEditor, + updateActiveTabValues, + ]); + + useSynchronizeSchema(queryEditor, schema ?? null, codeMirrorRef); + useSynchronizeValidationRules( + queryEditor, + validationRules ?? null, + codeMirrorRef, + ); + useSynchronizeExternalFragments( + queryEditor, + externalFragments, + codeMirrorRef, + ); + + useCompletion(queryEditor, onClickReference || null, useQueryEditor); + + const run = executionContext?.run; + const runAtCursor = useCallback(() => { + if ( + !run || + !queryEditor || + !queryEditor.operations || + !queryEditor.hasFocus() + ) { + run?.(); + return; + } + + const cursorIndex = queryEditor.indexFromPos(queryEditor.getCursor()); + + // Loop through all operations to see if one contains the cursor. + let operationName: string | undefined; + for (const operation of queryEditor.operations) { + if ( + operation.loc && + operation.loc.start <= cursorIndex && + operation.loc.end >= cursorIndex + ) { + operationName = operation.name?.value; + } + } + + if (operationName && operationName !== queryEditor.operationName) { + setOperationName(operationName); + } + + run(); + }, [queryEditor, run, setOperationName]); + + useKeyMap(queryEditor, ['Cmd-Enter', 'Ctrl-Enter'], runAtCursor); + useKeyMap(queryEditor, ['Shift-Ctrl-C'], copy); + useKeyMap( + queryEditor, + [ + 'Shift-Ctrl-P', + // Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to prettify + 'Shift-Ctrl-F', + ], + prettify, + ); + useKeyMap(queryEditor, ['Shift-Ctrl-M'], merge); + + return ref; +} + +function useSynchronizeSchema( + editor: CodeMirrorEditor | null, + schema: GraphQLSchema | null, + codeMirrorRef: MutableRefObject, +) { + useEffect(() => { + if (!editor) { + return; + } + + const didChange = editor.options.lint.schema !== schema; + + editor.state.lint.linterOptions.schema = schema; + editor.options.lint.schema = schema; + editor.options.hintOptions.schema = schema; + editor.options.info.schema = schema; + editor.options.jump.schema = schema; + + if (didChange && codeMirrorRef.current) { + codeMirrorRef.current.signal(editor, 'change', editor); + } + }, [editor, schema, codeMirrorRef]); +} + +function useSynchronizeValidationRules( + editor: CodeMirrorEditor | null, + validationRules: ValidationRule[] | null, + codeMirrorRef: MutableRefObject, +) { + useEffect(() => { + if (!editor) { + return; + } + + const didChange = editor.options.lint.validationRules !== validationRules; + + editor.state.lint.linterOptions.validationRules = validationRules; + editor.options.lint.validationRules = validationRules; + + if (didChange && codeMirrorRef.current) { + codeMirrorRef.current.signal(editor, 'change', editor); + } + }, [editor, validationRules, codeMirrorRef]); +} + +function useSynchronizeExternalFragments( + editor: CodeMirrorEditor | null, + externalFragments: Map, + codeMirrorRef: MutableRefObject, +) { + const externalFragmentList = useMemo( + () => [...externalFragments.values()], + [externalFragments], + ); + + useEffect(() => { + if (!editor) { + return; + } + + const didChange = + editor.options.lint.externalFragments !== externalFragmentList; + + editor.state.lint.linterOptions.externalFragments = externalFragmentList; + editor.options.lint.externalFragments = externalFragmentList; + editor.options.hintOptions.externalFragments = externalFragmentList; + + if (didChange && codeMirrorRef.current) { + codeMirrorRef.current.signal(editor, 'change', editor); + } + }, [editor, externalFragmentList, codeMirrorRef]); +} + +const AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/; + +export const STORAGE_KEY_QUERY = 'query'; + +const STORAGE_KEY_OPERATION_NAME = 'operationName'; diff --git a/packages/graphiql-react/src/editor/response-editor.tsx b/packages/graphiql-react/src/editor/response-editor.tsx new file mode 100644 index 00000000000..c7b7cb31b75 --- /dev/null +++ b/packages/graphiql-react/src/editor/response-editor.tsx @@ -0,0 +1,154 @@ +import { formatError } from '@graphiql/toolkit'; +import type { Position, Token } from 'codemirror'; +import { ComponentType, useEffect, useRef } from 'react'; +import ReactDOM from 'react-dom'; +import { useSchemaContext } from '../schema'; + +import { + commonKeys, + DEFAULT_EDITOR_THEME, + DEFAULT_KEY_MAP, + importCodeMirror, +} from './common'; +import { ImagePreview } from './components'; +import { useEditorContext } from './context'; +import { useSynchronizeOption } from './hooks'; +import { CodeMirrorEditor, CommonEditorProps } from './types'; + +export type ResponseTooltipType = ComponentType<{ + /** + * The position of the token in the editor contents. + */ + pos: Position; + /** + * The token that has been hovered over. + */ + token: Token; +}>; + +export type UseResponseEditorArgs = CommonEditorProps & { + /** + * Customize the tooltip when hovering over properties in the response + * editor. + */ + responseTooltip?: ResponseTooltipType; +}; + +export function useResponseEditor( + { + responseTooltip, + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + }: UseResponseEditorArgs = {}, + caller?: Function, +) { + const { fetchError, validationErrors } = useSchemaContext({ + nonNull: true, + caller: caller || useResponseEditor, + }); + const { initialResponse, responseEditor, setResponseEditor } = + useEditorContext({ + nonNull: true, + caller: caller || useResponseEditor, + }); + const ref = useRef(null); + + const responseTooltipRef = useRef( + responseTooltip, + ); + useEffect(() => { + responseTooltipRef.current = responseTooltip; + }, [responseTooltip]); + + useEffect(() => { + let isActive = true; + void importCodeMirror( + [ + import('codemirror/addon/fold/foldgutter'), + import('codemirror/addon/fold/brace-fold'), + import('codemirror/addon/dialog/dialog'), + import('codemirror/addon/search/search'), + import('codemirror/addon/search/searchcursor'), + import('codemirror/addon/search/jump-to-line'), + // @ts-expect-error + import('codemirror/keymap/sublime'), + import('codemirror-graphql/esm/results/mode'), + import('codemirror-graphql/esm/utils/info-addon'), + ], + { useCommonAddons: false }, + ).then(CodeMirror => { + // Don't continue if the effect has already been cleaned up + if (!isActive) { + return; + } + + // Handle image tooltips and custom tooltips + const tooltipDiv = document.createElement('div'); + CodeMirror.registerHelper( + 'info', + 'graphql-results', + (token: Token, _options: any, _cm: CodeMirrorEditor, pos: Position) => { + const infoElements: JSX.Element[] = []; + + const ResponseTooltipComponent = responseTooltipRef.current; + if (ResponseTooltipComponent) { + infoElements.push( + , + ); + } + + if (ImagePreview.shouldRender(token)) { + infoElements.push( + , + ); + } + + // We can't refactor to root.unmount() from React 18 because we support React 16/17 too + if (!infoElements.length) { + ReactDOM.unmountComponentAtNode(tooltipDiv); + return null; + } + ReactDOM.render(infoElements, tooltipDiv); + return tooltipDiv; + }, + ); + + const container = ref.current; + if (!container) { + return; + } + + const newEditor = CodeMirror(container, { + value: initialResponse, + lineWrapping: true, + readOnly: true, + theme: editorTheme, + mode: 'graphql-results', + foldGutter: true, + gutters: ['CodeMirror-foldgutter'], + // @ts-expect-error + info: true, + extraKeys: commonKeys, + }); + + setResponseEditor(newEditor); + }); + + return () => { + isActive = false; + }; + }, [editorTheme, initialResponse, setResponseEditor]); + + useSynchronizeOption(responseEditor, 'keyMap', keyMap); + + useEffect(() => { + if (fetchError) { + responseEditor?.setValue(fetchError); + } + if (validationErrors.length > 0) { + responseEditor?.setValue(formatError(validationErrors)); + } + }, [responseEditor, fetchError, validationErrors]); + + return ref; +} diff --git a/packages/graphiql-react/src/editor/style/auto-insertion.css b/packages/graphiql-react/src/editor/style/auto-insertion.css new file mode 100644 index 00000000000..82c86f0b395 --- /dev/null +++ b/packages/graphiql-react/src/editor/style/auto-insertion.css @@ -0,0 +1,18 @@ +.auto-inserted-leaf.cm-property { + animation-duration: 6s; + animation-name: insertionFade; + border-radius: var(--border-radius-4); + padding: var(--px-2); +} + +@keyframes insertionFade { + from, + to { + background-color: none; + } + + 15%, + 85% { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + } +} diff --git a/packages/graphiql-react/src/editor/style/codemirror.css b/packages/graphiql-react/src/editor/style/codemirror.css new file mode 100644 index 00000000000..e1911ab2711 --- /dev/null +++ b/packages/graphiql-react/src/editor/style/codemirror.css @@ -0,0 +1,180 @@ +@import url('codemirror/lib/codemirror.css'); + +/* Make the editors fill up their container and make them scrollable */ +.graphiql-container .CodeMirror { + height: 100%; + position: absolute; + width: 100%; +} + +/* Override font settings */ +.graphiql-container .CodeMirror { + font-family: var(--font-family-mono); +} + +/* Set default background color */ +.graphiql-container .CodeMirror, +.graphiql-container .CodeMirror-gutters { + background: none; + background-color: var(--editor-background, hsl(var(--color-base))); +} + +/* No padding around line numbers */ +.graphiql-container .CodeMirror-linenumber { + padding: 0; +} + +/* No border between gutter and editor */ +.graphiql-container .CodeMirror-gutters { + border: none; +} + +/** + * Editor theme + */ + +.cm-s-graphiql { + /* Default to punctuation */ + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + + /* OperationType, `fragment`, `on` */ + & .cm-keyword { + color: hsl(var(--color-primary)); + } + /* Name (OperationDefinition), FragmentName */ + & .cm-def { + color: hsl(var(--color-tertiary)); + } + /* Punctuator (except `$` and `@`) */ + & .cm-punctuation { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + } + /* Variable */ + & .cm-variable { + color: hsl(var(--color-secondary)); + } + /* NamedType */ + & .cm-atom { + color: hsl(var(--color-tertiary)); + } + /* IntValue, FloatValue */ + & .cm-number { + color: hsl(var(--color-success)); + } + /* StringValue */ + & .cm-string { + color: hsl(var(--color-warning)); + } + /* BooleanValue */ + & .cm-builtin { + color: hsl(var(--color-success)); + } + /* EnumValue */ + & .cm-string-2 { + color: hsl(var(--color-secondary)); + } + /* Name (ObjectField, Argument) */ + & .cm-attribute { + color: hsl(var(--color-tertiary)); + } + /* Name (Directive) */ + & .cm-meta { + color: hsl(var(--color-tertiary)); + } + /* Name (Alias, Field without Alias) */ + & .cm-property { + color: hsl(var(--color-info)); + } + /* Name (Field with Alias) */ + & .cm-qualifier { + color: hsl(var(--color-secondary)); + } + /* Comment */ + & .cm-comment { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + } + /* Whitespace */ + & .cm-ws { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + } + /* Invalid characters */ + & .cm-invalidchar { + color: hsl(var(--color-error)); + } + + /* Cursor */ + & .CodeMirror-cursor { + border-left: 2px solid hsla(var(--color-neutral), var(--alpha-secondary)); + } + + /* Color for line numbers and fold-gutters */ + & .CodeMirror-linenumber { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + } +} + +/* Matching bracket colors */ +.graphiql-container div.CodeMirror span.CodeMirror-matchingbracket, +.graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket { + color: hsl(var(--color-warning)); +} + +/* Selected text blocks */ +.graphiql-container .CodeMirror-selected, +.graphiql-container .CodeMirror-focused .CodeMirror-selected { + background: hsla(var(--color-neutral), var(--alpha-background-heavy)); +} + +/* Position the search dialog */ +.graphiql-container .CodeMirror-dialog { + background: inherit; + color: inherit; + left: 0; + right: 0; + overflow: hidden; + padding: var(--px-2) var(--px-6); + position: absolute; + z-index: 6; +} +.graphiql-container .CodeMirror-dialog-top { + border-bottom: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding-bottom: var(--px-12); + top: 0; +} +.graphiql-container .CodeMirror-dialog-bottom { + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + bottom: 0; + padding-top: var(--px-12); +} + +/* Hide the search hint */ +.graphiql-container .CodeMirror-search-hint { + display: none; +} + +/* Style the input field for searching */ +.graphiql-container .CodeMirror-dialog input { + border: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-radius: var(--border-radius-4); + padding: var(--px-4); +} +.graphiql-container .CodeMirror-dialog input:focus { + outline: hsl(var(--color-primary)) solid 2px; +} + +/* Set the highlight color for search results */ +.graphiql-container .cm-searching { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + /** + * When cycling through search results, CodeMirror overlays the current + * selection with another element that has the .CodeMirror-selected class + * applied. This adds another background color (see above), but this extra + * box does not quite match the height of this element. To match them up we + * add some extra padding here. (Note that this doesn't affect the line + * height of the CodeMirror editor as all line wrappers have a fixed height.) + */ + padding-bottom: 1.5px; + padding-top: 0.5px; +} diff --git a/packages/graphiql-react/src/editor/style/editor.css b/packages/graphiql-react/src/editor/style/editor.css new file mode 100644 index 00000000000..c9d0767d20c --- /dev/null +++ b/packages/graphiql-react/src/editor/style/editor.css @@ -0,0 +1,13 @@ +.graphiql-editor { + height: 100%; + position: relative; + width: 100%; + + &.hidden { + /* Just setting `display: none;` would break the editor gutters */ + left: -9999px; + position: absolute; + top: -9999px; + visibility: hidden; + } +} diff --git a/packages/graphiql-react/src/editor/style/fold.css b/packages/graphiql-react/src/editor/style/fold.css new file mode 100644 index 00000000000..0ca7d6703ab --- /dev/null +++ b/packages/graphiql-react/src/editor/style/fold.css @@ -0,0 +1,24 @@ +@import url('codemirror/addon/fold/foldgutter.css'); + +.CodeMirror-foldgutter { + width: var(--px-12); +} + +.CodeMirror-foldmarker { + background-color: hsl(var(--color-info)); + border-radius: var(--border-radius-4); + color: hsl(var(--color-base)); + font-family: inherit; + margin: 0 var(--px-4); + padding: 0 var(--px-8); + text-shadow: none; +} + +.CodeMirror-foldgutter-open, +.CodeMirror-foldgutter-folded { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + + &::after { + margin: 0 var(--px-2); + } +} diff --git a/packages/graphiql-react/src/editor/style/hint.css b/packages/graphiql-react/src/editor/style/hint.css new file mode 100644 index 00000000000..85e89336b49 --- /dev/null +++ b/packages/graphiql-react/src/editor/style/hint.css @@ -0,0 +1,73 @@ +@import url('codemirror/addon/hint/show-hint.css'); + +/* Popup styles */ +.CodeMirror-hints { + background: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow); + display: grid; + font-family: var(--font-family); + font-size: var(--font-size-body); + grid-template-columns: auto fit-content(300px); + /* By default this is equals exactly 8 items including margins */ + max-height: 264px; + padding: 0; +} + +/* Autocomplete items */ +.CodeMirror-hint { + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + grid-column: 1 / 2; + margin: var(--px-4); + /* Override element style added by codemirror */ + padding: var(--px-6) var(--px-8) !important; + + &:not(:first-child) { + margin-top: 0; + } +} +li.CodeMirror-hint-active { + background: hsla(var(--color-primary), var(--alpha-background-medium)); + color: hsl(var(--color-primary)); +} + +/* Sidebar with additional information */ +.CodeMirror-hint-information { + border-left: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + grid-column: 2 / 3; + grid-row: 1 / 99999; + /* Same as the popup */ + max-height: 264px; + overflow: auto; + padding: var(--px-12); +} +.CodeMirror-hint-information-header { + display: flex; + align-items: baseline; +} +.CodeMirror-hint-information-field-name { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +.CodeMirror-hint-information-type-name-pill { + border: 1px solid hsla(var(--color-neutral), var(--alpha-tertiary)); + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-left: var(--px-6); + padding: var(--px-4); +} +.CodeMirror-hint-information-type-name { + color: inherit; + text-decoration: none; + + &:hover { + text-decoration: underline dotted; + } +} +.CodeMirror-hint-information-description { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-top: var(--px-12); +} diff --git a/packages/graphiql-react/src/editor/style/info.css b/packages/graphiql-react/src/editor/style/info.css new file mode 100644 index 00000000000..82a8414326e --- /dev/null +++ b/packages/graphiql-react/src/editor/style/info.css @@ -0,0 +1,60 @@ +/* Popup styles */ +.CodeMirror-info { + background-color: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow); + color: hsla(var(--color-neutral), 1); + max-height: 300px; + max-width: 400px; + opacity: 0; + overflow: auto; + padding: var(--px-12); + position: fixed; + transition: opacity 0.15s; + z-index: 10; + + /* Link styles */ + & a { + color: inherit; + text-decoration: none; + + &:hover { + text-decoration: underline dotted; + } + } + + /* Align elements in header */ + & .CodeMirror-info-header { + display: flex; + align-items: baseline; + } + + /* Main elements */ + & .CodeMirror-info-header { + & > .type-name, + & > .field-name, + & > .arg-name, + & > .directive-name, + & > .enum-value { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); + } + } + + /* Type names */ + & .type-name-pill { + border: 1px solid hsla(var(--color-neutral), var(--alpha-tertiary)); + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-left: var(--px-6); + padding: var(--px-4); + } + + /* Descriptions */ + & .info-description { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-top: var(--px-12); + overflow: hidden; + } +} diff --git a/packages/graphiql-react/src/editor/style/jump.css b/packages/graphiql-react/src/editor/style/jump.css new file mode 100644 index 00000000000..7ba8e49c214 --- /dev/null +++ b/packages/graphiql-react/src/editor/style/jump.css @@ -0,0 +1,5 @@ +/* Underline the clickable token */ +.CodeMirror-jump-token { + text-decoration: underline dotted; + cursor: pointer; +} diff --git a/packages/graphiql-react/src/editor/style/lint.css b/packages/graphiql-react/src/editor/style/lint.css new file mode 100644 index 00000000000..7dc8a064bfc --- /dev/null +++ b/packages/graphiql-react/src/editor/style/lint.css @@ -0,0 +1,95 @@ +@import url('codemirror/addon/lint/lint.css'); + +/* Text styles */ +.CodeMirror-lint-mark-error, +.CodeMirror-lint-mark-warning { + background-repeat: repeat-x; + /** + * The following two are very specific to the font size, so we use + * "magic values" instead of variables. + */ + background-size: 10px 3px; + background-position: 0 95%; +} +.cm-s-graphiql .CodeMirror-lint-mark-error { + color: hsl(var(--color-error)); +} +.CodeMirror-lint-mark-error { + background-image: linear-gradient( + 45deg, + transparent 65%, + hsl(var(--color-error)) 80%, + transparent 90% + ), + linear-gradient( + 135deg, + transparent 5%, + hsl(var(--color-error)) 15%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 45%, + hsl(var(--color-error)) 55%, + transparent 65% + ), + linear-gradient( + 45deg, + transparent 25%, + hsl(var(--color-error)) 35%, + transparent 50% + ); +} +.cm-s-graphiql .CodeMirror-lint-mark-warning { + color: hsl(var(--color-warning)); +} +.CodeMirror-lint-mark-warning { + background-image: linear-gradient( + 45deg, + transparent 65%, + hsl(var(--color-warning)) 80%, + transparent 90% + ), + linear-gradient( + 135deg, + transparent 5%, + hsl(var(--color-warning)) 15%, + transparent 25% + ), + linear-gradient( + 135deg, + transparent 45%, + hsl(var(--color-warning)) 55%, + transparent 65% + ), + linear-gradient( + 45deg, + transparent 25%, + hsl(var(--color-warning)) 35%, + transparent 50% + ); +} + +/* Popup styles */ +.CodeMirror-lint-tooltip { + background-color: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow); + font-size: var(--font-size-body); + font-family: var(--font-family); + max-width: 600px; + overflow: hidden; + padding: var(--px-12); +} +.CodeMirror-lint-message-error, +.CodeMirror-lint-message-warning { + background-image: none; + padding: 0; +} +.CodeMirror-lint-message-error { + color: hsl(var(--color-error)); +} +.CodeMirror-lint-message-warning { + color: hsl(var(--color-warning)); +} diff --git a/packages/graphiql-react/src/editor/tabs.ts b/packages/graphiql-react/src/editor/tabs.ts new file mode 100644 index 00000000000..7622cb0cfc1 --- /dev/null +++ b/packages/graphiql-react/src/editor/tabs.ts @@ -0,0 +1,362 @@ +import { StorageAPI } from '@graphiql/toolkit'; +import { useCallback, useMemo } from 'react'; + +import debounce from '../utility/debounce'; +import { CodeMirrorEditorWithOperationFacts } from './context'; +import { CodeMirrorEditor } from './types'; + +export type TabDefinition = { + /** + * The contents of the query editor of this tab. + */ + query: string | null; + /** + * The contents of the variable editor of this tab. + */ + variables?: string | null; + /** + * The contents of the headers editor of this tab. + */ + headers?: string | null; +}; + +/** + * This object describes the state of a single tab. + */ +export type TabState = TabDefinition & { + /** + * A GUID value generated when the tab was created. + */ + id: string; + /** + * A hash that is unique for a combination of the contents of the query + * editor, the variable editor and the header editor (i.e. all the editor + * where the contents are persisted in storage). + */ + hash: string; + /** + * The title of the tab shown in the tab element. + */ + title: string; + /** + * The operation name derived from the contents of the query editor of this + * tab. + */ + operationName: string | null; + /** + * The contents of the response editor of this tab. + */ + response: string | null; +}; + +/** + * This object describes the state of all tabs. + */ +export type TabsState = { + /** + * A list of state objects for each tab. + */ + tabs: TabState[]; + /** + * The index of the currently active tab with regards to the `tabs` list of + * this object. + */ + activeTabIndex: number; +}; + +export function getDefaultTabState({ + defaultQuery, + defaultHeaders, + headers, + defaultTabs, + query, + variables, + storage, +}: { + defaultQuery: string; + defaultHeaders?: string; + headers: string | null; + defaultTabs?: TabDefinition[]; + query: string | null; + variables: string | null; + storage: StorageAPI | null; +}) { + const storedState = storage?.get(STORAGE_KEY); + try { + if (!storedState) { + throw new Error('Storage for tabs is empty'); + } + const parsed = JSON.parse(storedState); + if (isTabsState(parsed)) { + const expectedHash = hashFromTabContents({ query, variables, headers }); + let matchingTabIndex = -1; + + for (let index = 0; index < parsed.tabs.length; index++) { + const tab = parsed.tabs[index]; + tab.hash = hashFromTabContents({ + query: tab.query, + variables: tab.variables, + headers: tab.headers, + }); + if (tab.hash === expectedHash) { + matchingTabIndex = index; + } + } + + if (matchingTabIndex >= 0) { + parsed.activeTabIndex = matchingTabIndex; + } else { + const operationName = query ? fuzzyExtractOperationName(query) : null; + parsed.tabs.push({ + id: guid(), + hash: expectedHash, + title: operationName || DEFAULT_TITLE, + query, + variables, + headers, + operationName, + response: null, + }); + parsed.activeTabIndex = parsed.tabs.length - 1; + } + + return parsed; + } + throw new Error('Storage for tabs is invalid'); + } catch { + return { + activeTabIndex: 0, + tabs: ( + defaultTabs || [ + { + query: query ?? defaultQuery, + variables, + headers: headers ?? defaultHeaders, + }, + ] + ).map(createTab), + }; + } +} + +function isTabsState(obj: any): obj is TabsState { + return ( + obj && + typeof obj === 'object' && + !Array.isArray(obj) && + hasNumberKey(obj, 'activeTabIndex') && + 'tabs' in obj && + Array.isArray(obj.tabs) && + obj.tabs.every(isTabState) + ); +} + +function isTabState(obj: any): obj is TabState { + // We don't persist the hash, so we skip the check here + return ( + obj && + typeof obj === 'object' && + !Array.isArray(obj) && + hasStringKey(obj, 'id') && + hasStringKey(obj, 'title') && + hasStringOrNullKey(obj, 'query') && + hasStringOrNullKey(obj, 'variables') && + hasStringOrNullKey(obj, 'headers') && + hasStringOrNullKey(obj, 'operationName') && + hasStringOrNullKey(obj, 'response') + ); +} + +function hasNumberKey(obj: Record, key: string) { + return key in obj && typeof obj[key] === 'number'; +} + +function hasStringKey(obj: Record, key: string) { + return key in obj && typeof obj[key] === 'string'; +} + +function hasStringOrNullKey(obj: Record, key: string) { + return key in obj && (typeof obj[key] === 'string' || obj[key] === null); +} + +export function useSynchronizeActiveTabValues({ + queryEditor, + variableEditor, + headerEditor, + responseEditor, +}: { + queryEditor: CodeMirrorEditorWithOperationFacts | null; + variableEditor: CodeMirrorEditor | null; + headerEditor: CodeMirrorEditor | null; + responseEditor: CodeMirrorEditor | null; +}) { + return useCallback<(state: TabsState) => TabsState>( + state => { + const query = queryEditor?.getValue() ?? null; + const variables = variableEditor?.getValue() ?? null; + const headers = headerEditor?.getValue() ?? null; + const operationName = queryEditor?.operationName ?? null; + const response = responseEditor?.getValue() ?? null; + return setPropertiesInActiveTab(state, { + query, + variables, + headers, + response, + operationName, + }); + }, + [queryEditor, variableEditor, headerEditor, responseEditor], + ); +} + +export function serializeTabState( + tabState: TabsState, + shouldPersistHeaders = false, +) { + return JSON.stringify(tabState, (key, value) => + key === 'hash' || + key === 'response' || + (!shouldPersistHeaders && key === 'headers') + ? null + : value, + ); +} + +export function useStoreTabs({ + storage, + shouldPersistHeaders, +}: { + storage: StorageAPI | null; + shouldPersistHeaders?: boolean; +}) { + const store = useMemo( + () => + debounce(500, (value: string) => { + storage?.set(STORAGE_KEY, value); + }), + [storage], + ); + return useCallback( + (currentState: TabsState) => { + store(serializeTabState(currentState, shouldPersistHeaders)); + }, + [shouldPersistHeaders, store], + ); +} + +export function useSetEditorValues({ + queryEditor, + variableEditor, + headerEditor, + responseEditor, +}: { + queryEditor: CodeMirrorEditorWithOperationFacts | null; + variableEditor: CodeMirrorEditor | null; + headerEditor: CodeMirrorEditor | null; + responseEditor: CodeMirrorEditor | null; +}) { + return useCallback( + ({ + query, + variables, + headers, + response, + }: { + query: string | null; + variables?: string | null; + headers?: string | null; + response: string | null; + }) => { + queryEditor?.setValue(query ?? ''); + variableEditor?.setValue(variables ?? ''); + headerEditor?.setValue(headers ?? ''); + responseEditor?.setValue(response ?? ''); + }, + [headerEditor, queryEditor, responseEditor, variableEditor], + ); +} + +export function createTab({ + query = null, + variables = null, + headers = null, +}: Partial = {}): TabState { + return { + id: guid(), + hash: hashFromTabContents({ query, variables, headers }), + title: (query && fuzzyExtractOperationName(query)) || DEFAULT_TITLE, + query, + variables, + headers, + operationName: null, + response: null, + }; +} + +export function setPropertiesInActiveTab( + state: TabsState, + partialTab: Partial>, +): TabsState { + return { + ...state, + tabs: state.tabs.map((tab, index) => { + if (index !== state.activeTabIndex) { + return tab; + } + const newTab = { ...tab, ...partialTab }; + return { + ...newTab, + hash: hashFromTabContents(newTab), + title: + newTab.operationName || + (newTab.query + ? fuzzyExtractOperationName(newTab.query) + : undefined) || + DEFAULT_TITLE, + }; + }), + }; +} + +function guid(): string { + const s4 = () => { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .slice(1); + }; + // return id of format 'aaaaaaaa'-'aaaa'-'aaaa'-'aaaa'-'aaaaaaaaaaaa' + return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; +} + +function hashFromTabContents(args: { + query: string | null; + variables?: string | null; + headers?: string | null; +}): string { + return [args.query ?? '', args.variables ?? '', args.headers ?? ''].join('|'); +} + +export function fuzzyExtractOperationName(str: string): string | null { + const regex = /^(?!#).*(query|subscription|mutation)\s+([a-zA-Z0-9_]+)/m; + + const match = regex.exec(str); + + return match?.[2] ?? null; +} + +export function clearHeadersFromTabs(storage: StorageAPI | null) { + const persistedTabs = storage?.get(STORAGE_KEY); + if (persistedTabs) { + const parsedTabs = JSON.parse(persistedTabs); + storage?.set( + STORAGE_KEY, + JSON.stringify(parsedTabs, (key, value) => + key === 'headers' ? null : value, + ), + ); + } +} + +const DEFAULT_TITLE = ''; + +export const STORAGE_KEY = 'tabState'; diff --git a/packages/graphiql-react/src/editor/types.ts b/packages/graphiql-react/src/editor/types.ts new file mode 100644 index 00000000000..3fca9f0d936 --- /dev/null +++ b/packages/graphiql-react/src/editor/types.ts @@ -0,0 +1,29 @@ +import type { Editor } from 'codemirror'; + +export type CodeMirrorType = typeof import('codemirror'); + +export type CodeMirrorEditor = Editor & { options?: any }; + +export type KeyMap = 'sublime' | 'emacs' | 'vim'; + +export type CommonEditorProps = { + /** + * Sets the color theme you want to use for the editor. + * @default 'graphiql' + */ + editorTheme?: string; + /** + * Sets the key map to use when using the editor. + * @default 'sublime' + * @see {@link https://codemirror.net/5/doc/manual.html#keymaps} + */ + keyMap?: KeyMap; +}; + +export type WriteableEditorProps = CommonEditorProps & { + /** + * Makes the editor read-only. + * @default false + */ + readOnly?: boolean; +}; diff --git a/packages/graphiql-react/src/editor/variable-editor.ts b/packages/graphiql-react/src/editor/variable-editor.ts new file mode 100644 index 00000000000..2213c383e27 --- /dev/null +++ b/packages/graphiql-react/src/editor/variable-editor.ts @@ -0,0 +1,154 @@ +import type { SchemaReference } from 'codemirror-graphql/utils/SchemaReference'; +import { useEffect, useRef } from 'react'; + +import { useExecutionContext } from '../execution'; +import { + commonKeys, + DEFAULT_EDITOR_THEME, + DEFAULT_KEY_MAP, + importCodeMirror, +} from './common'; +import { useEditorContext } from './context'; +import { + useChangeHandler, + useCompletion, + useKeyMap, + useMergeQuery, + usePrettifyEditors, + useSynchronizeOption, +} from './hooks'; +import { CodeMirrorType, WriteableEditorProps } from './types'; + +export type UseVariableEditorArgs = WriteableEditorProps & { + /** + * Invoked when a reference to the GraphQL schema (type or field) is clicked + * as part of the editor or one of its tooltips. + * @param reference The reference that has been clicked. + */ + onClickReference?(reference: SchemaReference): void; + /** + * Invoked when the contents of the variables editor change. + * @param value The new contents of the editor. + */ + onEdit?(value: string): void; +}; + +export function useVariableEditor( + { + editorTheme = DEFAULT_EDITOR_THEME, + keyMap = DEFAULT_KEY_MAP, + onClickReference, + onEdit, + readOnly = false, + }: UseVariableEditorArgs = {}, + caller?: Function, +) { + const { initialVariables, variableEditor, setVariableEditor } = + useEditorContext({ + nonNull: true, + caller: caller || useVariableEditor, + }); + const executionContext = useExecutionContext(); + const merge = useMergeQuery({ caller: caller || useVariableEditor }); + const prettify = usePrettifyEditors({ caller: caller || useVariableEditor }); + const ref = useRef(null); + const codeMirrorRef = useRef(); + + useEffect(() => { + let isActive = true; + + void importCodeMirror([ + import('codemirror-graphql/esm/variables/hint'), + import('codemirror-graphql/esm/variables/lint'), + import('codemirror-graphql/esm/variables/mode'), + ]).then(CodeMirror => { + // Don't continue if the effect has already been cleaned up + if (!isActive) { + return; + } + + codeMirrorRef.current = CodeMirror; + + const container = ref.current; + if (!container) { + return; + } + + const newEditor = CodeMirror(container, { + value: initialVariables, + lineNumbers: true, + tabSize: 2, + mode: 'graphql-variables', + theme: editorTheme, + autoCloseBrackets: true, + matchBrackets: true, + showCursorWhenSelecting: true, + readOnly: readOnly ? 'nocursor' : false, + foldGutter: true, + lint: { + // @ts-expect-error + variableToType: undefined, + }, + hintOptions: { + closeOnUnfocus: false, + completeSingle: false, + container, + // @ts-expect-error + variableToType: undefined, + }, + gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], + extraKeys: commonKeys, + }); + + newEditor.addKeyMap({ + 'Cmd-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Ctrl-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Alt-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + 'Shift-Space'() { + newEditor.showHint({ completeSingle: false, container }); + }, + }); + + newEditor.on('keyup', (editorInstance, event) => { + const { code, key, shiftKey } = event; + const isLetter = code.startsWith('Key'); + const isNumber = !shiftKey && code.startsWith('Digit'); + if (isLetter || isNumber || key === '_' || key === '"') { + editorInstance.execCommand('autocomplete'); + } + }); + + setVariableEditor(newEditor); + }); + + return () => { + isActive = false; + }; + }, [editorTheme, initialVariables, readOnly, setVariableEditor]); + + useSynchronizeOption(variableEditor, 'keyMap', keyMap); + + useChangeHandler( + variableEditor, + onEdit, + STORAGE_KEY, + 'variables', + useVariableEditor, + ); + + useCompletion(variableEditor, onClickReference || null, useVariableEditor); + + useKeyMap(variableEditor, ['Cmd-Enter', 'Ctrl-Enter'], executionContext?.run); + useKeyMap(variableEditor, ['Shift-Ctrl-P'], prettify); + useKeyMap(variableEditor, ['Shift-Ctrl-M'], merge); + + return ref; +} + +export const STORAGE_KEY = 'variables'; diff --git a/packages/graphiql-react/src/editor/whitespace.ts b/packages/graphiql-react/src/editor/whitespace.ts new file mode 100644 index 00000000000..e0c7d4e86c2 --- /dev/null +++ b/packages/graphiql-react/src/editor/whitespace.ts @@ -0,0 +1,11 @@ +// Unicode whitespace characters that break the interface. +export const invalidCharacters = Array.from({ length: 11 }, (_, i) => { + // \u2000 -> \u200a + return String.fromCharCode(0x2000 + i); +}).concat(['\u2028', '\u2029', '\u202f', '\u00a0']); + +const sanitizeRegex = new RegExp('[' + invalidCharacters.join('') + ']', 'g'); + +export function normalizeWhitespace(line: string) { + return line.replace(sanitizeRegex, ' '); +} diff --git a/packages/graphiql-react/src/execution.tsx b/packages/graphiql-react/src/execution.tsx new file mode 100644 index 00000000000..d66b4eea78b --- /dev/null +++ b/packages/graphiql-react/src/execution.tsx @@ -0,0 +1,363 @@ +import { + Fetcher, + FetcherResultPayload, + formatError, + formatResult, + isAsyncIterable, + isObservable, + Unsubscribable, +} from '@graphiql/toolkit'; +import { ExecutionResult, FragmentDefinitionNode, print } from 'graphql'; +import { getFragmentDependenciesForAST } from 'graphql-language-service'; +import { ReactNode, useCallback, useMemo, useRef, useState } from 'react'; +import setValue from 'set-value'; + +import { useAutoCompleteLeafs, useEditorContext } from './editor'; +import { UseAutoCompleteLeafsArgs } from './editor/hooks'; +import { useHistoryContext } from './history'; +import { createContextHook, createNullableContext } from './utility/context'; + +export type ExecutionContextType = { + /** + * If there is currently a GraphQL request in-flight. For multi-part + * requests like subscriptions, this will be `true` while fetching the + * first partial response and `false` while fetching subsequent batches. + */ + isFetching: boolean; + /** + * If there is currently a GraphQL request in-flight. For multi-part + * requests like subscriptions, this will be `true` until the last batch + * has been fetched or the connection is closed from the client. + */ + isSubscribed: boolean; + /** + * The operation name that will be sent with all GraphQL requests. + */ + operationName: string | null; + /** + * Start a GraphQL requests based of the current editor contents. + */ + run(): void; + /** + * Stop the GraphQL request that is currently in-flight. + */ + stop(): void; +}; + +export const ExecutionContext = + createNullableContext('ExecutionContext'); + +export type ExecutionContextProviderProps = Pick< + UseAutoCompleteLeafsArgs, + 'getDefaultFieldNames' +> & { + children: ReactNode; + /** + * A function which accepts GraphQL HTTP parameters and returns a `Promise`, + * `Observable` or `AsyncIterable` that returns the GraphQL response in + * parsed JSON format. + * + * We suggest using the `createGraphiQLFetcher` utility from `@graphiql/toolkit` + * to create these fetcher functions. + * + * @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#creategraphiqlfetcher-2|`createGraphiQLFetcher`} + */ + fetcher: Fetcher; + /** + * This prop sets the operation name that is passed with a GraphQL request. + */ + operationName?: string; +}; + +export function ExecutionContextProvider({ + fetcher, + getDefaultFieldNames, + children, + operationName, +}: ExecutionContextProviderProps) { + if (!fetcher) { + throw new TypeError( + 'The `ExecutionContextProvider` component requires a `fetcher` function to be passed as prop.', + ); + } + + const { + externalFragments, + headerEditor, + queryEditor, + responseEditor, + variableEditor, + updateActiveTabValues, + } = useEditorContext({ nonNull: true, caller: ExecutionContextProvider }); + const history = useHistoryContext(); + const autoCompleteLeafs = useAutoCompleteLeafs({ + getDefaultFieldNames, + caller: ExecutionContextProvider, + }); + const [isFetching, setIsFetching] = useState(false); + const [subscription, setSubscription] = useState(null); + const queryIdRef = useRef(0); + + const stop = useCallback(() => { + subscription?.unsubscribe(); + setIsFetching(false); + setSubscription(null); + }, [subscription]); + + const run = useCallback(async () => { + if (!queryEditor || !responseEditor) { + return; + } + + // If there's an active subscription, unsubscribe it and return + if (subscription) { + stop(); + return; + } + + const setResponse = (value: string) => { + responseEditor.setValue(value); + updateActiveTabValues({ response: value }); + }; + + queryIdRef.current += 1; + const queryId = queryIdRef.current; + + // Use the edited query after autoCompleteLeafs() runs or, + // in case autoCompletion fails (the function returns undefined), + // the current query from the editor. + let query = autoCompleteLeafs() || queryEditor.getValue(); + + const variablesString = variableEditor?.getValue(); + let variables: Record | undefined; + try { + variables = tryParseJsonObject({ + json: variablesString, + errorMessageParse: 'Variables are invalid JSON', + errorMessageType: 'Variables are not a JSON object.', + }); + } catch (error) { + setResponse(error instanceof Error ? error.message : `${error}`); + return; + } + + const headersString = headerEditor?.getValue(); + let headers: Record | undefined; + try { + headers = tryParseJsonObject({ + json: headersString, + errorMessageParse: 'Headers are invalid JSON', + errorMessageType: 'Headers are not a JSON object.', + }); + } catch (error) { + setResponse(error instanceof Error ? error.message : `${error}`); + return; + } + + if (externalFragments) { + const fragmentDependencies = queryEditor.documentAST + ? getFragmentDependenciesForAST( + queryEditor.documentAST, + externalFragments, + ) + : []; + if (fragmentDependencies.length > 0) { + query += + '\n' + + fragmentDependencies + .map((node: FragmentDefinitionNode) => print(node)) + .join('\n'); + } + } + + setResponse(''); + setIsFetching(true); + + const opName = operationName ?? queryEditor.operationName ?? undefined; + + history?.addToHistory({ + query, + variables: variablesString, + headers: headersString, + operationName: opName, + }); + + try { + let fullResponse: FetcherResultPayload = { data: {} }; + const handleResponse = (result: ExecutionResult) => { + // A different query was dispatched in the meantime, so don't + // show the results of this one. + if (queryId !== queryIdRef.current) { + return; + } + + let maybeMultipart = Array.isArray(result) ? result : false; + if ( + !maybeMultipart && + typeof result === 'object' && + result !== null && + 'hasNext' in result + ) { + maybeMultipart = [result]; + } + + if (maybeMultipart) { + const payload: FetcherResultPayload = { + data: fullResponse.data, + }; + const maybeErrors = [ + ...(fullResponse?.errors || []), + ...maybeMultipart.flatMap(i => i.errors).filter(Boolean), + ]; + + if (maybeErrors.length) { + payload.errors = maybeErrors; + } + + for (const part of maybeMultipart) { + // We pull out errors here, so we dont include it later + const { path, data, errors, ...rest } = part; + if (path) { + if (!data) { + throw new Error( + `Expected part to contain a data property, but got ${part}`, + ); + } + + setValue(payload.data, path, data, { merge: true }); + } else if (data) { + // If there is no path, we don't know what to do with the payload, + // so we just set it. + payload.data = data; + } + + // Ensures we also bring extensions and alike along for the ride + fullResponse = { + ...payload, + ...rest, + }; + } + + setIsFetching(false); + setResponse(formatResult(fullResponse)); + } else { + const response = formatResult(result); + setIsFetching(false); + setResponse(response); + } + }; + + const fetch = fetcher( + { + query, + variables, + operationName: opName, + }, + { + headers: headers ?? undefined, + documentAST: queryEditor.documentAST ?? undefined, + }, + ); + + const value = await Promise.resolve(fetch); + if (isObservable(value)) { + // If the fetcher returned an Observable, then subscribe to it, calling + // the callback on each next value, and handling both errors and the + // completion of the Observable. + setSubscription( + value.subscribe({ + next(result) { + handleResponse(result); + }, + error(error: Error) { + setIsFetching(false); + if (error) { + setResponse(formatError(error)); + } + setSubscription(null); + }, + complete() { + setIsFetching(false); + setSubscription(null); + }, + }), + ); + } else if (isAsyncIterable(value)) { + setSubscription({ + unsubscribe: () => value[Symbol.asyncIterator]().return?.(), + }); + for await (const result of value) { + handleResponse(result); + } + setIsFetching(false); + setSubscription(null); + } else { + handleResponse(value); + } + } catch (error) { + setIsFetching(false); + setResponse(formatError(error)); + setSubscription(null); + } + }, [ + autoCompleteLeafs, + externalFragments, + fetcher, + headerEditor, + history, + operationName, + queryEditor, + responseEditor, + stop, + subscription, + updateActiveTabValues, + variableEditor, + ]); + + const isSubscribed = Boolean(subscription); + const value = useMemo( + () => ({ + isFetching, + isSubscribed, + operationName: operationName ?? null, + run, + stop, + }), + [isFetching, isSubscribed, operationName, run, stop], + ); + + return ( + + {children} + + ); +} + +export const useExecutionContext = createContextHook(ExecutionContext); + +function tryParseJsonObject({ + json, + errorMessageParse, + errorMessageType, +}: { + json: string | undefined; + errorMessageParse: string; + errorMessageType: string; +}) { + let parsed: Record | undefined; + try { + parsed = json && json.trim() !== '' ? JSON.parse(json) : undefined; + } catch (error) { + throw new Error( + `${errorMessageParse}: ${ + error instanceof Error ? error.message : error + }.`, + ); + } + const isObject = + typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed); + if (parsed !== undefined && !isObject) { + throw new Error(errorMessageType); + } + return parsed; +} diff --git a/packages/graphiql-react/src/explorer/components/__tests__/doc-explorer.spec.tsx b/packages/graphiql-react/src/explorer/components/__tests__/doc-explorer.spec.tsx new file mode 100644 index 00000000000..2d8ee2dd1f4 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/__tests__/doc-explorer.spec.tsx @@ -0,0 +1,243 @@ +import { render } from '@testing-library/react'; +import { GraphQLInt, GraphQLObjectType, GraphQLSchema } from 'graphql'; +import { useContext, useEffect } from 'react'; + +import { SchemaContext, SchemaContextType } from '../../../schema'; +import { ExplorerContext, ExplorerContextProvider } from '../../context'; +import { DocExplorer } from '../doc-explorer'; + +function makeSchema(fieldName = 'field') { + return new GraphQLSchema({ + description: 'GraphQL Schema for testing', + query: new GraphQLObjectType({ + name: 'Query', + fields: { + [fieldName]: { + type: GraphQLInt, + args: { + arg: { + type: GraphQLInt, + }, + }, + }, + }, + }), + }); +} + +const defaultSchemaContext: SchemaContextType = { + fetchError: null, + introspect() {}, + isFetching: false, + schema: makeSchema(), + validationErrors: [], +}; + +const withErrorSchemaContext: SchemaContextType = { + fetchError: 'Error fetching schema', + introspect() {}, + isFetching: false, + schema: new GraphQLSchema({ description: 'GraphQL Schema for testing' }), + validationErrors: [], +}; + +function DocExplorerWithContext() { + return ( + + + + ); +} + +describe('DocExplorer', () => { + it('renders spinner when the schema is loading', () => { + const { container } = render( + + + , + ); + const spinner = container.querySelectorAll('.graphiql-spinner'); + expect(spinner).toHaveLength(1); + }); + it('renders with null schema', () => { + const { container } = render( + + + , + ); + const error = container.querySelectorAll('.graphiql-doc-explorer-error'); + expect(error).toHaveLength(1); + expect(error[0]).toHaveTextContent('No GraphQL schema available'); + }); + it('renders with schema', () => { + const { container } = render( + + , + , + ); + const error = container.querySelectorAll('.graphiql-doc-explorer-error'); + expect(error).toHaveLength(0); + expect( + container.querySelector('.graphiql-markdown-description'), + ).toHaveTextContent('GraphQL Schema for testing'); + }); + it('renders correctly with schema error', () => { + const { rerender, container } = render( + + , + , + ); + + const error = container.querySelector('.graphiql-doc-explorer-error'); + + expect(error).toHaveTextContent('Error fetching schema'); + + rerender( + + , + , + ); + + const errors = container.querySelectorAll('.graphiql-doc-explorer-error'); + expect(errors).toHaveLength(0); + }); + it('maintains nav stack when possible', () => { + const initialSchema = makeSchema(); + const Query = initialSchema.getType('Query'); + const { field } = (Query as GraphQLObjectType).getFields(); + + // A hacky component to set the initial explorer nav stack + const SetInitialStack: React.FC = () => { + const context = useContext(ExplorerContext)!; + useEffect(() => { + if (context.explorerNavStack.length === 1) { + context.push({ name: 'Query', def: Query }); + // eslint-disable-next-line unicorn/no-array-push-push -- false positive, push here accept only 1 argument + context.push({ name: 'field', def: field }); + } + }, [context]); + return null; + }; + + // Initial render, set initial state + const { container, rerender } = render( + + + + + , + ); + + // First proper render of doc explorer + rerender( + + + + + , + ); + + const [title] = container.querySelectorAll('.graphiql-doc-explorer-title'); + expect(title.textContent).toEqual('field'); + + // Second render of doc explorer, this time with a new schema, with _same_ field name + rerender( + + + + + , + ); + const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title'); + // Because `Query.field` still exists in the new schema, we can still render it + expect(title2.textContent).toEqual('field'); + }); + it('trims nav stack when necessary', () => { + const initialSchema = makeSchema(); + const Query = initialSchema.getType('Query'); + const { field } = (Query as GraphQLObjectType).getFields(); + + // A hacky component to set the initial explorer nav stack + // eslint-disable-next-line sonarjs/no-identical-functions -- todo: could be refactored + const SetInitialStack: React.FC = () => { + const context = useContext(ExplorerContext)!; + useEffect(() => { + if (context.explorerNavStack.length === 1) { + context.push({ name: 'Query', def: Query }); + // eslint-disable-next-line unicorn/no-array-push-push -- false positive, push here accept only 1 argument + context.push({ name: 'field', def: field }); + } + }, [context]); + return null; + }; + + // Initial render, set initial state + const { container, rerender } = render( + + + + + , + ); + + // First proper render of doc explorer + rerender( + + + + + , + ); + + const [title] = container.querySelectorAll('.graphiql-doc-explorer-title'); + expect(title.textContent).toEqual('field'); + + // Second render of doc explorer, this time with a new schema, with different field name + rerender( + + + + + , + ); + const [title2] = container.querySelectorAll('.graphiql-doc-explorer-title'); + // Because `Query.field` doesn't exist any more, the top-most item we can render is `Query` + expect(title2.textContent).toEqual('Query'); + }); +}); diff --git a/packages/graphiql-react/src/explorer/components/__tests__/field-documentation.spec.tsx b/packages/graphiql-react/src/explorer/components/__tests__/field-documentation.spec.tsx new file mode 100644 index 00000000000..c6ac30f8c5a --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/__tests__/field-documentation.spec.tsx @@ -0,0 +1,160 @@ +import { fireEvent, render } from '@testing-library/react'; +import { GraphQLString, GraphQLObjectType, Kind } from 'graphql'; + +import { ExplorerContext, ExplorerFieldDef } from '../../context'; +import { FieldDocumentation } from '../field-documentation'; +import { mockExplorerContextValue } from './test-utils'; + +const exampleObject = new GraphQLObjectType({ + name: 'Query', + fields: { + string: { + type: GraphQLString, + }, + stringWithArgs: { + type: GraphQLString, + description: 'Example String field with arguments', + args: { + stringArg: { + type: GraphQLString, + }, + deprecatedStringArg: { + type: GraphQLString, + deprecationReason: 'no longer used', + }, + }, + }, + stringWithDirective: { + type: GraphQLString, + astNode: { + kind: Kind.FIELD_DEFINITION, + name: { + kind: Kind.NAME, + value: 'stringWithDirective', + }, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'GraphQLString', + }, + }, + directives: [ + { + kind: Kind.DIRECTIVE, + name: { + kind: Kind.NAME, + value: 'development', + }, + }, + ], + }, + }, + }, +}); + +function FieldDocumentationWithContext(props: { field: ExplorerFieldDef }) { + return ( + + + + ); +} + +describe('FieldDocumentation', () => { + it('should render a simple string field', () => { + const { container } = render( + , + ); + expect( + container.querySelector('.graphiql-markdown-description'), + ).not.toBeInTheDocument(); + expect( + container.querySelector('.graphiql-doc-explorer-type-name'), + ).toHaveTextContent('String'); + expect( + container.querySelector('.graphiql-doc-explorer-argument'), + ).not.toBeInTheDocument(); + }); + + it('should re-render on field change', () => { + const { container, rerender } = render( + , + ); + expect( + container.querySelector('.graphiql-markdown-description'), + ).not.toBeInTheDocument(); + expect( + container.querySelector('.graphiql-doc-explorer-type-name'), + ).toHaveTextContent('String'); + expect( + container.querySelector('.graphiql-doc-explorer-argument'), + ).not.toBeInTheDocument(); + + rerender( + , + ); + expect( + container.querySelector('.graphiql-doc-explorer-type-name'), + ).toHaveTextContent('String'); + expect( + container.querySelector('.graphiql-markdown-description'), + ).toHaveTextContent('Example String field with arguments'); + }); + + it('should render a string field with arguments', () => { + const { container, getByText } = render( + , + ); + expect( + container.querySelector('.graphiql-doc-explorer-type-name'), + ).toHaveTextContent('String'); + expect( + container.querySelector('.graphiql-markdown-description'), + ).toHaveTextContent('Example String field with arguments'); + expect( + container.querySelectorAll('.graphiql-doc-explorer-argument'), + ).toHaveLength(1); + expect( + container.querySelector('.graphiql-doc-explorer-argument'), + ).toHaveTextContent('stringArg: String'); + // by default, the deprecation docs should be hidden + expect( + container.querySelectorAll('.graphiql-markdown-deprecation'), + ).toHaveLength(0); + // make sure deprecation is present + fireEvent.click(getByText('Show Deprecated Arguments')); + const deprecationDocs = container.querySelectorAll( + '.graphiql-markdown-deprecation', + ); + expect(deprecationDocs).toHaveLength(1); + expect(deprecationDocs[0]).toHaveTextContent('no longer used'); + }); + + it('should render a string field with directives', () => { + const { container } = render( + , + ); + expect( + container.querySelector('.graphiql-doc-explorer-type-name'), + ).toHaveTextContent('String'); + expect( + container.querySelector('.graphiql-doc-explorer-directive'), + ).toHaveTextContent('@development'); + }); +}); diff --git a/packages/graphiql-react/src/explorer/components/__tests__/test-utils.ts b/packages/graphiql-react/src/explorer/components/__tests__/test-utils.ts new file mode 100644 index 00000000000..5dc86423223 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/__tests__/test-utils.ts @@ -0,0 +1,18 @@ +import { GraphQLNamedType, GraphQLType } from 'graphql'; + +import { ExplorerContextType, ExplorerNavStackItem } from '../../context'; + +export function mockExplorerContextValue( + navStackItem: ExplorerNavStackItem, +): ExplorerContextType { + return { + explorerNavStack: [navStackItem], + pop() {}, + push() {}, + reset() {}, + }; +} + +export function unwrapType(type: GraphQLType): GraphQLNamedType { + return 'ofType' in type ? unwrapType(type.ofType) : type; +} diff --git a/packages/graphiql-react/src/explorer/components/__tests__/type-documentation.spec.tsx b/packages/graphiql-react/src/explorer/components/__tests__/type-documentation.spec.tsx new file mode 100644 index 00000000000..f462717478e --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/__tests__/type-documentation.spec.tsx @@ -0,0 +1,204 @@ +import { fireEvent, render } from '@testing-library/react'; +import { + GraphQLBoolean, + GraphQLEnumType, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + GraphQLUnionType, +} from 'graphql'; + +import { SchemaContext } from '../../../schema'; +import { ExplorerContext } from '../../context'; +import { TypeDocumentation } from '../type-documentation'; +import { mockExplorerContextValue, unwrapType } from './test-utils'; + +function TypeDocumentationWithContext(props: { type: GraphQLNamedType }) { + return ( + + + + + + ); +} + +describe('TypeDocumentation', () => { + it('renders a top-level query object type', () => { + const { container } = render( + , + ); + const description = container.querySelectorAll( + '.graphiql-markdown-description', + ); + expect(description).toHaveLength(1); + expect(description[0]).toHaveTextContent('Query description\nSecond line', { + normalizeWhitespace: false, + }); + + const cats = container.querySelectorAll('.graphiql-doc-explorer-item'); + expect(cats[0]).toHaveTextContent('string: String'); + expect(cats[1]).toHaveTextContent('union: exampleUnion'); + expect(cats[2]).toHaveTextContent( + 'fieldWithArgs(stringArg: String): String', + ); + }); + + it('renders deprecated fields when you click to see them', () => { + const { container, getByText } = render( + , + ); + let cats = container.querySelectorAll('.graphiql-doc-explorer-item'); + expect(cats).toHaveLength(3); + + fireEvent.click(getByText('Show Deprecated Fields')); + + cats = container.querySelectorAll('.graphiql-doc-explorer-item'); + expect(cats).toHaveLength(4); + expect( + container.querySelectorAll('.graphiql-doc-explorer-field-name')[3], + ).toHaveTextContent('deprecatedField'); + expect( + container.querySelector('.graphiql-markdown-deprecation'), + ).toHaveTextContent('example deprecation reason'); + }); + + it('renders a Union type', () => { + const { container } = render( + , + ); + const title = container.querySelector( + '.graphiql-doc-explorer-section-title', + ); + title?.childNodes[0].remove(); + expect(title).toHaveTextContent('Possible Types'); + }); + + it('renders an Enum type', () => { + const { container } = render( + , + ); + const title = container.querySelector( + '.graphiql-doc-explorer-section-title', + ); + title?.childNodes[0].remove(); + expect(title).toHaveTextContent('Enum Values'); + const enums = container.querySelectorAll( + '.graphiql-doc-explorer-enum-value', + ); + expect(enums[0]).toHaveTextContent('value1'); + expect(enums[1]).toHaveTextContent('value2'); + }); + + it('shows deprecated enum values on click', () => { + const { getByText, container } = render( + , + ); + const showBtn = getByText('Show Deprecated Values'); + expect(showBtn).toBeInTheDocument(); + + const title = container.querySelector( + '.graphiql-doc-explorer-section-title', + ); + title?.childNodes[0].remove(); + expect(title).toHaveTextContent('Enum Values'); + + let enums = container.querySelectorAll('.graphiql-doc-explorer-enum-value'); + expect(enums).toHaveLength(2); + + // click button to show deprecated enum values + fireEvent.click(showBtn); + expect(showBtn).not.toBeInTheDocument(); + + const deprecatedTitle = container.querySelectorAll( + '.graphiql-doc-explorer-section-title', + )[1]; + deprecatedTitle.childNodes[0].remove(); + expect(deprecatedTitle).toHaveTextContent('Deprecated Enum Values'); + + enums = container.querySelectorAll('.graphiql-doc-explorer-enum-value'); + expect(enums).toHaveLength(3); + expect(enums[2]).toHaveTextContent('value3'); + expect( + container.querySelector('.graphiql-markdown-deprecation'), + ).toHaveTextContent('Only two are needed'); + }); +}); + +const ExampleInterface = new GraphQLInterfaceType({ + name: 'exampleInterface', + fields: { + name: { type: GraphQLString }, + }, +}); + +const ExampleEnum = new GraphQLEnumType({ + name: 'exampleEnum', + values: { + value1: { value: 'Value 1' }, + value2: { value: 'Value 2' }, + value3: { value: 'Value 3', deprecationReason: 'Only two are needed' }, + }, +}); + +const ExampleUnionType1 = new GraphQLObjectType({ + name: 'Union_Type_1', + interfaces: [ExampleInterface], + fields: { + name: { type: GraphQLString }, + enum: { type: ExampleEnum }, + }, +}); + +const ExampleUnionType2 = new GraphQLObjectType({ + name: 'Union_Type_2', + interfaces: [ExampleInterface], + fields: { + name: { type: GraphQLString }, + string: { type: GraphQLString }, + }, +}); + +const ExampleUnion = new GraphQLUnionType({ + name: 'exampleUnion', + types: [ExampleUnionType1, ExampleUnionType2], +}); + +const ExampleQuery = new GraphQLObjectType({ + name: 'Query', + description: 'Query description\n Second line', + fields: { + string: { type: GraphQLString }, + union: { type: ExampleUnion }, + fieldWithArgs: { + type: GraphQLString, + args: { + stringArg: { type: GraphQLString }, + }, + }, + deprecatedField: { + type: GraphQLBoolean, + deprecationReason: 'example deprecation reason', + }, + }, +}); + +const ExampleSchema = new GraphQLSchema({ + query: ExampleQuery, + description: 'GraphQL Schema for testing', +}); diff --git a/packages/graphiql-react/src/explorer/components/__tests__/type-link.spec.tsx b/packages/graphiql-react/src/explorer/components/__tests__/type-link.spec.tsx new file mode 100644 index 00000000000..5532ef8c366 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/__tests__/type-link.spec.tsx @@ -0,0 +1,66 @@ +import { fireEvent, render } from '@testing-library/react'; +import { GraphQLNonNull, GraphQLList, GraphQLString } from 'graphql'; +import { ComponentProps } from 'react'; + +import { ExplorerContext } from '../../context'; +import { TypeLink } from '../type-link'; +import { mockExplorerContextValue, unwrapType } from './test-utils'; + +const nonNullType = new GraphQLNonNull(GraphQLString); +const listType = new GraphQLList(GraphQLString); + +function TypeLinkWithContext(props: ComponentProps) { + return ( + + + {/* Print the top of the current nav stack for test assertions */} + + {context => ( + + {JSON.stringify( + context!.explorerNavStack[context!.explorerNavStack.length + 1], + )} + + )} + + + ); +} + +describe('TypeLink', () => { + it('should render a string', () => { + const { container } = render(); + expect(container).toHaveTextContent('String'); + expect(container.querySelectorAll('a')).toHaveLength(1); + }); + it('should render a non-null type', () => { + const { container } = render(); + expect(container).toHaveTextContent('String!'); + expect(container.querySelectorAll('span')).toHaveLength(1); + }); + it('should render a list type', () => { + const { container } = render(); + expect(container).toHaveTextContent('[String]'); + expect(container.querySelectorAll('span')).toHaveLength(1); + }); + it('should push to the nav stack on click', () => { + const { container, getByTestId } = render( + , + ); + fireEvent.click(container.querySelector('a')!); + expect(getByTestId('nav-stack')).toHaveTextContent(''); + }); + it('should re-render on type change', () => { + const { container, rerender } = render( + , + ); + expect(container).toHaveTextContent('[String]'); + rerender(); + expect(container).toHaveTextContent('String'); + }); +}); diff --git a/packages/graphiql-react/src/explorer/components/argument.css b/packages/graphiql-react/src/explorer/components/argument.css new file mode 100644 index 00000000000..56cc073dd0c --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/argument.css @@ -0,0 +1,22 @@ +.graphiql-doc-explorer-argument { + & > * + * { + margin-top: var(--px-12); + } +} + +.graphiql-doc-explorer-argument-name { + color: hsl(var(--color-secondary)); +} + +.graphiql-doc-explorer-argument-deprecation { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + border: 1px solid hsl(var(--color-warning)); + border-radius: var(--border-radius-4); + color: hsl(var(--color-warning)); + padding: var(--px-8); +} + +.graphiql-doc-explorer-argument-deprecation-label { + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); +} diff --git a/packages/graphiql-react/src/explorer/components/argument.tsx b/packages/graphiql-react/src/explorer/components/argument.tsx new file mode 100644 index 00000000000..f38d127501a --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/argument.tsx @@ -0,0 +1,58 @@ +import { GraphQLArgument } from 'graphql'; + +import { DefaultValue } from './default-value'; +import { TypeLink } from './type-link'; + +import './argument.css'; +import { MarkdownContent } from '../../ui'; + +type ArgumentProps = { + /** + * The argument that should be rendered. + */ + arg: GraphQLArgument; + /** + * Toggle if the default value for the argument is shown (if there is one) + * @default false + */ + showDefaultValue?: boolean; + /** + * Toggle whether to render the whole argument including description and + * deprecation reason (`false`) or to just render the argument name, type, + * and default value in a single line (`true`). + * @default false + */ + inline?: boolean; +}; + +export function Argument({ arg, showDefaultValue, inline }: ArgumentProps) { + const definition = ( + + {arg.name} + {': '} + + {showDefaultValue !== false && } + + ); + if (inline) { + return definition; + } + return ( +
+ {definition} + {arg.description ? ( + {arg.description} + ) : null} + {arg.deprecationReason ? ( +
+
+ Deprecated +
+ + {arg.deprecationReason} + +
+ ) : null} +
+ ); +} diff --git a/packages/graphiql-react/src/explorer/components/default-value.css b/packages/graphiql-react/src/explorer/components/default-value.css new file mode 100644 index 00000000000..59c2dcf2454 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/default-value.css @@ -0,0 +1,3 @@ +.graphiql-doc-explorer-default-value { + color: hsl(var(--color-success)); +} diff --git a/packages/graphiql-react/src/explorer/components/default-value.tsx b/packages/graphiql-react/src/explorer/components/default-value.tsx new file mode 100644 index 00000000000..4e3cb0e83be --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/default-value.tsx @@ -0,0 +1,37 @@ +import { astFromValue, print, ValueNode } from 'graphql'; + +import { ExplorerFieldDef } from '../context'; + +import './default-value.css'; + +const printDefault = (ast?: ValueNode | null): string => { + if (!ast) { + return ''; + } + return print(ast); +}; + +type DefaultValueProps = { + /** + * The field or argument for which to render the default value. + */ + field: ExplorerFieldDef; +}; + +export function DefaultValue({ field }: DefaultValueProps) { + if (!('defaultValue' in field) || field.defaultValue === undefined) { + return null; + } + const ast = astFromValue(field.defaultValue, field.type); + if (!ast) { + return null; + } + return ( + <> + {' = '} + + {printDefault(ast)} + + + ); +} diff --git a/packages/graphiql-react/src/explorer/components/deprecation-reason.css b/packages/graphiql-react/src/explorer/components/deprecation-reason.css new file mode 100644 index 00000000000..123929af72d --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/deprecation-reason.css @@ -0,0 +1,12 @@ +.graphiql-doc-explorer-deprecation { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + border: 1px solid hsl(var(--color-warning)); + border-radius: var(--px-4); + color: hsl(var(--color-warning)); + padding: var(--px-8); +} + +.graphiql-doc-explorer-deprecation-label { + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); +} diff --git a/packages/graphiql-react/src/explorer/components/deprecation-reason.tsx b/packages/graphiql-react/src/explorer/components/deprecation-reason.tsx new file mode 100644 index 00000000000..84a9170638c --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/deprecation-reason.tsx @@ -0,0 +1,21 @@ +import { MarkdownContent } from '../../ui'; + +import './deprecation-reason.css'; + +type DeprecationReasonProps = { + /** + * The deprecation reason as markdown string. + */ + children?: string | null; +}; + +export function DeprecationReason(props: DeprecationReasonProps) { + return props.children ? ( +
+
Deprecated
+ + {props.children} + +
+ ) : null; +} diff --git a/packages/graphiql-react/src/explorer/components/directive.css b/packages/graphiql-react/src/explorer/components/directive.css new file mode 100644 index 00000000000..17783cac86b --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/directive.css @@ -0,0 +1,3 @@ +.graphiql-doc-explorer-directive { + color: hsl(var(--color-secondary)); +} diff --git a/packages/graphiql-react/src/explorer/components/directive.tsx b/packages/graphiql-react/src/explorer/components/directive.tsx new file mode 100644 index 00000000000..087d83c585a --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/directive.tsx @@ -0,0 +1,18 @@ +import { DirectiveNode } from 'graphql'; + +import './directive.css'; + +type DirectiveProps = { + /** + * The directive that should be rendered. + */ + directive: DirectiveNode; +}; + +export function Directive({ directive }: DirectiveProps) { + return ( + + @{directive.name.value} + + ); +} diff --git a/packages/graphiql-react/src/explorer/components/doc-explorer.css b/packages/graphiql-react/src/explorer/components/doc-explorer.css new file mode 100644 index 00000000000..87c81282175 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/doc-explorer.css @@ -0,0 +1,102 @@ +/* The header of the doc explorer */ +.graphiql-doc-explorer-header { + display: flex; + justify-content: space-between; + position: relative; + + &:focus-within { + & .graphiql-doc-explorer-title { + /* Hide the header when focussing the search input */ + visibility: hidden; + } + + & .graphiql-doc-explorer-back:not(:focus) { + /** + * Make the back link invisible when focussing the search input. Hiding + * it in any other way makes it impossible to focus the link by pressing + * Shift-Tab while the input is focussed. + */ + color: transparent; + } + } +} +.graphiql-doc-explorer-header-content { + display: flex; + flex-direction: column; + min-width: 0; +} + +/* The search input in the header of the doc explorer */ +.graphiql-doc-explorer-search { + position: absolute; + right: 0; + top: 0; + + &:focus-within { + left: 0; + } + + & [role='combobox'] { + height: 24px; + width: 4ch; + } + + & [role='combobox']:focus { + width: 100%; + } +} + +/* The back-button in the doc explorer */ +a.graphiql-doc-explorer-back { + align-items: center; + color: hsla(var(--color-neutral), var(--alpha-secondary)); + display: flex; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + &:focus { + outline: hsla(var(--color-neutral), var(--alpha-secondary)) auto 1px; + + & + .graphiql-doc-explorer-title { + /* Don't hide the header when focussing the back link */ + visibility: unset; + } + } + + & > svg { + height: var(--px-8); + margin-right: var(--px-8); + width: var(--px-8); + } +} + +/* The title of the currently active page in the doc explorer */ +.graphiql-doc-explorer-title { + font-weight: var(--font-weight-medium); + font-size: var(--font-size-h2); + overflow-x: hidden; + text-overflow: ellipsis; + white-space: nowrap; + &:not(:first-child) { + font-size: var(--font-size-h3); + margin-top: var(--px-8); + } +} + +/* The contents of the currently active page in the doc explorer */ +.graphiql-doc-explorer-content > * { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-top: var(--px-20); +} + +/* Error message */ +.graphiql-doc-explorer-error { + background-color: hsla(var(--color-error), var(--alpha-background-heavy)); + border: 1px solid hsl(var(--color-error)); + border-radius: var(--border-radius-8); + color: hsl(var(--color-error)); + padding: var(--px-8) var(--px-12); +} diff --git a/packages/graphiql-react/src/explorer/components/doc-explorer.tsx b/packages/graphiql-react/src/explorer/components/doc-explorer.tsx new file mode 100644 index 00000000000..63385469c58 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/doc-explorer.tsx @@ -0,0 +1,89 @@ +import { isType } from 'graphql'; +import { ReactNode } from 'react'; + +import { ChevronLeftIcon } from '../../icons'; +import { useSchemaContext } from '../../schema'; +import { Spinner } from '../../ui'; +import { useExplorerContext } from '../context'; +import { FieldDocumentation } from './field-documentation'; +import { SchemaDocumentation } from './schema-documentation'; +import { Search } from './search'; +import { TypeDocumentation } from './type-documentation'; + +import './doc-explorer.css'; + +export function DocExplorer() { + const { fetchError, isFetching, schema, validationErrors } = useSchemaContext( + { nonNull: true, caller: DocExplorer }, + ); + const { explorerNavStack, pop } = useExplorerContext({ + nonNull: true, + caller: DocExplorer, + }); + + const navItem = explorerNavStack.at(-1)!; + + let content: ReactNode = null; + if (fetchError) { + content = ( +
Error fetching schema
+ ); + } else if (validationErrors.length > 0) { + content = ( +
+ Schema is invalid: {validationErrors[0].message} +
+ ); + } else if (isFetching) { + // Schema is undefined when it is being loaded via introspection. + content = ; + } else if (!schema) { + // Schema is null when it explicitly does not exist, typically due to + // an error during introspection. + content = ( +
+ No GraphQL schema available +
+ ); + } else if (explorerNavStack.length === 1) { + content = ; + } else if (isType(navItem.def)) { + content = ; + } else if (navItem.def) { + content = ; + } + + let prevName; + if (explorerNavStack.length > 1) { + prevName = explorerNavStack.at(-2)!.name; + } + + return ( +
+ +
{content}
+
+ ); +} diff --git a/packages/graphiql-react/src/explorer/components/field-documentation.tsx b/packages/graphiql-react/src/explorer/components/field-documentation.tsx new file mode 100644 index 00000000000..90b210be2d1 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/field-documentation.tsx @@ -0,0 +1,97 @@ +import { GraphQLArgument } from 'graphql'; +import { useCallback, useState } from 'react'; + +import { Button, MarkdownContent } from '../../ui'; +import { ExplorerFieldDef } from '../context'; +import { Argument } from './argument'; +import { DeprecationReason } from './deprecation-reason'; +import { Directive } from './directive'; +import { ExplorerSection } from './section'; +import { TypeLink } from './type-link'; + +type FieldDocumentationProps = { + /** + * The field or argument that should be rendered. + */ + field: ExplorerFieldDef; +}; + +export function FieldDocumentation(props: FieldDocumentationProps) { + return ( + <> + {props.field.description ? ( + + {props.field.description} + + ) : null} + {props.field.deprecationReason} + + + + + + + ); +} + +function Arguments({ field }: { field: ExplorerFieldDef }) { + const [showDeprecated, setShowDeprecated] = useState(false); + const handleShowDeprecated = useCallback(() => { + setShowDeprecated(true); + }, []); + + if (!('args' in field)) { + return null; + } + + const args: GraphQLArgument[] = []; + const deprecatedArgs: GraphQLArgument[] = []; + for (const argument of field.args) { + if (argument.deprecationReason) { + deprecatedArgs.push(argument); + } else { + args.push(argument); + } + } + + return ( + <> + {args.length > 0 ? ( + + {args.map(arg => ( + + ))} + + ) : null} + {deprecatedArgs.length > 0 ? ( + showDeprecated || args.length === 0 ? ( + + {deprecatedArgs.map(arg => ( + + ))} + + ) : ( + + ) + ) : null} + + ); +} + +function Directives({ field }: { field: ExplorerFieldDef }) { + const directives = field.astNode?.directives || []; + if (!directives || directives.length === 0) { + return null; + } + return ( + + {directives.map(directive => ( +
+ +
+ ))} +
+ ); +} diff --git a/packages/graphiql-react/src/explorer/components/field-link.css b/packages/graphiql-react/src/explorer/components/field-link.css new file mode 100644 index 00000000000..ff377b0902b --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/field-link.css @@ -0,0 +1,12 @@ +a.graphiql-doc-explorer-field-name { + color: hsl(var(--color-info)); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + &:focus { + outline: hsl(var(--color-info)) auto 1px; + } +} diff --git a/packages/graphiql-react/src/explorer/components/field-link.tsx b/packages/graphiql-react/src/explorer/components/field-link.tsx new file mode 100644 index 00000000000..09245b90cc0 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/field-link.tsx @@ -0,0 +1,27 @@ +import { ExplorerFieldDef, useExplorerContext } from '../context'; + +import './field-link.css'; + +type FieldLinkProps = { + /** + * The field or argument that should be linked to. + */ + field: ExplorerFieldDef; +}; + +export function FieldLink(props: FieldLinkProps) { + const { push } = useExplorerContext({ nonNull: true }); + + return ( + { + event.preventDefault(); + push({ name: props.field.name, def: props.field }); + }} + href="#" + > + {props.field.name} + + ); +} diff --git a/packages/graphiql-react/src/explorer/components/schema-documentation.css b/packages/graphiql-react/src/explorer/components/schema-documentation.css new file mode 100644 index 00000000000..c4e690b4be0 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/schema-documentation.css @@ -0,0 +1,3 @@ +.graphiql-doc-explorer-root-type { + color: hsl(var(--color-info)); +} diff --git a/packages/graphiql-react/src/explorer/components/schema-documentation.tsx b/packages/graphiql-react/src/explorer/components/schema-documentation.tsx new file mode 100644 index 00000000000..f4eced066fc --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/schema-documentation.tsx @@ -0,0 +1,80 @@ +import type { GraphQLSchema } from 'graphql'; + +import { MarkdownContent } from '../../ui'; +import { ExplorerSection } from './section'; +import { TypeLink } from './type-link'; + +import './schema-documentation.css'; + +type SchemaDocumentationProps = { + /** + * The schema that should be rendered. + */ + schema: GraphQLSchema; +}; + +export function SchemaDocumentation(props: SchemaDocumentationProps) { + const queryType = props.schema.getQueryType(); + const mutationType = props.schema.getMutationType?.(); + const subscriptionType = props.schema.getSubscriptionType?.(); + const typeMap = props.schema.getTypeMap(); + const ignoreTypesInAllSchema = [ + queryType?.name, + mutationType?.name, + subscriptionType?.name, + ]; + + return ( + <> + + {props.schema.description || + 'A GraphQL schema provides a root type for each kind of operation.'} + + + {queryType ? ( +
+ query + {': '} + +
+ ) : null} + {mutationType && ( +
+ mutation + {': '} + +
+ )} + {subscriptionType && ( +
+ + subscription + + {': '} + +
+ )} +
+ + {typeMap && ( +
+ {Object.values(typeMap).map(type => { + if ( + ignoreTypesInAllSchema.includes(type.name) || + type.name.startsWith('__') + ) { + return null; + } + + return ( +
+ +
+ ); + })} +
+ )} +
+ + ); +} diff --git a/packages/graphiql-react/src/explorer/components/search.css b/packages/graphiql-react/src/explorer/components/search.css new file mode 100644 index 00000000000..236f6106588 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/search.css @@ -0,0 +1,106 @@ +.graphiql-doc-explorer-search { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + + &:not([data-state='idle']) { + border: var(--popover-border); + border-radius: var(--border-radius-4); + box-shadow: var(--popover-box-shadow); + color: hsla(var(--color-neutral), 1); + + & .graphiql-doc-explorer-search-input { + background: hsl(var(--color-base)); + } + } +} + +.graphiql-doc-explorer-search-input { + align-items: center; + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + border-radius: var(--border-radius-4); + display: flex; + padding: var(--px-8) var(--px-12); +} + +.graphiql-doc-explorer-search [role='combobox'] { + border: none; + background-color: transparent; + margin-left: var(--px-4); + width: 100%; + + &:focus { + outline: none; + } +} + +.graphiql-doc-explorer-search [role='listbox'] { + background-color: hsl(var(--color-base)); + border: none; + border-bottom-left-radius: var(--border-radius-4); + border-bottom-right-radius: var(--border-radius-4); + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + max-height: 400px; + overflow-y: auto; + margin: 0; + font-size: var(--font-size-body); + padding: var(--px-4); + /** + * This makes sure that the logic for auto-scrolling the search results when + * using keyboard navigation works properly (we use `offsetTop` there). + */ + position: relative; +} + +.graphiql-doc-explorer-search [role='option'] { + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + overflow-x: hidden; + padding: var(--px-8) var(--px-12); + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + + &[data-headlessui-state='active'] { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + } + + &:hover { + background-color: hsla( + var(--color-neutral), + var(--alpha-background-medium) + ); + } + + &[data-headlessui-state='active']:hover { + background-color: hsla(var(--color-neutral), var(--alpha-background-heavy)); + } + + & + & { + margin-top: var(--px-4); + } +} + +.graphiql-doc-explorer-search-type { + color: hsl(var(--color-info)); +} + +.graphiql-doc-explorer-search-field { + color: hsl(var(--color-warning)); +} + +.graphiql-doc-explorer-search-argument { + color: hsl(var(--color-secondary)); +} + +.graphiql-doc-explorer-search-divider { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); + margin-top: var(--px-8); + padding: var(--px-8) var(--px-12); +} + +.graphiql-doc-explorer-search-empty { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + padding: var(--px-8) var(--px-12); +} diff --git a/packages/graphiql-react/src/explorer/components/search.tsx b/packages/graphiql-react/src/explorer/components/search.tsx new file mode 100644 index 00000000000..7affdeca08a --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/search.tsx @@ -0,0 +1,307 @@ +import { + GraphQLArgument, + GraphQLField, + GraphQLInputField, + GraphQLNamedType, + isInputObjectType, + isInterfaceType, + isObjectType, +} from 'graphql'; +import { + FocusEventHandler, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Combobox } from '@headlessui/react'; +import { MagnifyingGlassIcon } from '../../icons'; +import { useSchemaContext } from '../../schema'; +import debounce from '../../utility/debounce'; + +import { useExplorerContext } from '../context'; + +import './search.css'; +import { renderType } from './utils'; + +export function Search() { + const { explorerNavStack, push } = useExplorerContext({ + nonNull: true, + caller: Search, + }); + + const inputRef = useRef(null); + const getSearchResults = useSearchResults(); + const [searchValue, setSearchValue] = useState(''); + const [results, setResults] = useState(getSearchResults(searchValue)); + const debouncedGetSearchResults = useMemo( + () => + debounce(200, (search: string) => { + setResults(getSearchResults(search)); + }), + [getSearchResults], + ); + useEffect(() => { + debouncedGetSearchResults(searchValue); + }, [debouncedGetSearchResults, searchValue]); + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.metaKey && event.key === 'k') { + inputRef.current?.focus(); + } + } + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const navItem = explorerNavStack.at(-1)!; + + const onSelect = useCallback( + (def: TypeMatch | FieldMatch) => { + push( + 'field' in def + ? { name: def.field.name, def: def.field } + : { name: def.type.name, def: def.type }, + ); + }, + [push], + ); + const [isFocused, setIsFocused] = useState(false); + const handleFocus: FocusEventHandler = useCallback(e => { + setIsFocused(e.type === 'focus'); + }, []); + + const shouldSearchBoxAppear = + explorerNavStack.length === 1 || + isObjectType(navItem.def) || + isInterfaceType(navItem.def) || + isInputObjectType(navItem.def); + if (!shouldSearchBoxAppear) { + return null; + } + + return ( + +
{ + inputRef.current?.focus(); + }} + > + + setSearchValue(event.target.value)} + placeholder="⌘ K" + ref={inputRef} + value={searchValue} + data-cy="doc-explorer-input" + /> +
+ + {/* hide on blur */} + {isFocused && ( + + {results.within.length + + results.types.length + + results.fields.length === + 0 ? ( +
  • + No results found +
  • + ) : ( + results.within.map((result, i) => ( + + + + )) + )} + {results.within.length > 0 && + results.types.length + results.fields.length > 0 ? ( +
    + Other results +
    + ) : null} + {results.types.map((result, i) => ( + + + + ))} + {results.fields.map((result, i) => ( + + . + + + ))} +
    + )} +
    + ); +} + +type TypeMatch = { type: GraphQLNamedType }; + +type FieldMatch = { + type: GraphQLNamedType; + field: GraphQLField | GraphQLInputField; + argument?: GraphQLArgument; +}; + +export function useSearchResults(caller?: Function) { + const { explorerNavStack } = useExplorerContext({ + nonNull: true, + caller: caller || useSearchResults, + }); + const { schema } = useSchemaContext({ + nonNull: true, + caller: caller || useSearchResults, + }); + + const navItem = explorerNavStack.at(-1)!; + + return useCallback( + (searchValue: string) => { + const matches: { + within: FieldMatch[]; + types: TypeMatch[]; + fields: FieldMatch[]; + } = { + within: [], + types: [], + fields: [], + }; + + if (!schema) { + return matches; + } + + const withinType = navItem.def; + + const typeMap = schema.getTypeMap(); + let typeNames = Object.keys(typeMap); + + // Move the within type name to be the first searched. + if (withinType) { + typeNames = typeNames.filter(n => n !== withinType.name); + typeNames.unshift(withinType.name); + } + for (const typeName of typeNames) { + if ( + matches.within.length + + matches.types.length + + matches.fields.length >= + 100 + ) { + break; + } + + const type = typeMap[typeName]; + if (withinType !== type && isMatch(typeName, searchValue)) { + matches.types.push({ type }); + } + + if ( + !isObjectType(type) && + !isInterfaceType(type) && + !isInputObjectType(type) + ) { + continue; + } + + const fields = type.getFields(); + for (const fieldName in fields) { + const field = fields[fieldName]; + let matchingArgs: GraphQLArgument[] | undefined; + + if (!isMatch(fieldName, searchValue)) { + if ('args' in field) { + matchingArgs = field.args.filter(arg => + isMatch(arg.name, searchValue), + ); + if (matchingArgs.length === 0) { + continue; + } + } else { + continue; + } + } + + matches[withinType === type ? 'within' : 'fields'].push( + ...(matchingArgs + ? matchingArgs.map(argument => ({ type, field, argument })) + : [{ type, field }]), + ); + } + } + + return matches; + }, + [navItem.def, schema], + ); +} + +function isMatch(sourceText: string, searchValue: string): boolean { + try { + const escaped = searchValue.replaceAll(/[^_0-9A-Za-z]/g, ch => '\\' + ch); + return sourceText.search(new RegExp(escaped, 'i')) !== -1; + } catch { + return sourceText.toLowerCase().includes(searchValue.toLowerCase()); + } +} + +type TypeProps = { type: GraphQLNamedType }; + +function Type(props: TypeProps) { + return ( + {props.type.name} + ); +} + +type FieldProps = { + field: GraphQLField | GraphQLInputField; + argument?: GraphQLArgument; +}; + +function Field({ field, argument }: FieldProps) { + return ( + <> + {field.name} + {argument ? ( + <> + ( + + {argument.name} + + :{' '} + {renderType(argument.type, namedType => ( + + ))} + ) + + ) : null} + + ); +} diff --git a/packages/graphiql-react/src/explorer/components/section.css b/packages/graphiql-react/src/explorer/components/section.css new file mode 100644 index 00000000000..3cc72bb874a --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/section.css @@ -0,0 +1,22 @@ +.graphiql-doc-explorer-section-title { + align-items: center; + display: flex; + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); + line-height: 1; + + & > svg { + height: var(--px-16); + margin-right: var(--px-8); + width: var(--px-16); + } +} + +.graphiql-doc-explorer-section-content { + margin-left: var(--px-8); + margin-top: var(--px-16); + + & > * + * { + margin-top: var(--px-16); + } +} diff --git a/packages/graphiql-react/src/explorer/components/section.tsx b/packages/graphiql-react/src/explorer/components/section.tsx new file mode 100644 index 00000000000..4204d528f6f --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/section.tsx @@ -0,0 +1,69 @@ +import { ComponentType, ReactNode } from 'react'; + +import { + ArgumentIcon, + DeprecatedArgumentIcon, + DeprecatedEnumValueIcon, + DeprecatedFieldIcon, + DirectiveIcon, + EnumValueIcon, + FieldIcon, + ImplementsIcon, + RootTypeIcon, + TypeIcon, +} from '../../icons'; + +import './section.css'; + +type ExplorerSectionProps = { + children: ReactNode; + /** + * The title of the section, which will also determine the icon rendered next + * to the headline. + */ + title: + | 'Root Types' + | 'Fields' + | 'Deprecated Fields' + | 'Type' + | 'Arguments' + | 'Deprecated Arguments' + | 'Implements' + | 'Implementations' + | 'Possible Types' + | 'Enum Values' + | 'Deprecated Enum Values' + | 'Directives' + | 'All Schema Types'; +}; + +export function ExplorerSection(props: ExplorerSectionProps) { + const Icon = TYPE_TO_ICON[props.title]; + return ( +
    +
    + + {props.title} +
    +
    + {props.children} +
    +
    + ); +} + +const TYPE_TO_ICON: Record = { + Arguments: ArgumentIcon, + 'Deprecated Arguments': DeprecatedArgumentIcon, + 'Deprecated Enum Values': DeprecatedEnumValueIcon, + 'Deprecated Fields': DeprecatedFieldIcon, + Directives: DirectiveIcon, + 'Enum Values': EnumValueIcon, + Fields: FieldIcon, + Implements: ImplementsIcon, + Implementations: TypeIcon, + 'Possible Types': TypeIcon, + 'Root Types': RootTypeIcon, + Type: TypeIcon, + 'All Schema Types': TypeIcon, +}; diff --git a/packages/graphiql-react/src/explorer/components/type-documentation.css b/packages/graphiql-react/src/explorer/components/type-documentation.css new file mode 100644 index 00000000000..7f65c236ba5 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/type-documentation.css @@ -0,0 +1,11 @@ +.graphiql-doc-explorer-item > :not(:first-child) { + margin-top: var(--px-12); +} + +.graphiql-doc-explorer-argument-multiple { + margin-left: var(--px-8); +} + +.graphiql-doc-explorer-enum-value { + color: hsl(var(--color-info)); +} diff --git a/packages/graphiql-react/src/explorer/components/type-documentation.tsx b/packages/graphiql-react/src/explorer/components/type-documentation.tsx new file mode 100644 index 00000000000..3e5bccbea62 --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/type-documentation.tsx @@ -0,0 +1,238 @@ +import { + GraphQLEnumValue, + GraphQLNamedType, + isAbstractType, + isEnumType, + isInputObjectType, + isInterfaceType, + isNamedType, + isObjectType, +} from 'graphql'; +import { useCallback, useState } from 'react'; + +import { useSchemaContext } from '../../schema'; +import { Button, MarkdownContent } from '../../ui'; +import { ExplorerFieldDef } from '../context'; +import { Argument } from './argument'; +import { DefaultValue } from './default-value'; +import { DeprecationReason } from './deprecation-reason'; +import { FieldLink } from './field-link'; +import { ExplorerSection } from './section'; +import { TypeLink } from './type-link'; + +import './type-documentation.css'; + +type TypeDocumentationProps = { + /** + * The type that should be rendered. + */ + type: GraphQLNamedType; +}; + +export function TypeDocumentation(props: TypeDocumentationProps) { + return isNamedType(props.type) ? ( + <> + {props.type.description ? ( + + {props.type.description} + + ) : null} + + + + + + ) : null; +} + +function ImplementsInterfaces({ type }: { type: GraphQLNamedType }) { + if (!isObjectType(type)) { + return null; + } + const interfaces = type.getInterfaces(); + return interfaces.length > 0 ? ( + + {type.getInterfaces().map(implementedInterface => ( +
    + +
    + ))} +
    + ) : null; +} + +function Fields({ type }: { type: GraphQLNamedType }) { + const [showDeprecated, setShowDeprecated] = useState(false); + const handleShowDeprecated = useCallback(() => { + setShowDeprecated(true); + }, []); + + if ( + !isObjectType(type) && + !isInterfaceType(type) && + !isInputObjectType(type) + ) { + return null; + } + + const fieldMap = type.getFields(); + + const fields: ExplorerFieldDef[] = []; + const deprecatedFields: ExplorerFieldDef[] = []; + + for (const field of Object.keys(fieldMap).map(name => fieldMap[name])) { + if (field.deprecationReason) { + deprecatedFields.push(field); + } else { + fields.push(field); + } + } + + return ( + <> + {fields.length > 0 ? ( + + {fields.map(field => ( + + ))} + + ) : null} + {deprecatedFields.length > 0 ? ( + showDeprecated || fields.length === 0 ? ( + + {deprecatedFields.map(field => ( + + ))} + + ) : ( + + ) + ) : null} + + ); +} + +function Field({ field }: { field: ExplorerFieldDef }) { + const args = + 'args' in field ? field.args.filter(arg => !arg.deprecationReason) : []; + return ( +
    +
    + + {args.length > 0 ? ( + <> + ( + + {args.map(arg => + args.length === 1 ? ( + + ) : ( +
    + +
    + ), + )} +
    + ) + + ) : null} + {': '} + + +
    + {field.description ? ( + + {field.description} + + ) : null} + {field.deprecationReason} +
    + ); +} + +function EnumValues({ type }: { type: GraphQLNamedType }) { + const [showDeprecated, setShowDeprecated] = useState(false); + const handleShowDeprecated = useCallback(() => { + setShowDeprecated(true); + }, []); + + if (!isEnumType(type)) { + return null; + } + + const values: GraphQLEnumValue[] = []; + const deprecatedValues: GraphQLEnumValue[] = []; + for (const value of type.getValues()) { + if (value.deprecationReason) { + deprecatedValues.push(value); + } else { + values.push(value); + } + } + + return ( + <> + {values.length > 0 ? ( + + {values.map(value => ( + + ))} + + ) : null} + {deprecatedValues.length > 0 ? ( + showDeprecated || values.length === 0 ? ( + + {deprecatedValues.map(value => ( + + ))} + + ) : ( + + ) + ) : null} + + ); +} + +function EnumValue({ value }: { value: GraphQLEnumValue }) { + return ( +
    +
    {value.name}
    + {value.description ? ( + + {value.description} + + ) : null} + {value.deprecationReason ? ( + + {value.deprecationReason} + + ) : null} +
    + ); +} + +function PossibleTypes({ type }: { type: GraphQLNamedType }) { + const { schema } = useSchemaContext({ nonNull: true }); + if (!schema || !isAbstractType(type)) { + return null; + } + return ( + + {schema.getPossibleTypes(type).map(possibleType => ( +
    + +
    + ))} +
    + ); +} diff --git a/packages/graphiql-react/src/explorer/components/type-link.css b/packages/graphiql-react/src/explorer/components/type-link.css new file mode 100644 index 00000000000..afd2048462d --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/type-link.css @@ -0,0 +1,12 @@ +a.graphiql-doc-explorer-type-name { + color: hsl(var(--color-warning)); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + + &:focus { + outline: hsl(var(--color-warning)) auto 1px; + } +} diff --git a/packages/graphiql-react/src/explorer/components/type-link.tsx b/packages/graphiql-react/src/explorer/components/type-link.tsx new file mode 100644 index 00000000000..a472fa7dbdc --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/type-link.tsx @@ -0,0 +1,34 @@ +import { GraphQLType } from 'graphql'; + +import { useExplorerContext } from '../context'; +import { renderType } from './utils'; + +import './type-link.css'; + +type TypeLinkProps = { + /** + * The type that should be linked to. + */ + type: GraphQLType; +}; + +export function TypeLink(props: TypeLinkProps) { + const { push } = useExplorerContext({ nonNull: true, caller: TypeLink }); + + if (!props.type) { + return null; + } + + return renderType(props.type, namedType => ( + { + event.preventDefault(); + push({ name: namedType.name, def: namedType }); + }} + href="#" + > + {namedType.name} + + )); +} diff --git a/packages/graphiql-react/src/explorer/components/utils.tsx b/packages/graphiql-react/src/explorer/components/utils.tsx new file mode 100644 index 00000000000..6bef3892d7e --- /dev/null +++ b/packages/graphiql-react/src/explorer/components/utils.tsx @@ -0,0 +1,19 @@ +import { + GraphQLNamedType, + GraphQLType, + isListType, + isNonNullType, +} from 'graphql'; + +export function renderType( + type: GraphQLType, + renderNamedType: (namedType: GraphQLNamedType) => JSX.Element, +): JSX.Element { + if (isNonNullType(type)) { + return <>{renderType(type.ofType, renderNamedType)}!; + } + if (isListType(type)) { + return <>[{renderType(type.ofType, renderNamedType)}]; + } + return renderNamedType(type); +} diff --git a/packages/graphiql-react/src/explorer/context.tsx b/packages/graphiql-react/src/explorer/context.tsx new file mode 100644 index 00000000000..cf545334306 --- /dev/null +++ b/packages/graphiql-react/src/explorer/context.tsx @@ -0,0 +1,205 @@ +import type { + GraphQLArgument, + GraphQLField, + GraphQLInputField, + GraphQLNamedType, +} from 'graphql'; +import { + isEnumType, + isInputObjectType, + isInterfaceType, + isNamedType, + isObjectType, + isScalarType, + isUnionType, +} from 'graphql'; +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import { useSchemaContext } from '../schema'; +import { createContextHook, createNullableContext } from '../utility/context'; + +export type ExplorerFieldDef = + | GraphQLField<{}, {}, {}> + | GraphQLInputField + | GraphQLArgument; + +export type ExplorerNavStackItem = { + /** + * The name of the item. + */ + name: string; + /** + * The definition object of the item, this can be a named type, a field, an + * input field or an argument. + */ + def?: GraphQLNamedType | ExplorerFieldDef; +}; + +// There's always at least one item in the nav stack +export type ExplorerNavStack = [ + ExplorerNavStackItem, + ...ExplorerNavStackItem[], +]; + +const initialNavStackItem: ExplorerNavStackItem = { name: 'Docs' }; + +export type ExplorerContextType = { + /** + * A stack of navigation items. The last item in the list is the current one. + * This list always contains at least one item. + */ + explorerNavStack: ExplorerNavStack; + /** + * Push an item to the navigation stack. + * @param item The item that should be pushed to the stack. + */ + push(item: ExplorerNavStackItem): void; + /** + * Pop the last item from the navigation stack. + */ + pop(): void; + /** + * Reset the navigation stack to its initial state, this will remove all but + * the initial stack item. + */ + reset(): void; +}; + +export const ExplorerContext = + createNullableContext('ExplorerContext'); + +export type ExplorerContextProviderProps = { + children: ReactNode; +}; + +export function ExplorerContextProvider(props: ExplorerContextProviderProps) { + const { schema, validationErrors } = useSchemaContext({ + nonNull: true, + caller: ExplorerContextProvider, + }); + + const [navStack, setNavStack] = useState([ + initialNavStackItem, + ]); + + const push = useCallback((item: ExplorerNavStackItem) => { + setNavStack(currentState => { + const lastItem = currentState.at(-1)!; + return lastItem.def === item.def + ? // Avoid pushing duplicate items + currentState + : [...currentState, item]; + }); + }, []); + + const pop = useCallback(() => { + setNavStack(currentState => + currentState.length > 1 + ? (currentState.slice(0, -1) as ExplorerNavStack) + : currentState, + ); + }, []); + + const reset = useCallback(() => { + setNavStack(currentState => + currentState.length === 1 ? currentState : [initialNavStackItem], + ); + }, []); + + useEffect(() => { + // Whenever the schema changes, we must revalidate/replace the nav stack. + if (schema == null || validationErrors.length > 0) { + reset(); + } else { + // Replace the nav stack with an updated version using the new schema + setNavStack(oldNavStack => { + if (oldNavStack.length === 1) { + return oldNavStack; + } + const newNavStack: ExplorerNavStack = [initialNavStackItem]; + let lastEntity: GraphQLNamedType | GraphQLField | null = + null; + for (const item of oldNavStack) { + if (item === initialNavStackItem) { + // No need to copy the initial item + continue; + } + if (item.def) { + // If item.def isn't a named type, it must be a field, inputField, or argument + if (isNamedType(item.def)) { + // The type needs to be replaced with the new schema type of the same name + const newType = schema.getType(item.def.name); + if (newType) { + newNavStack.push({ + name: item.name, + def: newType, + }); + lastEntity = newType; + } else { + // This type no longer exists; the stack cannot be built beyond here + break; + } + } else if (lastEntity === null) { + // We can't have a sub-entity if we have no entity; stop rebuilding the nav stack + break; + } else if ( + isObjectType(lastEntity) || + isInputObjectType(lastEntity) + ) { + // item.def must be a Field / input field; replace with the new field of the same name + const field = lastEntity.getFields()[item.name]; + if (field) { + newNavStack.push({ + name: item.name, + def: field, + }); + } else { + // This field no longer exists; the stack cannot be built beyond here + break; + } + } else if ( + isScalarType(lastEntity) || + isEnumType(lastEntity) || + isInterfaceType(lastEntity) || + isUnionType(lastEntity) + ) { + // These don't (currently) have non-type sub-entries; something has gone wrong. + // Handle gracefully by discontinuing rebuilding the stack. + break; + } else { + // lastEntity must be a field (because it's not a named type) + const field: GraphQLField = lastEntity; + // Thus item.def must be an argument, so find the same named argument in the new schema + const arg = field.args.find(a => a.name === item.name); + if (arg) { + newNavStack.push({ + name: item.name, + def: field, + }); + } else { + // This argument no longer exists; the stack cannot be built beyond here + break; + } + } + } else { + lastEntity = null; + newNavStack.push(item); + } + } + return newNavStack; + }); + } + }, [reset, schema, validationErrors]); + + const value = useMemo( + () => ({ explorerNavStack: navStack, push, pop, reset }), + [navStack, push, pop, reset], + ); + + return ( + + {props.children} + + ); +} + +export const useExplorerContext = createContextHook(ExplorerContext); diff --git a/packages/graphiql-react/src/explorer/index.ts b/packages/graphiql-react/src/explorer/index.ts new file mode 100644 index 00000000000..0991d26a7b8 --- /dev/null +++ b/packages/graphiql-react/src/explorer/index.ts @@ -0,0 +1,25 @@ +export { Argument } from './components/argument'; +export { DefaultValue } from './components/default-value'; +export { DeprecationReason } from './components/deprecation-reason'; +export { Directive } from './components/directive'; +export { DocExplorer } from './components/doc-explorer'; +export { FieldDocumentation } from './components/field-documentation'; +export { FieldLink } from './components/field-link'; +export { SchemaDocumentation } from './components/schema-documentation'; +export { Search } from './components/search'; +export { ExplorerSection } from './components/section'; +export { TypeDocumentation } from './components/type-documentation'; +export { TypeLink } from './components/type-link'; +export { + ExplorerContext, + ExplorerContextProvider, + useExplorerContext, +} from './context'; + +export type { + ExplorerContextType, + ExplorerContextProviderProps, + ExplorerFieldDef, + ExplorerNavStack, + ExplorerNavStackItem, +} from './context'; diff --git a/packages/graphiql-react/src/history/__tests__/components.spec.tsx b/packages/graphiql-react/src/history/__tests__/components.spec.tsx new file mode 100644 index 00000000000..602e68d9e89 --- /dev/null +++ b/packages/graphiql-react/src/history/__tests__/components.spec.tsx @@ -0,0 +1,130 @@ +import { fireEvent, render } from '@testing-library/react'; +import { ComponentProps } from 'react'; +import { formatQuery, HistoryItem } from '../components'; +import { HistoryContextProvider } from '../context'; +import { useEditorContext } from '../../editor'; +import { Tooltip } from '../../ui'; + +jest.mock('../../editor', () => { + const mockedSetQueryEditor = jest.fn(); + const mockedSetVariableEditor = jest.fn(); + const mockedSetHeaderEditor = jest.fn(); + return { + useEditorContext() { + return { + queryEditor: { setValue: mockedSetQueryEditor }, + variableEditor: { setValue: mockedSetVariableEditor }, + headerEditor: { setValue: mockedSetHeaderEditor }, + }; + }, + }; +}); + +const mockQuery = /* GraphQL */ ` + query Test($string: String) { + test { + hasArgs(string: $string) + } + } +`; + +const mockVariables = JSON.stringify({ string: 'string' }); + +const mockHeaders = JSON.stringify({ foo: 'bar' }); + +const mockOperationName = 'Test'; + +type QueryHistoryItemProps = ComponentProps; + +function QueryHistoryItemWithContext(props: QueryHistoryItemProps) { + return ( + + + + + + ); +} + +const baseMockProps: QueryHistoryItemProps = { + item: { + query: mockQuery, + variables: mockVariables, + headers: mockHeaders, + favorite: false, + }, +}; + +function getMockProps( + customProps?: Partial, +): QueryHistoryItemProps { + return { + ...baseMockProps, + ...customProps, + item: { ...baseMockProps.item, ...customProps?.item }, + }; +} + +describe('QueryHistoryItem', () => { + const mockedSetQueryEditor = useEditorContext()?.queryEditor + ?.setValue as jest.Mock; + const mockedSetVariableEditor = useEditorContext()?.variableEditor + ?.setValue as jest.Mock; + const mockedSetHeaderEditor = useEditorContext()?.headerEditor + ?.setValue as jest.Mock; + beforeEach(() => { + mockedSetQueryEditor.mockClear(); + mockedSetVariableEditor.mockClear(); + mockedSetHeaderEditor.mockClear(); + }); + it('renders operationName if label is not provided', () => { + const otherMockProps = { item: { operationName: mockOperationName } }; + const props = getMockProps(otherMockProps); + const { container } = render(); + expect( + container.querySelector('button.graphiql-history-item-label')! + .textContent, + ).toBe(mockOperationName); + }); + + it('renders a string version of the query if label or operation name are not provided', () => { + const { container } = render( + , + ); + expect( + container.querySelector('button.graphiql-history-item-label')! + .textContent, + ).toBe(formatQuery(mockQuery)); + }); + + it('selects the item when history label button is clicked', () => { + const otherMockProps = { item: { operationName: mockOperationName } }; + const mockProps = getMockProps(otherMockProps); + const { container } = render( + , + ); + fireEvent.click( + container.querySelector('button.graphiql-history-item-label')!, + ); + expect(mockedSetQueryEditor).toHaveBeenCalledTimes(1); + expect(mockedSetQueryEditor).toHaveBeenCalledWith(mockProps.item.query); + expect(mockedSetVariableEditor).toHaveBeenCalledTimes(1); + expect(mockedSetVariableEditor).toHaveBeenCalledWith( + mockProps.item.variables, + ); + expect(mockedSetHeaderEditor).toHaveBeenCalledTimes(1); + expect(mockedSetHeaderEditor).toHaveBeenCalledWith(mockProps.item.headers); + }); + + it('renders label input if the edit label button is clicked', () => { + const { container, getByLabelText } = render( + , + ); + fireEvent.click(getByLabelText('Edit label')); + expect(container.querySelectorAll('li.editable').length).toBe(1); + expect(container.querySelectorAll('input').length).toBe(1); + expect( + container.querySelectorAll('button.graphiql-history-item-label').length, + ).toBe(0); + }); +}); diff --git a/packages/graphiql-react/src/history/components.tsx b/packages/graphiql-react/src/history/components.tsx new file mode 100644 index 00000000000..9ee49574be3 --- /dev/null +++ b/packages/graphiql-react/src/history/components.tsx @@ -0,0 +1,268 @@ +import type { QueryStoreItem } from '@graphiql/toolkit'; +import { + MouseEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { clsx } from 'clsx'; + +import { useEditorContext } from '../editor'; +import { + CloseIcon, + PenIcon, + StarFilledIcon, + StarIcon, + TrashIcon, +} from '../icons'; +import { Button, Tooltip, UnStyledButton } from '../ui'; +import { useHistoryContext } from './context'; + +import './style.css'; + +export function History() { + const { items: all, deleteFromHistory } = useHistoryContext({ + nonNull: true, + }); + + // Reverse items since we push them in so want the latest one at the top, and pass the + // original index in case multiple items share the same label so we can edit correct item + let items = all + .slice() + .map((item, i) => ({ ...item, index: i })) + .reverse(); + const favorites = items.filter(item => item.favorite); + if (favorites.length) { + items = items.filter(item => !item.favorite); + } + + const [clearStatus, setClearStatus] = useState<'success' | 'error' | null>( + null, + ); + useEffect(() => { + if (clearStatus) { + // reset button after a couple seconds + setTimeout(() => { + setClearStatus(null); + }, 2000); + } + }, [clearStatus]); + + const handleClearStatus = useCallback(() => { + try { + for (const item of items) { + deleteFromHistory(item, true); + } + setClearStatus('success'); + } catch { + setClearStatus('error'); + } + }, [deleteFromHistory, items]); + + return ( +
    +
    + History + {(clearStatus || items.length > 0) && ( + + )} +
    + + {Boolean(favorites.length) && ( +
      + {favorites.map(item => ( + + ))} +
    + )} + + {Boolean(favorites.length) && Boolean(items.length) && ( +
    + )} + + {Boolean(items.length) && ( +
      + {items.map(item => ( + + ))} +
    + )} +
    + ); +} + +type QueryHistoryItemProps = { + item: QueryStoreItem & { index?: number }; +}; + +export function HistoryItem(props: QueryHistoryItemProps) { + const { editLabel, toggleFavorite, deleteFromHistory, setActive } = + useHistoryContext({ + nonNull: true, + caller: HistoryItem, + }); + const { headerEditor, queryEditor, variableEditor } = useEditorContext({ + nonNull: true, + caller: HistoryItem, + }); + const inputRef = useRef(null); + const buttonRef = useRef(null); + const [isEditable, setIsEditable] = useState(false); + + useEffect(() => { + if (isEditable) { + inputRef.current?.focus(); + } + }, [isEditable]); + + const displayName = + props.item.label || + props.item.operationName || + formatQuery(props.item.query); + + const handleSave = useCallback(() => { + setIsEditable(false); + const { index, ...item } = props.item; + editLabel({ ...item, label: inputRef.current?.value }, index); + }, [editLabel, props.item]); + + const handleClose = useCallback(() => { + setIsEditable(false); + }, []); + + const handleEditLabel: MouseEventHandler = useCallback( + e => { + e.stopPropagation(); + setIsEditable(true); + }, + [], + ); + + const handleHistoryItemClick: MouseEventHandler = + useCallback(() => { + const { query, variables, headers } = props.item; + queryEditor?.setValue(query ?? ''); + variableEditor?.setValue(variables ?? ''); + headerEditor?.setValue(headers ?? ''); + setActive(props.item); + }, [headerEditor, props.item, queryEditor, setActive, variableEditor]); + + const handleDeleteItemFromHistory: MouseEventHandler = + useCallback( + e => { + e.stopPropagation(); + deleteFromHistory(props.item); + }, + [props.item, deleteFromHistory], + ); + + const handleToggleFavorite: MouseEventHandler = + useCallback( + e => { + e.stopPropagation(); + toggleFavorite(props.item); + }, + [props.item, toggleFavorite], + ); + + return ( +
  • + {isEditable ? ( + <> + { + if (e.key === 'Esc') { + setIsEditable(false); + } else if (e.key === 'Enter') { + setIsEditable(false); + editLabel({ ...props.item, label: e.currentTarget.value }); + } + }} + placeholder="Type a label" + /> + + Save + + + + + + ) : ( + <> + + + {displayName} + + + + + + + + + {props.item.favorite ? ( + + + + + + + + )} +
  • + ); +} + +export function formatQuery(query?: string) { + return query + ?.split('\n') + .map(line => line.replace(/#(.*)/, '')) + .join(' ') + .replaceAll('{', ' { ') + .replaceAll('}', ' } ') + .replaceAll(/[\s]{2,}/g, ' '); +} diff --git a/packages/graphiql-react/src/history/context.tsx b/packages/graphiql-react/src/history/context.tsx new file mode 100644 index 00000000000..1a51e4a8487 --- /dev/null +++ b/packages/graphiql-react/src/history/context.tsx @@ -0,0 +1,167 @@ +import { HistoryStore, QueryStoreItem, StorageAPI } from '@graphiql/toolkit'; +import { ReactNode, useCallback, useMemo, useRef, useState } from 'react'; + +import { useStorageContext } from '../storage'; +import { createContextHook, createNullableContext } from '../utility/context'; + +export type HistoryContextType = { + /** + * Add an operation to the history. + * @param operation The operation that was executed, consisting of the query, + * variables, headers, and operation name. + */ + addToHistory(operation: { + query?: string; + variables?: string; + headers?: string; + operationName?: string; + }): void; + /** + * Change the custom label of an item from the history. + * @param args An object containing the label (`undefined` if it should be + * unset) and properties that identify the history item that the label should + * be applied to. (This can result in the label being applied to multiple + * history items.) + * @param index Index to edit. Without it, will look for the first index matching the + * operation, which may lead to misleading results if multiple items have the same label + */ + editLabel( + args: { + query?: string; + variables?: string; + headers?: string; + operationName?: string; + label?: string; + favorite?: boolean; + }, + index?: number, + ): void; + /** + * The list of history items. + */ + items: readonly QueryStoreItem[]; + /** + * Toggle the favorite state of an item from the history. + * @param args An object containing the favorite state (`undefined` if it + * should be unset) and properties that identify the history item that the + * label should be applied to. (This can result in the label being applied + * to multiple history items.) + */ + toggleFavorite(args: { + query?: string; + variables?: string; + headers?: string; + operationName?: string; + label?: string; + favorite?: boolean; + }): void; + /** + * Delete an operation from the history. + * @param args The operation that was executed, consisting of the query, + * variables, headers, and operation name. + * @param clearFavorites This is only if you press the 'clear' button + */ + deleteFromHistory(args: QueryStoreItem, clearFavorites?: boolean): void; + /** + * If you need to know when an item in history is set as active to customize + * your application. + */ + setActive(args: QueryStoreItem): void; +}; + +export const HistoryContext = + createNullableContext('HistoryContext'); + +export type HistoryContextProviderProps = { + children: ReactNode; + /** + * The maximum number of executed operations to store. + * @default 20 + */ + maxHistoryLength?: number; +}; + +/** + * The functions send the entire operation so users can customize their own application with + * and get access to the operation plus + * any additional props they added for their needs (i.e., build their own functions that may save + * to a backend instead of localStorage and might need an id property added to the QueryStoreItem) + */ +export function HistoryContextProvider(props: HistoryContextProviderProps) { + const storage = useStorageContext(); + const historyStore = useRef( + new HistoryStore( + // Fall back to a noop storage when the StorageContext is empty + storage || new StorageAPI(null), + props.maxHistoryLength || DEFAULT_HISTORY_LENGTH, + ), + ); + const [items, setItems] = useState(historyStore.current?.queries || []); + + const addToHistory: HistoryContextType['addToHistory'] = useCallback( + (operation: QueryStoreItem) => { + historyStore.current?.updateHistory(operation); + setItems(historyStore.current.queries); + }, + [], + ); + + const editLabel: HistoryContextType['editLabel'] = useCallback( + (operation: QueryStoreItem, index?: number) => { + historyStore.current.editLabel(operation, index); + setItems(historyStore.current.queries); + }, + [], + ); + + const toggleFavorite: HistoryContextType['toggleFavorite'] = useCallback( + (operation: QueryStoreItem) => { + historyStore.current.toggleFavorite(operation); + setItems(historyStore.current.queries); + }, + [], + ); + + const setActive: HistoryContextType['setActive'] = useCallback( + (item: QueryStoreItem) => { + return item; + }, + [], + ); + + const deleteFromHistory: HistoryContextType['deleteFromHistory'] = + useCallback((item: QueryStoreItem, clearFavorites = false) => { + historyStore.current.deleteHistory(item, clearFavorites); + setItems(historyStore.current.queries); + }, []); + + const value = useMemo( + () => ({ + addToHistory, + editLabel, + items, + toggleFavorite, + setActive, + deleteFromHistory, + }), + [ + addToHistory, + editLabel, + items, + toggleFavorite, + setActive, + deleteFromHistory, + ], + ); + + return ( + + {props.children} + + ); +} + +export const useHistoryContext = + createContextHook(HistoryContext); + +const DEFAULT_HISTORY_LENGTH = 20; diff --git a/packages/graphiql-react/src/history/index.ts b/packages/graphiql-react/src/history/index.ts new file mode 100644 index 00000000000..e032d22d9a0 --- /dev/null +++ b/packages/graphiql-react/src/history/index.ts @@ -0,0 +1,11 @@ +export { History } from './components'; +export { + HistoryContext, + HistoryContextProvider, + useHistoryContext, +} from './context'; + +export type { + HistoryContextType, + HistoryContextProviderProps, +} from './context'; diff --git a/packages/graphiql-react/src/history/style.css b/packages/graphiql-react/src/history/style.css new file mode 100644 index 00000000000..c12302d0301 --- /dev/null +++ b/packages/graphiql-react/src/history/style.css @@ -0,0 +1,105 @@ +.graphiql-history-header { + font-size: var(--font-size-h2); + font-weight: var(--font-weight-medium); + display: flex; + justify-content: space-between; + align-items: center; +} + +.graphiql-history-header button { + font-size: var(--font-size-inline-code); + padding: var(--px-6) var(--px-10); +} + +.graphiql-history-items { + margin: var(--px-16) 0 0; + list-style: none; + padding: 0; +} + +.graphiql-history-item { + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + display: flex; + font-size: var(--font-size-inline-code); + font-family: var(--font-family-mono); + height: 34px; + + &:hover { + color: hsla(var(--color-neutral), 1); + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + } + + &:not(:first-child) { + margin-top: var(--px-4); + } + + &.editable { + background-color: hsla( + var(--color-primary), + var(--alpha-background-medium) + ); + + & > input { + background: transparent; + border: none; + flex: 1; + margin: 0; + outline: none; + padding: 0 var(--px-10); + width: 100%; + + &::placeholder { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + } + } + + & > button { + color: hsl(var(--color-primary)); + padding: 0 var(--px-10); + + &:active { + background-color: hsla( + var(--color-primary), + var(--alpha-background-heavy) + ); + } + + &:focus { + outline: hsl(var(--color-primary)) auto 1px; + } + + & > svg { + display: block; + } + } + } +} + +button.graphiql-history-item-label { + flex: 1; + padding: var(--px-8) var(--px-10); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +button.graphiql-history-item-action { + align-items: center; + color: hsla(var(--color-neutral), var(--alpha-secondary)); + display: flex; + padding: var(--px-8) var(--px-6); + + &:hover { + color: hsla(var(--color-neutral), 1); + } + + & > svg { + height: 14px; + width: 14px; + } +} + +.graphiql-history-item-spacer { + height: var(--px-16); +} diff --git a/packages/graphiql-react/src/icons/argument.svg b/packages/graphiql-react/src/icons/argument.svg new file mode 100644 index 00000000000..3a981c90c84 --- /dev/null +++ b/packages/graphiql-react/src/icons/argument.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/chevron-down.svg b/packages/graphiql-react/src/icons/chevron-down.svg new file mode 100644 index 00000000000..d02fbfb48c2 --- /dev/null +++ b/packages/graphiql-react/src/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/chevron-left.svg b/packages/graphiql-react/src/icons/chevron-left.svg new file mode 100644 index 00000000000..b740c1d21df --- /dev/null +++ b/packages/graphiql-react/src/icons/chevron-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/chevron-up.svg b/packages/graphiql-react/src/icons/chevron-up.svg new file mode 100644 index 00000000000..617bf886d24 --- /dev/null +++ b/packages/graphiql-react/src/icons/chevron-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/close.svg b/packages/graphiql-react/src/icons/close.svg new file mode 100644 index 00000000000..229315632b3 --- /dev/null +++ b/packages/graphiql-react/src/icons/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/copy.svg b/packages/graphiql-react/src/icons/copy.svg new file mode 100644 index 00000000000..c18b6e78a00 --- /dev/null +++ b/packages/graphiql-react/src/icons/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/deprecated-argument.svg b/packages/graphiql-react/src/icons/deprecated-argument.svg new file mode 100644 index 00000000000..5da9ce09f52 --- /dev/null +++ b/packages/graphiql-react/src/icons/deprecated-argument.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/graphiql-react/src/icons/deprecated-enum-value.svg b/packages/graphiql-react/src/icons/deprecated-enum-value.svg new file mode 100644 index 00000000000..8e44d419817 --- /dev/null +++ b/packages/graphiql-react/src/icons/deprecated-enum-value.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/graphiql-react/src/icons/deprecated-field.svg b/packages/graphiql-react/src/icons/deprecated-field.svg new file mode 100644 index 00000000000..fd07672d975 --- /dev/null +++ b/packages/graphiql-react/src/icons/deprecated-field.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/graphiql-react/src/icons/directive.svg b/packages/graphiql-react/src/icons/directive.svg new file mode 100644 index 00000000000..5cdd18314ff --- /dev/null +++ b/packages/graphiql-react/src/icons/directive.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/docs-filled.svg b/packages/graphiql-react/src/icons/docs-filled.svg new file mode 100644 index 00000000000..15e197e760d --- /dev/null +++ b/packages/graphiql-react/src/icons/docs-filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/graphiql-react/src/icons/docs.svg b/packages/graphiql-react/src/icons/docs.svg new file mode 100644 index 00000000000..4c2bf68a40c --- /dev/null +++ b/packages/graphiql-react/src/icons/docs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/enum-value.svg b/packages/graphiql-react/src/icons/enum-value.svg new file mode 100644 index 00000000000..d677ef519f0 --- /dev/null +++ b/packages/graphiql-react/src/icons/enum-value.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/field.svg b/packages/graphiql-react/src/icons/field.svg new file mode 100644 index 00000000000..58bfbfa987f --- /dev/null +++ b/packages/graphiql-react/src/icons/field.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/history.svg b/packages/graphiql-react/src/icons/history.svg new file mode 100644 index 00000000000..3a632094812 --- /dev/null +++ b/packages/graphiql-react/src/icons/history.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/graphiql-react/src/icons/implements.svg b/packages/graphiql-react/src/icons/implements.svg new file mode 100644 index 00000000000..31b7ba1c310 --- /dev/null +++ b/packages/graphiql-react/src/icons/implements.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/index.ts b/packages/graphiql-react/src/icons/index.ts new file mode 100644 index 00000000000..bc4c4ad5758 --- /dev/null +++ b/packages/graphiql-react/src/icons/index.ts @@ -0,0 +1,79 @@ +import { ComponentProps, FC } from 'react'; + +import _ArgumentIcon from './argument.svg'; +import _ChevronDownIcon from './chevron-down.svg'; +import _ChevronLeftIcon from './chevron-left.svg'; +import _ChevronUpIcon from './chevron-up.svg'; +import _CloseIcon from './close.svg'; +import _CopyIcon from './copy.svg'; +import _DeprecatedArgumentIcon from './deprecated-argument.svg'; +import _DeprecatedEnumValueIcon from './deprecated-enum-value.svg'; +import _DeprecatedFieldIcon from './deprecated-field.svg'; +import _DirectiveIcon from './directive.svg'; +import _DocsFilledIcon from './docs-filled.svg'; +import _DocsIcon from './docs.svg'; +import _EnumValueIcon from './enum-value.svg'; +import _FieldIcon from './field.svg'; +import _HistoryIcon from './history.svg'; +import _ImplementsIcon from './implements.svg'; +import _KeyboardShortcutIcon from './keyboard-shortcut.svg'; +import _MagnifyingGlassIcon from './magnifying-glass.svg'; +import _MergeIcon from './merge.svg'; +import _PenIcon from './pen.svg'; +import _PlayIcon from './play.svg'; +import _PlusIcon from './plus.svg'; +import _PrettifyIcon from './prettify.svg'; +import _ReloadIcon from './reload.svg'; +import _RootTypeIcon from './root-type.svg'; +import _SettingsIcon from './settings.svg'; +import _StarFilledIcon from './star-filled.svg'; +import _StarIcon from './star.svg'; +import _StopIcon from './stop.svg'; +import _TrashIcon from './trash.svg'; +import _TypeIcon from './type.svg'; + +export const ArgumentIcon = generateIcon(_ArgumentIcon); +export const ChevronDownIcon = generateIcon(_ChevronDownIcon); +export const ChevronLeftIcon = generateIcon(_ChevronLeftIcon); +export const ChevronUpIcon = generateIcon(_ChevronUpIcon); +export const CloseIcon = generateIcon(_CloseIcon); +export const CopyIcon = generateIcon(_CopyIcon); +export const DeprecatedArgumentIcon = generateIcon(_DeprecatedArgumentIcon); +export const DeprecatedEnumValueIcon = generateIcon(_DeprecatedEnumValueIcon); +export const DeprecatedFieldIcon = generateIcon(_DeprecatedFieldIcon); +export const DirectiveIcon = generateIcon(_DirectiveIcon); +export const DocsFilledIcon = generateIcon(_DocsFilledIcon, 'filled docs icon'); +export const DocsIcon = generateIcon(_DocsIcon); +export const EnumValueIcon = generateIcon(_EnumValueIcon); +export const FieldIcon = generateIcon(_FieldIcon); +export const HistoryIcon = generateIcon(_HistoryIcon); +export const ImplementsIcon = generateIcon(_ImplementsIcon); +export const KeyboardShortcutIcon = generateIcon(_KeyboardShortcutIcon); +export const MagnifyingGlassIcon = generateIcon(_MagnifyingGlassIcon); +export const MergeIcon = generateIcon(_MergeIcon); +export const PenIcon = generateIcon(_PenIcon); +export const PlayIcon = generateIcon(_PlayIcon); +export const PlusIcon = generateIcon(_PlusIcon); +export const PrettifyIcon = generateIcon(_PrettifyIcon); +export const ReloadIcon = generateIcon(_ReloadIcon); +export const RootTypeIcon = generateIcon(_RootTypeIcon); +export const SettingsIcon = generateIcon(_SettingsIcon); +export const StarFilledIcon = generateIcon(_StarFilledIcon, 'filled star icon'); +export const StarIcon = generateIcon(_StarIcon); +export const StopIcon = generateIcon(_StopIcon); +export const TrashIcon = generateIcon(_TrashIcon, 'trash icon'); +export const TypeIcon = generateIcon(_TypeIcon); + +function generateIcon( + RawComponent: any, + title = RawComponent.name + // Icon component name starts with `Svg${CamelCaseFilename without .svg}` + .replace('Svg', '') + // Insert a space before all caps + .replaceAll(/([A-Z])/g, ' $1') + .trimStart() + .toLowerCase() + ' icon', +): FC> { + RawComponent.defaultProps = { title }; + return RawComponent; +} diff --git a/packages/graphiql-react/src/icons/keyboard-shortcut.svg b/packages/graphiql-react/src/icons/keyboard-shortcut.svg new file mode 100644 index 00000000000..c578c7715ed --- /dev/null +++ b/packages/graphiql-react/src/icons/keyboard-shortcut.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/graphiql-react/src/icons/magnifying-glass.svg b/packages/graphiql-react/src/icons/magnifying-glass.svg new file mode 100644 index 00000000000..b2593871633 --- /dev/null +++ b/packages/graphiql-react/src/icons/magnifying-glass.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/merge.svg b/packages/graphiql-react/src/icons/merge.svg new file mode 100644 index 00000000000..c4db221b4d8 --- /dev/null +++ b/packages/graphiql-react/src/icons/merge.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/graphiql-react/src/icons/pen.svg b/packages/graphiql-react/src/icons/pen.svg new file mode 100644 index 00000000000..365a3b2431a --- /dev/null +++ b/packages/graphiql-react/src/icons/pen.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/graphiql-react/src/icons/play.svg b/packages/graphiql-react/src/icons/play.svg new file mode 100644 index 00000000000..9f194110940 --- /dev/null +++ b/packages/graphiql-react/src/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/plus.svg b/packages/graphiql-react/src/icons/plus.svg new file mode 100644 index 00000000000..5d02b3c4a5f --- /dev/null +++ b/packages/graphiql-react/src/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/prettify.svg b/packages/graphiql-react/src/icons/prettify.svg new file mode 100644 index 00000000000..490de60576e --- /dev/null +++ b/packages/graphiql-react/src/icons/prettify.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/graphiql-react/src/icons/reload.svg b/packages/graphiql-react/src/icons/reload.svg new file mode 100644 index 00000000000..853c18128fa --- /dev/null +++ b/packages/graphiql-react/src/icons/reload.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/graphiql-react/src/icons/root-type.svg b/packages/graphiql-react/src/icons/root-type.svg new file mode 100644 index 00000000000..29ffd5a325a --- /dev/null +++ b/packages/graphiql-react/src/icons/root-type.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/icons/settings.svg b/packages/graphiql-react/src/icons/settings.svg new file mode 100644 index 00000000000..f7cf68be0a1 --- /dev/null +++ b/packages/graphiql-react/src/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/star-filled.svg b/packages/graphiql-react/src/icons/star-filled.svg new file mode 100644 index 00000000000..3c71764a882 --- /dev/null +++ b/packages/graphiql-react/src/icons/star-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/star.svg b/packages/graphiql-react/src/icons/star.svg new file mode 100644 index 00000000000..8399e72b0d7 --- /dev/null +++ b/packages/graphiql-react/src/icons/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/stop.svg b/packages/graphiql-react/src/icons/stop.svg new file mode 100644 index 00000000000..02d9a7b321a --- /dev/null +++ b/packages/graphiql-react/src/icons/stop.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/graphiql-react/src/icons/trash.svg b/packages/graphiql-react/src/icons/trash.svg new file mode 100644 index 00000000000..91917ac8f29 --- /dev/null +++ b/packages/graphiql-react/src/icons/trash.svg @@ -0,0 +1,5 @@ + diff --git a/packages/graphiql-react/src/icons/type.svg b/packages/graphiql-react/src/icons/type.svg new file mode 100644 index 00000000000..59c2c5b21b5 --- /dev/null +++ b/packages/graphiql-react/src/icons/type.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/graphiql-react/src/index.ts b/packages/graphiql-react/src/index.ts new file mode 100644 index 00000000000..4e9b87fad90 --- /dev/null +++ b/packages/graphiql-react/src/index.ts @@ -0,0 +1,113 @@ +import './style/root.css'; + +export { + EditorContext, + EditorContextProvider, + HeaderEditor, + ImagePreview, + QueryEditor, + ResponseEditor, + useAutoCompleteLeafs, + useCopyQuery, + useEditorContext, + useHeaderEditor, + useMergeQuery, + usePrettifyEditors, + useQueryEditor, + useResponseEditor, + useVariableEditor, + VariableEditor, +} from './editor'; +export { + ExecutionContext, + ExecutionContextProvider, + useExecutionContext, +} from './execution'; +export { + Argument, + DefaultValue, + DeprecationReason, + Directive, + DocExplorer, + ExplorerContext, + ExplorerContextProvider, + ExplorerSection, + FieldDocumentation, + FieldLink, + SchemaDocumentation, + Search, + TypeDocumentation, + TypeLink, + useExplorerContext, +} from './explorer'; +export { + History, + HistoryContext, + HistoryContextProvider, + useHistoryContext, +} from './history'; +export { + DOC_EXPLORER_PLUGIN, + HISTORY_PLUGIN, + PluginContext, + PluginContextProvider, + usePluginContext, +} from './plugin'; +export { GraphiQLProvider } from './provider'; +export { + SchemaContext, + SchemaContextProvider, + useSchemaContext, +} from './schema'; +export { + StorageContext, + StorageContextProvider, + useStorageContext, +} from './storage'; +export { useTheme } from './theme'; +export { useDragResize } from './utility/resize'; + +export * from './icons'; +export * from './ui'; +export * from './toolbar'; + +export type { + CommonEditorProps, + EditorContextProviderProps, + EditorContextType, + KeyMap, + ResponseTooltipType, + TabsState, + UseHeaderEditorArgs, + UseQueryEditorArgs, + UseResponseEditorArgs, + UseVariableEditorArgs, + WriteableEditorProps, +} from './editor'; +export type { + ExecutionContextProviderProps, + ExecutionContextType, +} from './execution'; +export type { + ExplorerContextProviderProps, + ExplorerContextType, + ExplorerFieldDef, + ExplorerNavStack, + ExplorerNavStackItem, +} from './explorer'; +export type { + HistoryContextProviderProps, + HistoryContextType, +} from './history'; +export type { + GraphiQLPlugin, + PluginContextType, + PluginContextProviderProps, +} from './plugin'; +export type { GraphiQLProviderProps } from './provider'; +export type { SchemaContextProviderProps, SchemaContextType } from './schema'; +export type { + StorageContextProviderProps, + StorageContextType, +} from './storage'; +export type { Theme } from './theme'; diff --git a/packages/graphiql-react/src/markdown.ts b/packages/graphiql-react/src/markdown.ts new file mode 100644 index 00000000000..9386e370973 --- /dev/null +++ b/packages/graphiql-react/src/markdown.ts @@ -0,0 +1,6 @@ +import MarkdownIt from 'markdown-it'; + +export const markdown = new MarkdownIt({ + breaks: true, + linkify: true, +}); diff --git a/packages/graphiql-react/src/plugin.tsx b/packages/graphiql-react/src/plugin.tsx new file mode 100644 index 00000000000..27194b13f97 --- /dev/null +++ b/packages/graphiql-react/src/plugin.tsx @@ -0,0 +1,196 @@ +import { + ComponentType, + ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { DocExplorer, useExplorerContext } from './explorer'; +import { History, useHistoryContext } from './history'; +import { DocsFilledIcon, DocsIcon, HistoryIcon } from './icons'; +import { useStorageContext } from './storage'; +import { createContextHook, createNullableContext } from './utility/context'; + +export type GraphiQLPlugin = { + /** + * A component that renders content into the plugin pane. + */ + content: ComponentType; + /** + * A component that renders an icon that will be shown inside a button that + * toggles the plugin visibility. + */ + icon: ComponentType; + /** + * The unique title of the plugin. If two plugins are present with the same + * title the provider component will throw an error. + */ + title: string; +}; + +export const DOC_EXPLORER_PLUGIN: GraphiQLPlugin = { + title: 'Documentation Explorer', + icon: function Icon() { + const pluginContext = usePluginContext(); + return pluginContext?.visiblePlugin === DOC_EXPLORER_PLUGIN ? ( + + ) : ( + + ); + }, + content: DocExplorer, +}; +export const HISTORY_PLUGIN: GraphiQLPlugin = { + title: 'History', + icon: HistoryIcon, + content: History, +}; + +export type PluginContextType = { + /** + * A list of all current plugins, including the built-in ones (the doc + * explorer and the history). + */ + plugins: GraphiQLPlugin[]; + /** + * Defines the plugin which is currently visible. + * @param plugin The plugin that should become visible. You can either pass + * the plugin object (has to be referentially equal to the one passed as + * prop) or the plugin title as string. If `null` is passed, no plugin will + * be visible. + */ + setVisiblePlugin(plugin: GraphiQLPlugin | string | null): void; + /** + * The plugin which is currently visible. + */ + visiblePlugin: GraphiQLPlugin | null; +}; + +export const PluginContext = + createNullableContext('PluginContext'); + +export type PluginContextProviderProps = { + children: ReactNode; + /** + * Invoked when the visibility state of any plugin changes. + * @param visiblePlugin The plugin object that is now visible. If no plugin + * is visible, the function will be invoked with `null`. + */ + onTogglePluginVisibility?(visiblePlugin: GraphiQLPlugin | null): void; + /** + * This props accepts a list of plugins that will be shown in addition to the + * built-in ones (the doc explorer and the history). + */ + plugins?: GraphiQLPlugin[]; + /** + * This prop can be used to set the visibility state of plugins. Every time + * this prop changes, the visibility state will be overridden. Note that the + * visibility state can change in between these updates, for example by + * calling the `setVisiblePlugin` function provided by the context. + */ + visiblePlugin?: GraphiQLPlugin | string; +}; + +export function PluginContextProvider(props: PluginContextProviderProps) { + const storage = useStorageContext(); + const explorerContext = useExplorerContext(); + const historyContext = useHistoryContext(); + + const hasExplorerContext = Boolean(explorerContext); + const hasHistoryContext = Boolean(historyContext); + const plugins = useMemo(() => { + const pluginList: GraphiQLPlugin[] = []; + const pluginTitles: Record = {}; + + if (hasExplorerContext) { + pluginList.push(DOC_EXPLORER_PLUGIN); + pluginTitles[DOC_EXPLORER_PLUGIN.title] = true; + } + if (hasHistoryContext) { + pluginList.push(HISTORY_PLUGIN); + pluginTitles[HISTORY_PLUGIN.title] = true; + } + + for (const plugin of props.plugins || []) { + if (typeof plugin.title !== 'string' || !plugin.title) { + throw new Error('All GraphiQL plugins must have a unique title'); + } + if (pluginTitles[plugin.title]) { + throw new Error( + `All GraphiQL plugins must have a unique title, found two plugins with the title '${plugin.title}'`, + ); + } else { + pluginList.push(plugin); + pluginTitles[plugin.title] = true; + } + } + + return pluginList; + }, [hasExplorerContext, hasHistoryContext, props.plugins]); + + const [visiblePlugin, internalSetVisiblePlugin] = + useState(() => { + const storedValue = storage?.get(STORAGE_KEY); + const pluginForStoredValue = plugins.find( + plugin => plugin.title === storedValue, + ); + if (pluginForStoredValue) { + return pluginForStoredValue; + } + if (storedValue) { + storage?.set(STORAGE_KEY, ''); + } + + if (!props.visiblePlugin) { + return null; + } + + return ( + plugins.find( + plugin => + (typeof props.visiblePlugin === 'string' + ? plugin.title + : plugin) === props.visiblePlugin, + ) || null + ); + }); + + const { onTogglePluginVisibility, children } = props; + const setVisiblePlugin = useCallback( + plugin => { + const newVisiblePlugin = plugin + ? plugins.find( + p => (typeof plugin === 'string' ? p.title : p) === plugin, + ) || null + : null; + internalSetVisiblePlugin(current => { + if (newVisiblePlugin === current) { + return current; + } + onTogglePluginVisibility?.(newVisiblePlugin); + return newVisiblePlugin; + }); + }, + [onTogglePluginVisibility, plugins], + ); + + useEffect(() => { + if (props.visiblePlugin) { + setVisiblePlugin(props.visiblePlugin); + } + }, [plugins, props.visiblePlugin, setVisiblePlugin]); + + const value = useMemo( + () => ({ plugins, setVisiblePlugin, visiblePlugin }), + [plugins, setVisiblePlugin, visiblePlugin], + ); + + return ( + {children} + ); +} + +export const usePluginContext = createContextHook(PluginContext); + +const STORAGE_KEY = 'visiblePlugin'; diff --git a/packages/graphiql-react/src/provider.tsx b/packages/graphiql-react/src/provider.tsx new file mode 100644 index 00000000000..ead1907de8e --- /dev/null +++ b/packages/graphiql-react/src/provider.tsx @@ -0,0 +1,98 @@ +import { EditorContextProvider, EditorContextProviderProps } from './editor'; +import { + ExecutionContextProvider, + ExecutionContextProviderProps, +} from './execution'; +import { + ExplorerContextProvider, + ExplorerContextProviderProps, +} from './explorer/context'; +import { HistoryContextProvider, HistoryContextProviderProps } from './history'; +import { PluginContextProvider, PluginContextProviderProps } from './plugin'; +import { SchemaContextProvider, SchemaContextProviderProps } from './schema'; +import { StorageContextProvider, StorageContextProviderProps } from './storage'; + +export type GraphiQLProviderProps = EditorContextProviderProps & + ExecutionContextProviderProps & + ExplorerContextProviderProps & + HistoryContextProviderProps & + PluginContextProviderProps & + SchemaContextProviderProps & + StorageContextProviderProps; + +export function GraphiQLProvider({ + children, + dangerouslyAssumeSchemaIsValid, + defaultQuery, + defaultHeaders, + defaultTabs, + externalFragments, + fetcher, + getDefaultFieldNames, + headers, + inputValueDeprecation, + introspectionQueryName, + maxHistoryLength, + onEditOperationName, + onSchemaChange, + onTabChange, + onTogglePluginVisibility, + operationName, + plugins, + query, + response, + schema, + schemaDescription, + shouldPersistHeaders, + storage, + validationRules, + variables, + visiblePlugin, +}: GraphiQLProviderProps) { + return ( + + + + + + + + {children} + + + + + + + + ); +} diff --git a/packages/graphiql-react/src/schema.tsx b/packages/graphiql-react/src/schema.tsx new file mode 100644 index 00000000000..6284fc95ddc --- /dev/null +++ b/packages/graphiql-react/src/schema.tsx @@ -0,0 +1,423 @@ +import { + Fetcher, + FetcherOpts, + fetcherReturnToPromise, + formatError, + formatResult, + isPromise, +} from '@graphiql/toolkit'; +import { + buildClientSchema, + getIntrospectionQuery, + GraphQLError, + GraphQLSchema, + IntrospectionQuery, + isSchema, + validateSchema, +} from 'graphql'; +import { + ReactNode, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import { useEditorContext } from './editor'; +import { createContextHook, createNullableContext } from './utility/context'; + +type MaybeGraphQLSchema = GraphQLSchema | null | undefined; + +export type SchemaContextType = { + /** + * Stores an error raised during introspecting or building the GraphQL schema + * from the introspection result. + */ + fetchError: string | null; + /** + * Trigger building the GraphQL schema. This might trigger an introspection + * request if no schema is passed via props and if using a schema is not + * explicitly disabled by passing `null` as value for the `schema` prop. If + * there is a schema (either fetched using introspection or passed via props) + * it will be validated, unless this is explicitly skipped using the + * `dangerouslyAssumeSchemaIsValid` prop. + */ + introspect(): void; + /** + * If there currently is an introspection request in-flight. + */ + isFetching: boolean; + /** + * The current GraphQL schema. + */ + schema: MaybeGraphQLSchema; + /** + * A list of errors from validating the current GraphQL schema. The schema is + * valid if and only if this list is empty. + */ + validationErrors: readonly GraphQLError[]; +}; + +export const SchemaContext = + createNullableContext('SchemaContext'); + +export type SchemaContextProviderProps = { + children: ReactNode; + /** + * This prop can be used to skip validating the GraphQL schema. This applies + * to both schemas fetched via introspection and schemas explicitly passed + * via the `schema` prop. + * + * IMPORTANT NOTE: Without validating the schema, GraphiQL and its components + * are vulnerable to numerous exploits and might break. Only use this prop if + * you have full control over the schema passed to GraphiQL. + * + * @default false + */ + dangerouslyAssumeSchemaIsValid?: boolean; + /** + * A function which accepts GraphQL HTTP parameters and returns a `Promise`, + * `Observable` or `AsyncIterable` that returns the GraphQL response in + * parsed JSON format. + * + * We suggest using the `createGraphiQLFetcher` utility from `@graphiql/toolkit` + * to create these fetcher functions. + * + * @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#creategraphiqlfetcher-2|`createGraphiQLFetcher`} + */ + fetcher: Fetcher; + /** + * Invoked after a new GraphQL schema was built. This includes both fetching + * the schema via introspection and passing the schema using the `schema` + * prop. + * @param schema The GraphQL schema that is now used for GraphiQL. + */ + onSchemaChange?(schema: GraphQLSchema): void; + /** + * Explicitly provide the GraphiQL schema that shall be used for GraphiQL. + * If this props is... + * - ...passed and the value is a GraphQL schema, it will be validated and + * then used for GraphiQL if it is valid. + * - ...passed and the value is the result of an introspection query, a + * GraphQL schema will be built from this introspection data, it will be + * validated, and then used for GraphiQL if it is valid. + * - ...set to `null`, no introspection request will be triggered and + * GraphiQL will run without a schema. + * - ...set to `undefined` or not set at all, an introspection request will + * be triggered. If this request succeeds, a GraphQL schema will be built + * from the returned introspection data, it will be validated, and then + * used for GraphiQL if it is valid. If this request fails, GraphiQL will + * run without a schema. + */ + schema?: GraphQLSchema | IntrospectionQuery | null; +} & IntrospectionArgs; + +export function SchemaContextProvider(props: SchemaContextProviderProps) { + if (!props.fetcher) { + throw new TypeError( + 'The `SchemaContextProvider` component requires a `fetcher` function to be passed as prop.', + ); + } + + const { initialHeaders, headerEditor } = useEditorContext({ + nonNull: true, + caller: SchemaContextProvider, + }); + const [schema, setSchema] = useState(); + const [isFetching, setIsFetching] = useState(false); + const [fetchError, setFetchError] = useState(null); + + /** + * A counter that is incremented each time introspection is triggered or the + * schema state is updated. + */ + const counterRef = useRef(0); + + /** + * Synchronize prop changes with state + */ + useEffect(() => { + setSchema( + isSchema(props.schema) || + props.schema === null || + props.schema === undefined + ? props.schema + : undefined, + ); + + /** + * Increment the counter so that in-flight introspection requests don't + * override this change. + */ + counterRef.current++; + }, [props.schema]); + + /** + * Keep a ref to the current headers + */ + const headersRef = useRef(initialHeaders); + useEffect(() => { + if (headerEditor) { + headersRef.current = headerEditor.getValue(); + } + }); + + /** + * Get introspection query for settings given via props + */ + const { + introspectionQuery, + introspectionQueryName, + introspectionQuerySansSubscriptions, + } = useIntrospectionQuery({ + inputValueDeprecation: props.inputValueDeprecation, + introspectionQueryName: props.introspectionQueryName, + schemaDescription: props.schemaDescription, + }); + + /** + * Fetch the schema + */ + const { fetcher, onSchemaChange, dangerouslyAssumeSchemaIsValid, children } = + props; + const introspect = useCallback(() => { + /** + * Only introspect if there is no schema provided via props. If the + * prop is passed an introspection result, we do continue but skip the + * introspection request. + */ + if (isSchema(props.schema) || props.schema === null) { + return; + } + + const counter = ++counterRef.current; + + const maybeIntrospectionData = props.schema; + + async function fetchIntrospectionData() { + if (maybeIntrospectionData) { + // No need to introspect if we already have the data + return maybeIntrospectionData; + } + + const parsedHeaders = parseHeaderString(headersRef.current); + if (!parsedHeaders.isValidJSON) { + setFetchError('Introspection failed as headers are invalid.'); + return; + } + + const fetcherOpts: FetcherOpts = parsedHeaders.headers + ? { headers: parsedHeaders.headers } + : {}; + + const fetch = fetcherReturnToPromise( + fetcher( + { + query: introspectionQuery, + operationName: introspectionQueryName, + }, + fetcherOpts, + ), + ); + + if (!isPromise(fetch)) { + setFetchError('Fetcher did not return a Promise for introspection.'); + return; + } + + setIsFetching(true); + setFetchError(null); + + let result = await fetch; + + if ( + typeof result !== 'object' || + result === null || + !('data' in result) + ) { + // Try the stock introspection query first, falling back on the + // sans-subscriptions query for services which do not yet support it. + const fetch2 = fetcherReturnToPromise( + fetcher( + { + query: introspectionQuerySansSubscriptions, + operationName: introspectionQueryName, + }, + fetcherOpts, + ), + ); + if (!isPromise(fetch2)) { + throw new Error( + 'Fetcher did not return a Promise for introspection.', + ); + } + result = await fetch2; + } + + setIsFetching(false); + + if (result?.data && '__schema' in result.data) { + return result.data as IntrospectionQuery; + } + + // handle as if it were an error if the fetcher response is not a string or response.data is not present + const responseString = + typeof result === 'string' ? result : formatResult(result); + setFetchError(responseString); + } + + fetchIntrospectionData() + .then(introspectionData => { + /** + * Don't continue if another introspection request has been started in + * the meantime or if there is no introspection data. + */ + if (counter !== counterRef.current || !introspectionData) { + return; + } + + try { + const newSchema = buildClientSchema(introspectionData); + setSchema(newSchema); + onSchemaChange?.(newSchema); + } catch (error) { + setFetchError(formatError(error)); + } + }) + .catch(error => { + /** + * Don't continue if another introspection request has been started in + * the meantime. + */ + if (counter !== counterRef.current) { + return; + } + + setFetchError(formatError(error)); + setIsFetching(false); + }); + }, [ + fetcher, + introspectionQueryName, + introspectionQuery, + introspectionQuerySansSubscriptions, + onSchemaChange, + props.schema, + ]); + + /** + * Trigger introspection automatically + */ + useEffect(() => { + introspect(); + }, [introspect]); + + /** + * Trigger introspection manually via short key + */ + useEffect(() => { + function triggerIntrospection(event: KeyboardEvent) { + if (event.ctrlKey && event.key === 'R') { + introspect(); + } + } + + window.addEventListener('keydown', triggerIntrospection); + return () => window.removeEventListener('keydown', triggerIntrospection); + }); + + /** + * Derive validation errors from the schema + */ + const validationErrors = useMemo(() => { + if (!schema || dangerouslyAssumeSchemaIsValid) { + return []; + } + return validateSchema(schema); + }, [schema, dangerouslyAssumeSchemaIsValid]); + + /** + * Memoize context value + */ + const value = useMemo( + () => ({ + fetchError, + introspect, + isFetching, + schema, + validationErrors, + }), + [fetchError, introspect, isFetching, schema, validationErrors], + ); + + return ( + {children} + ); +} + +export const useSchemaContext = createContextHook(SchemaContext); + +type IntrospectionArgs = { + /** + * Can be used to set the equally named option for introspecting a GraphQL + * server. + * @default false + * @see {@link https://github.com/graphql/graphql-js/blob/main/src/utilities/getIntrospectionQuery.ts|Utility for creating the introspection query} + */ + inputValueDeprecation?: boolean; + /** + * Can be used to set a custom operation name for the introspection query. + */ + introspectionQueryName?: string; + /** + * Can be used to set the equally named option for introspecting a GraphQL + * server. + * @default false + * @see {@link https://github.com/graphql/graphql-js/blob/main/src/utilities/getIntrospectionQuery.ts|Utility for creating the introspection query} + */ + schemaDescription?: boolean; +}; + +function useIntrospectionQuery({ + inputValueDeprecation, + introspectionQueryName, + schemaDescription, +}: IntrospectionArgs) { + return useMemo(() => { + const queryName = introspectionQueryName || 'IntrospectionQuery'; + + let query = getIntrospectionQuery({ + inputValueDeprecation, + schemaDescription, + }); + if (introspectionQueryName) { + query = query.replace('query IntrospectionQuery', `query ${queryName}`); + } + + const querySansSubscriptions = query.replace( + 'subscriptionType { name }', + '', + ); + + return { + introspectionQueryName: queryName, + introspectionQuery: query, + introspectionQuerySansSubscriptions: querySansSubscriptions, + }; + }, [inputValueDeprecation, introspectionQueryName, schemaDescription]); +} + +function parseHeaderString(headersString: string | undefined) { + let headers: Record | null = null; + let isValidJSON = true; + + try { + if (headersString) { + headers = JSON.parse(headersString); + } + } catch { + isValidJSON = false; + } + return { headers, isValidJSON }; +} diff --git a/packages/graphiql-react/src/storage.tsx b/packages/graphiql-react/src/storage.tsx new file mode 100644 index 00000000000..a8d9f0740f2 --- /dev/null +++ b/packages/graphiql-react/src/storage.tsx @@ -0,0 +1,41 @@ +import { Storage, StorageAPI } from '@graphiql/toolkit'; +import { ReactNode, useEffect, useRef, useState } from 'react'; + +import { createContextHook, createNullableContext } from './utility/context'; + +export type StorageContextType = StorageAPI; + +export const StorageContext = + createNullableContext('StorageContext'); + +export type StorageContextProviderProps = { + children: ReactNode; + /** + * Provide a custom storage API. + * @default `localStorage`` + * @see {@link https://graphiql-test.netlify.app/typedoc/modules/graphiql_toolkit.html#storage-2|API docs} + * for details on the required interface. + */ + storage?: Storage; +}; + +export function StorageContextProvider(props: StorageContextProviderProps) { + const isInitialRender = useRef(true); + const [storage, setStorage] = useState(new StorageAPI(props.storage)); + + useEffect(() => { + if (isInitialRender.current) { + isInitialRender.current = false; + } else { + setStorage(new StorageAPI(props.storage)); + } + }, [props.storage]); + + return ( + + {props.children} + + ); +} + +export const useStorageContext = createContextHook(StorageContext); diff --git a/packages/graphiql-react/src/style/root.css b/packages/graphiql-react/src/style/root.css new file mode 100644 index 00000000000..adf9e2f002a --- /dev/null +++ b/packages/graphiql-react/src/style/root.css @@ -0,0 +1,148 @@ +/* a very simple box-model reset, intentionally does not include pseudo elements */ +.graphiql-container * { + box-sizing: border-box; + font-variant-ligatures: none; +} + +.graphiql-container, +.CodeMirror-info, +.CodeMirror-lint-tooltip, +.graphiql-dialog, +.graphiql-dialog-overlay, +.graphiql-tooltip, +[data-radix-popper-content-wrapper] { + /* Colors */ + --color-primary: 320, 95%, 43%; + --color-secondary: 242, 51%, 61%; + --color-tertiary: 188, 100%, 36%; + --color-info: 208, 100%, 46%; + --color-success: 158, 60%, 42%; + --color-warning: 36, 100%, 41%; + --color-error: 13, 93%, 58%; + --color-neutral: 219, 28%, 32%; + --color-base: 219, 28%, 100%; + + /* Color alpha values */ + --alpha-secondary: 0.76; + --alpha-tertiary: 0.5; + --alpha-background-heavy: 0.15; + --alpha-background-medium: 0.1; + --alpha-background-light: 0.07; + + /* Font */ + --font-family: 'Roboto', sans-serif; + --font-family-mono: 'Fira Code', monospace; + --font-size-hint: calc(12rem / 16); + --font-size-inline-code: calc(13rem / 16); + --font-size-body: calc(15rem / 16); + --font-size-h4: calc(18rem / 16); + --font-size-h3: calc(22rem / 16); + --font-size-h2: calc(29rem / 16); + --font-weight-regular: 400; + --font-weight-medium: 500; + --line-height: 1.5; + + /* Spacing */ + --px-2: 2px; + --px-4: 4px; + --px-6: 6px; + --px-8: 8px; + --px-10: 10px; + --px-12: 12px; + --px-16: 16px; + --px-20: 20px; + --px-24: 24px; + + /* Border radius */ + --border-radius-2: 2px; + --border-radius-4: 4px; + --border-radius-8: 8px; + --border-radius-12: 12px; + + /* Popover styles (tooltip, dialog, etc) */ + --popover-box-shadow: 0px 6px 20px rgba(59, 76, 106, 0.13), + 0px 1.34018px 4.46726px rgba(59, 76, 106, 0.0774939), + 0px 0.399006px 1.33002px rgba(59, 76, 106, 0.0525061); + --popover-border: none; + + /* Layout */ + --sidebar-width: 60px; + --toolbar-width: 40px; + --session-header-height: 51px; +} + +@media (prefers-color-scheme: dark) { + body:not(.graphiql-light) .graphiql-container, + body:not(.graphiql-light) .CodeMirror-info, + body:not(.graphiql-light) .CodeMirror-lint-tooltip, + body:not(.graphiql-light) .graphiql-dialog, + body:not(.graphiql-light) .graphiql-dialog-overlay, + body:not(.graphiql-light) .graphiql-tooltip, + body:not(.graphiql-light) [data-radix-popper-content-wrapper] { + --color-primary: 338, 100%, 67%; + --color-secondary: 243, 100%, 77%; + --color-tertiary: 188, 100%, 44%; + --color-info: 208, 100%, 72%; + --color-success: 158, 100%, 42%; + --color-warning: 30, 100%, 80%; + --color-error: 13, 100%, 58%; + --color-neutral: 219, 29%, 78%; + --color-base: 219, 29%, 18%; + + --popover-box-shadow: none; + --popover-border: 1px solid hsl(var(--color-neutral)); + } +} + +body.graphiql-dark .graphiql-container, +body.graphiql-dark .CodeMirror-info, +body.graphiql-dark .CodeMirror-lint-tooltip, +body.graphiql-dark .graphiql-dialog, +body.graphiql-dark .graphiql-dialog-overlay, +body.graphiql-dark .graphiql-tooltip, +body.graphiql-dark [data-radix-popper-content-wrapper] { + --color-primary: 338, 100%, 67%; + --color-secondary: 243, 100%, 77%; + --color-tertiary: 188, 100%, 44%; + --color-info: 208, 100%, 72%; + --color-success: 158, 100%, 42%; + --color-warning: 30, 100%, 80%; + --color-error: 13, 100%, 58%; + --color-neutral: 219, 29%, 78%; + --color-base: 219, 29%, 18%; + + --popover-box-shadow: none; + --popover-border: 1px solid hsl(var(--color-neutral)); +} + +.graphiql-container, +.CodeMirror-info, +.CodeMirror-lint-tooltip, +.graphiql-dialog { + &, + &:is(button) { + color: hsla(var(--color-neutral), 1); + font-family: var(--font-family); + font-size: var(--font-size-body); + font-weight: var(----font-weight-regular); + line-height: var(--line-height); + } + + & input { + color: hsla(var(--color-neutral), 1); + font-family: var(--font-family); + font-size: var(--font-size-caption); + + &::placeholder { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + } + } + + & a { + color: hsl(var(--color-primary)); + + &:focus { + outline: hsl(var(--color-primary)) auto 1px; + } + } +} diff --git a/packages/graphiql-react/src/theme.ts b/packages/graphiql-react/src/theme.ts new file mode 100644 index 00000000000..5dc3b8ea669 --- /dev/null +++ b/packages/graphiql-react/src/theme.ts @@ -0,0 +1,55 @@ +import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; +import { useStorageContext } from './storage'; + +/** + * The value `null` semantically means that the user does not explicitly choose + * any theme, so we use the system default. + */ +export type Theme = 'light' | 'dark' | null; + +export function useTheme() { + const storageContext = useStorageContext(); + + const [theme, setThemeInternal] = useState(() => { + if (!storageContext) { + return null; + } + + const stored = storageContext.get(STORAGE_KEY); + switch (stored) { + case 'light': + return 'light'; + case 'dark': + return 'dark'; + default: + if (typeof stored === 'string') { + // Remove the invalid stored value + storageContext.set(STORAGE_KEY, ''); + } + return null; + } + }); + + useLayoutEffect(() => { + if (typeof window === 'undefined') { + return; + } + + document.body.classList.remove('graphiql-light', 'graphiql-dark'); + if (theme) { + document.body.classList.add(`graphiql-${theme}`); + } + }, [theme]); + + const setTheme = useCallback( + (newTheme: Theme) => { + storageContext?.set(STORAGE_KEY, newTheme || ''); + setThemeInternal(newTheme); + }, + [storageContext], + ); + + return useMemo(() => ({ theme, setTheme }), [theme, setTheme]); +} + +const STORAGE_KEY = 'theme'; diff --git a/packages/graphiql-react/src/toolbar/button.css b/packages/graphiql-react/src/toolbar/button.css new file mode 100644 index 00000000000..6d66de96012 --- /dev/null +++ b/packages/graphiql-react/src/toolbar/button.css @@ -0,0 +1,11 @@ +button.graphiql-toolbar-button { + display: flex; + align-items: center; + justify-content: center; + height: var(--toolbar-width); + width: var(--toolbar-width); + + &.error { + background: hsla(var(--color-error), var(--alpha-background-heavy)); + } +} diff --git a/packages/graphiql-react/src/toolbar/button.tsx b/packages/graphiql-react/src/toolbar/button.tsx new file mode 100644 index 00000000000..5c132264187 --- /dev/null +++ b/packages/graphiql-react/src/toolbar/button.tsx @@ -0,0 +1,50 @@ +import { forwardRef, MouseEventHandler, useCallback, useState } from 'react'; +import { clsx } from 'clsx'; +import { Tooltip, UnStyledButton } from '../ui'; + +import './button.css'; + +type ToolbarButtonProps = { + label: string; +}; + +export const ToolbarButton = forwardRef< + HTMLButtonElement, + ToolbarButtonProps & JSX.IntrinsicElements['button'] +>(({ label, onClick, ...props }, ref) => { + const [error, setError] = useState(null); + const handleClick: MouseEventHandler = useCallback( + event => { + try { + onClick?.(event); + setError(null); + } catch (err) { + setError( + err instanceof Error + ? err + : new Error(`Toolbar button click failed: ${err}`), + ); + } + }, + [onClick], + ); + + return ( + + + + ); +}); +ToolbarButton.displayName = 'ToolbarButton'; diff --git a/packages/graphiql-react/src/toolbar/execute.css b/packages/graphiql-react/src/toolbar/execute.css new file mode 100644 index 00000000000..bb8aa8d8c52 --- /dev/null +++ b/packages/graphiql-react/src/toolbar/execute.css @@ -0,0 +1,33 @@ +.graphiql-execute-button-wrapper { + position: relative; +} + +button.graphiql-execute-button { + background-color: hsl(var(--color-primary)); + border: none; + border-radius: var(--border-radius-8); + cursor: pointer; + height: var(--toolbar-width); + padding: 0; + width: var(--toolbar-width); + + &:hover { + background-color: hsla(var(--color-primary), 0.9); + } + + &:active { + background-color: hsla(var(--color-primary), 0.8); + } + + &:focus { + outline: hsla(var(--color-primary), 0.8) auto 1px; + } + + & > svg { + color: white; + display: block; + height: var(--px-16); + margin: auto; + width: var(--px-16); + } +} diff --git a/packages/graphiql-react/src/toolbar/execute.tsx b/packages/graphiql-react/src/toolbar/execute.tsx new file mode 100644 index 00000000000..ff7eb1e70f9 --- /dev/null +++ b/packages/graphiql-react/src/toolbar/execute.tsx @@ -0,0 +1,77 @@ +import { useEditorContext } from '../editor'; +import { useExecutionContext } from '../execution'; +import { PlayIcon, StopIcon } from '../icons'; +import { DropdownMenu, Tooltip } from '../ui'; + +import './execute.css'; + +export function ExecuteButton() { + const { queryEditor, setOperationName } = useEditorContext({ + nonNull: true, + caller: ExecuteButton, + }); + const { isFetching, isSubscribed, operationName, run, stop } = + useExecutionContext({ + nonNull: true, + caller: ExecuteButton, + }); + + const operations = queryEditor?.operations || []; + const hasOptions = operations.length > 1 && typeof operationName !== 'string'; + const isRunning = isFetching || isSubscribed; + + const label = `${isRunning ? 'Stop' : 'Execute'} query (Ctrl-Enter)`; + const buttonProps = { + type: 'button' as const, + className: 'graphiql-execute-button', + children: isRunning ? : , + 'aria-label': label, + }; + + return hasOptions && !isRunning ? ( + + + + + + + {operations.map((operation, i) => { + const opName = operation.name + ? operation.name.value + : ``; + return ( + { + const selectedOperationName = operation.name?.value; + if ( + queryEditor && + selectedOperationName && + selectedOperationName !== queryEditor.operationName + ) { + setOperationName(selectedOperationName); + } + run(); + }} + > + {opName} + + ); + })} + + + ) : ( + + + + +
    + + + ); + } + ``` + +* [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - BREAKING: The following exports of the `graphiql` package have been removed: + - `DocExplorer`: Now exported from `@graphiql/react` as `DocExplorer` + - The `schema` prop has been removed, the component now uses the schema provided by the `ExplorerContext` + - `fillLeafs`: Now exported from `@graphiql/toolkit` as `fillLeafs` + - `getSelectedOperationName`: Now exported from `@graphiql/toolkit` as `getSelectedOperationName` + - `mergeAst`: Now exported from `@graphiql/toolkit` as `mergeAst` + - `onHasCompletion`: Now exported from `@graphiql/react` as `onHasCompletion` + - `QueryEditor`: Now exported from `@graphiql/react` as `QueryEditor` + - `ToolbarMenu`: Now exported from `@graphiql/react` as `ToolbarMenu` + - `ToolbarMenuItem`: Now exported from `@graphiql/react` as `ToolbarMenu.Item` + - `ToolbarSelect`: Now exported from `@graphiql/react` as `ToolbarListbox` + - `ToolbarSelectOption`: Now exported from `@graphiql/react` as `ToolbarListbox.Option` + - `VariableEditor`: Now exported from `@graphiql/react` as `VariableEditor` + - type `Fetcher`: Now exported from `@graphiql/toolkit` + - type `FetcherOpts`: Now exported from `@graphiql/toolkit` + - type `FetcherParams`: Now exported from `@graphiql/toolkit` + - type `FetcherResult`: Now exported from `@graphiql/toolkit` + - type `FetcherReturnType`: Now exported from `@graphiql/toolkit` + - type `Observable`: Now exported from `@graphiql/toolkit` + - type `Storage`: Now exported from `@graphiql/toolkit` + - type `SyncFetcherResult`: Now exported from `@graphiql/toolkit` + +- [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - BREAKING: The `GraphiQL` component has been refactored to be a function component. Attaching a ref to this component will no longer provide access to props, state or class methods. In order to interact with or change `GraphiQL` state you need to use the contexts and hooks provided by the `@graphiql/react` package. More details and examples can be found in the migration guide. + +* [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - BREAKING: The following props of the `GraphiQL` component have been changed: + - The props `defaultVariableEditorOpen` and `defaultSecondaryEditorOpen` have been merged into one prop `defaultEditorToolsVisibility`. The default behavior if this prop is not passed is that the editor tools are shown if at least one of the secondary editors has contents. You can pass the following values to the prop: + - Passing `false` hides the editor tools. + - Passing `true` shows the editor tools. + - Passing `"variables"` explicitly shows the variables editor. + - Passing `"headers"` explicitly shows the headers editor. + - The props `docExplorerOpen`, `onToggleDocs` and `onToggleHistory` have been removed. They are replaced by the more generic props `visiblePlugin` (for controlling which plugin is visible) and `onTogglePluginVisibility` (which is called each time the visibility of any plugin changes). + - The `headerEditorEnabled` prop has been renamed to `isHeadersEditorEnabled`. + - The `ResultsTooltip` prop has been renamed to `responseTooltip`. + - Tabs are now always enabled. The `tabs` prop has therefore been replaced with a prop `onTabChange`. If you used the `tabs` prop before to pass this function you can change your implementation like so: + ```diff + {/* do something */} }} + + onTabChange={(tabState) => {/* do something */}} + /> + ``` + +### Minor Changes + +- [#2694](https://github.com/graphql/graphiql/pull/2694) [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279) Thanks [@acao](https://github.com/acao)! - GraphiQL now ships with a dark theme. By default the interface respects the system settings, the theme can also be explicitly chosen via the new settings dialog. + +### Patch Changes + +- Updated dependencies [[`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279), [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279), [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279), [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279), [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279), [`e59ec32e`](https://github.com/graphql/graphiql/commit/e59ec32e7ccdf3f7f68656533555c63620826279)]: + - @graphiql/react@0.11.0 + - @graphiql/toolkit@0.7.0 + +## 1.11.6 + +### Patch Changes + +- Updated dependencies [[`d6ff4d7a`](https://github.com/graphql/graphiql/commit/d6ff4d7a5d535a0c43fe5914016bac9ef0c2b782)]: + - graphql-language-service@5.1.0 + - @graphiql/react@0.10.1 + +## 1.11.5 + +### Patch Changes + +- [#2678](https://github.com/graphql/graphiql/pull/2678) [`b3470b99`](https://github.com/graphql/graphiql/commit/b3470b993bd4c1b90ab7831581de2021af1bb6b0) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add the attribute `type="button"` to all buttons + +## 1.11.4 + +### Patch Changes + +- Updated dependencies [[`85d5af25`](https://github.com/graphql/graphiql/commit/85d5af25d77c29b7d02da90a431c8c15f610c22a), [`6ff0bab9`](https://github.com/graphql/graphiql/commit/6ff0bab978d63778b8ab4ba6e79fceb36c2db87f), [`0aff68a6`](https://github.com/graphql/graphiql/commit/0aff68a645cceb6b9689e0f394e8bece01710efc)]: + - @graphiql/react@0.10.0 + +## 1.11.3 + +### Patch Changes + +- [#2642](https://github.com/graphql/graphiql/pull/2642) [`100af928`](https://github.com/graphql/graphiql/commit/100af9284de18ca89524c646e86854313c5d067b) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix controlling the operation name sent with the request using the `operationName` prop + +- Updated dependencies [[`100af928`](https://github.com/graphql/graphiql/commit/100af9284de18ca89524c646e86854313c5d067b), [`100af928`](https://github.com/graphql/graphiql/commit/100af9284de18ca89524c646e86854313c5d067b)]: + - @graphiql/react@0.9.0 + +## 1.11.2 + +### Patch Changes + +- Updated dependencies [[`62317e0b`](https://github.com/graphql/graphiql/commit/62317e0bae6d4ccf89d9e1e6607fd8feeb100078)]: + - @graphiql/react@0.8.0 + +## 1.11.1 + +### Patch Changes + +- Updated dependencies [[`ea732ea8`](https://github.com/graphql/graphiql/commit/ea732ea8e12272c998f1467af8b3b88b6b508e12)]: + - @graphiql/toolkit@0.6.1 + - @graphiql/react@0.7.1 + +## 1.11.0 + +### Minor Changes + +- [#2618](https://github.com/graphql/graphiql/pull/2618) [`4c814506`](https://github.com/graphql/graphiql/commit/4c814506183579b78731659d871cd4b0ba93305a) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a toolbar button for manually triggering introspection + +### Patch Changes + +- Updated dependencies [[`4c814506`](https://github.com/graphql/graphiql/commit/4c814506183579b78731659d871cd4b0ba93305a)]: + - @graphiql/react@0.7.0 + +## 1.10.0 + +### Minor Changes + +- [#2574](https://github.com/graphql/graphiql/pull/2574) [`0c98fa59`](https://github.com/graphql/graphiql/commit/0c98fa5924eadaee33713ccd8a9be6419d50cab1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Allow passing introspection data to the `schema` prop of the `GraphiQL` component + +### Patch Changes + +- Updated dependencies [[`0c98fa59`](https://github.com/graphql/graphiql/commit/0c98fa5924eadaee33713ccd8a9be6419d50cab1), [`0c98fa59`](https://github.com/graphql/graphiql/commit/0c98fa5924eadaee33713ccd8a9be6419d50cab1)]: + - @graphiql/react@0.6.0 + +## 1.9.13 + +### Patch Changes + +- Updated dependencies [[`f581b437`](https://github.com/graphql/graphiql/commit/f581b437e5bdab6f3ad817d230ee6d1b410bb591)]: + - @graphiql/react@0.5.2 + +## 1.9.12 + +### Patch Changes + +- Updated dependencies [[`08346cba`](https://github.com/graphql/graphiql/commit/08346cba136825341881f9dfefc62a60d748e0ee)]: + - @graphiql/react@0.5.1 + +## 1.9.11 + +### Patch Changes + +- [#2541](https://github.com/graphql/graphiql/pull/2541) [`788d84ef`](https://github.com/graphql/graphiql/commit/788d84ef2784188981f1b4cfb78fba24153bf0cb) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix the `onSchemaChange` prop, it is now again called after the schema is fetched (this was broken since v1.9.3) + +- Updated dependencies [[`8ce5b483`](https://github.com/graphql/graphiql/commit/8ce5b483ee190b5f5dd84eaf42e5d1359ce185e6), [`788d84ef`](https://github.com/graphql/graphiql/commit/788d84ef2784188981f1b4cfb78fba24153bf0cb)]: + - @graphiql/react@0.5.0 + +## 1.9.10 + +### Patch Changes + +- Updated dependencies [[`26e44120`](https://github.com/graphql/graphiql/commit/26e44120a18d49af451c97619fe3386a65579e05)]: + - @graphiql/react@0.4.3 + +## 1.9.9 + +### Patch Changes + +- [#2501](https://github.com/graphql/graphiql/pull/2501) [`5437ee61`](https://github.com/graphql/graphiql/commit/5437ee61e1ba6cd28ccc1cb3543df1ea788278f4) Thanks [@acao](https://github.com/acao)! - Allow Codemirror 5 `keyMap` to be defined, default `vim` or `emacs` allowed in addition to the original default of `sublime`. + +- Updated dependencies [[`5437ee61`](https://github.com/graphql/graphiql/commit/5437ee61e1ba6cd28ccc1cb3543df1ea788278f4), [`cccefa70`](https://github.com/graphql/graphiql/commit/cccefa70c0466d60e8496e1df61aeb1490af723c)]: + - @graphiql/react@0.4.2 + - graphql-language-service@5.0.6 + +## 1.9.8 + +### Patch Changes + +- [#2499](https://github.com/graphql/graphiql/pull/2499) [`731b3b72`](https://github.com/graphql/graphiql/commit/731b3b72e9f087a3b429ef5e8143219a0dcf7f00) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - fix the default value for the `headerEditorEnabled` prop to be `true` + +## 1.9.7 + +### Patch Changes + +- Updated dependencies [[`c9c51b8a`](https://github.com/graphql/graphiql/commit/c9c51b8a98e1f0427272d3e9ad60989b32f1a1aa)]: + - graphql-language-service@5.0.5 + - @graphiql/react@0.4.1 + +## 1.9.6 + +### Patch Changes + +- [#2475](https://github.com/graphql/graphiql/pull/2475) [`d6558e43`](https://github.com/graphql/graphiql/commit/d6558e43bd24a3af7c5f78dbae572bd8ca7b3995) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix using the `GraphiQL` export as type by exporting a class again + +* [#2461](https://github.com/graphql/graphiql/pull/2461) [`7dfe3ece`](https://github.com/graphql/graphiql/commit/7dfe3ece4e8ab6b3400888f7f357e394db63439d) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Use the `useDragResize` hook from `@graphiql/react` for the sizing of the editors and the docs explorer + +* Updated dependencies [[`7dfe3ece`](https://github.com/graphql/graphiql/commit/7dfe3ece4e8ab6b3400888f7f357e394db63439d)]: + - @graphiql/react@0.4.0 + +## 1.9.5 + +### Patch Changes + +- [#2453](https://github.com/graphql/graphiql/pull/2453) [`1b41e33c`](https://github.com/graphql/graphiql/commit/1b41e33c4a871a345836de58f415b7c461ced1f8) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add execution context to `@graphiql/react` and move over the logic from `graphiql` + +* [#2454](https://github.com/graphql/graphiql/pull/2454) [`a53bec64`](https://github.com/graphql/graphiql/commit/a53bec64b511fca2da828d7c0ff100e3a110aec1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Deprecate the public methods `getQueryEditor`, `getVariableEditor`, `getHeaderEditor`, and `refresh` on the `GraphiQL` class. + +- [#2451](https://github.com/graphql/graphiql/pull/2451) [`0659e96e`](https://github.com/graphql/graphiql/commit/0659e96e07f98d532619f29f52cba59e2d528327) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Always use the current value of the headers for the introspection request + +* [#2452](https://github.com/graphql/graphiql/pull/2452) [`ee0fd8bf`](https://github.com/graphql/graphiql/commit/ee0fd8bf4042053ec647080b83656dc5e54a7239) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move tab state from `graphiql` into editor context from `@graphiql/react` + +- [#2454](https://github.com/graphql/graphiql/pull/2454) [`a53bec64`](https://github.com/graphql/graphiql/commit/a53bec64b511fca2da828d7c0ff100e3a110aec1) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Continue forwarding the ref to the class component to not break public methods + +* [#2449](https://github.com/graphql/graphiql/pull/2449) [`a0b02eda`](https://github.com/graphql/graphiql/commit/a0b02edaa629c6113c1c5518fd3aa05b355a1921) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Assume all context values are nullable and create hooks to consume individual contexts + +- [#2450](https://github.com/graphql/graphiql/pull/2450) [`1e6fc68b`](https://github.com/graphql/graphiql/commit/1e6fc68b73941544ee64e0499e459f9c7d39aa14) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Extract the `copy`, `merge`, `prettify`, and `autoCompleteLeafs` functions into hooks and remove these functions from the editor context value + +- Updated dependencies [[`1b41e33c`](https://github.com/graphql/graphiql/commit/1b41e33c4a871a345836de58f415b7c461ced1f8), [`0659e96e`](https://github.com/graphql/graphiql/commit/0659e96e07f98d532619f29f52cba59e2d528327), [`ee0fd8bf`](https://github.com/graphql/graphiql/commit/ee0fd8bf4042053ec647080b83656dc5e54a7239), [`a0b02eda`](https://github.com/graphql/graphiql/commit/a0b02edaa629c6113c1c5518fd3aa05b355a1921), [`1e6fc68b`](https://github.com/graphql/graphiql/commit/1e6fc68b73941544ee64e0499e459f9c7d39aa14)]: + - @graphiql/react@0.3.0 + +## 1.9.4 + +### Patch Changes + +- [#2437](https://github.com/graphql/graphiql/pull/2437) [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move prettify query functionality to editor context in `@graphiql/react` + +* [#2435](https://github.com/graphql/graphiql/pull/2435) [`89f0244f`](https://github.com/graphql/graphiql/commit/89f0244f7b7cdf01c168638a09f5137788401995) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic for deriving operation facts from the current query to `@graphiql/react` and store these facts as properties on the query editor instance + +- [#2437](https://github.com/graphql/graphiql/pull/2437) [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move copy query functionality to editor context in `@graphiql/react` + +* [#2437](https://github.com/graphql/graphiql/pull/2437) [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move merge query functionality to editor context in `@graphiql/react` + +- [#2436](https://github.com/graphql/graphiql/pull/2436) [`3e5295f0`](https://github.com/graphql/graphiql/commit/3e5295f0fd3b5f999643ea97e6cee706554f0b50) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Inline logic for clicking a reference to open the docs and remove the `onClickReference` and `onHintInformationRender` props of the editor components and hooks + +* [#2436](https://github.com/graphql/graphiql/pull/2436) [`3e5295f0`](https://github.com/graphql/graphiql/commit/3e5295f0fd3b5f999643ea97e6cee706554f0b50) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move visibility state for doc explorer from `graphiql` to the explorer context in `@graphiql/react` + +* Updated dependencies [[`89f0244f`](https://github.com/graphql/graphiql/commit/89f0244f7b7cdf01c168638a09f5137788401995), [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789), [`89f0244f`](https://github.com/graphql/graphiql/commit/89f0244f7b7cdf01c168638a09f5137788401995), [`3dae62fc`](https://github.com/graphql/graphiql/commit/3dae62fc871385e148a799cde55a52a5e6b41d19), [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789), [`1f933505`](https://github.com/graphql/graphiql/commit/1f9335051fffc9e6a6f950b6f8060ed521b56789), [`3e5295f0`](https://github.com/graphql/graphiql/commit/3e5295f0fd3b5f999643ea97e6cee706554f0b50), [`3e5295f0`](https://github.com/graphql/graphiql/commit/3e5295f0fd3b5f999643ea97e6cee706554f0b50)]: + - @graphiql/react@0.2.1 + +## 1.9.3 + +### Patch Changes + +- [#2419](https://github.com/graphql/graphiql/pull/2419) [`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the `fillLeafs` utility function from `graphiql` into `@graphiql/toolkit` and deprecate the export from `graphiql` + +* [#2413](https://github.com/graphql/graphiql/pull/2413) [`8be164b1`](https://github.com/graphql/graphiql/commit/8be164b1e158d00752d6d3f30630a797d07d08c9) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a `StorageContext` and a `HistoryContext` to `@graphiql/react` that replaces the logic in the `graphiql` package + +- [#2419](https://github.com/graphql/graphiql/pull/2419) [`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the `mergeAst` utility function from `graphiql` into `@graphiql/toolkit` and deprecate the export from `graphiql` + +* [#2420](https://github.com/graphql/graphiql/pull/2420) [`3467cd33`](https://github.com/graphql/graphiql/commit/3467cd33264e0766a0a43cf53e52ec371df26962) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix sending multiple introspection requests when loading the page + +- [#2420](https://github.com/graphql/graphiql/pull/2420) [`3467cd33`](https://github.com/graphql/graphiql/commit/3467cd33264e0766a0a43cf53e52ec371df26962) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Deprecate the `autoCompleteLeafs` method of the `GraphiQL` component in favor of the function provided by the `EditorContext` from `@graphiql/react` + +* [#2420](https://github.com/graphql/graphiql/pull/2420) [`3467cd33`](https://github.com/graphql/graphiql/commit/3467cd33264e0766a0a43cf53e52ec371df26962) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a `SchemaContext` to `@graphiql/react` that replaces the logic for fetching and validating the schema in the `graphiql` package + +- [#2419](https://github.com/graphql/graphiql/pull/2419) [`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the `getSelectedOperationName` utility function from `graphiql` into `@graphiql/toolkit` and deprecate the export from `graphiql` + +- Updated dependencies [[`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e), [`8be164b1`](https://github.com/graphql/graphiql/commit/8be164b1e158d00752d6d3f30630a797d07d08c9), [`8be164b1`](https://github.com/graphql/graphiql/commit/8be164b1e158d00752d6d3f30630a797d07d08c9), [`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e), [`3467cd33`](https://github.com/graphql/graphiql/commit/3467cd33264e0766a0a43cf53e52ec371df26962), [`84d8985b`](https://github.com/graphql/graphiql/commit/84d8985b87701133cc41fd424a24bb61c9b7272e)]: + - @graphiql/toolkit@0.6.0 + - @graphiql/react@0.2.0 + +## 1.9.2 + +### Patch Changes + +- Updated dependencies [[`ebc864f0`](https://github.com/graphql/graphiql/commit/ebc864f0ab05000758cb2898daaa73a2f15255ec), [`ebc864f0`](https://github.com/graphql/graphiql/commit/ebc864f0ab05000758cb2898daaa73a2f15255ec)]: + - @graphiql/react@0.1.2 + +## 1.9.1 + +### Patch Changes + +- [#2423](https://github.com/graphql/graphiql/pull/2423) [`838e58da`](https://github.com/graphql/graphiql/commit/838e58dad652d8f5559af7b88d049b1c62348f2f) Thanks [@chentsulin](https://github.com/chentsulin)! - Fix peer dependency declaration by using `||` instead of `|` to link multiple major versions + +- Updated dependencies [[`838e58da`](https://github.com/graphql/graphiql/commit/838e58dad652d8f5559af7b88d049b1c62348f2f)]: + - @graphiql/react@0.1.1 + +## 1.9.0 + +### Minor Changes + +- [#2412](https://github.com/graphql/graphiql/pull/2412) [`c2e2f53d`](https://github.com/graphql/graphiql/commit/c2e2f53d3b2ae369feb68537f92c73bcfd962f29) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move QueryStore from `graphiql` package to `@graphiql/toolkit` + +* [#2412](https://github.com/graphql/graphiql/pull/2412) [`c2e2f53d`](https://github.com/graphql/graphiql/commit/c2e2f53d3b2ae369feb68537f92c73bcfd962f29) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move HistoryStore from `graphiql` package to `@graphiql/toolkit` + +- [#2409](https://github.com/graphql/graphiql/pull/2409) [`f2025ba0`](https://github.com/graphql/graphiql/commit/f2025ba06c5aa8e8ac68d29538ff135f3efc8e46) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic of the variable editor from the `graphiql` package into a hook `useVariableEditor` provided by `@graphiql/react` + +* [#2408](https://github.com/graphql/graphiql/pull/2408) [`d825bb75`](https://github.com/graphql/graphiql/commit/d825bb7569ca6b1ebbe534b893354645c790e003) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic of the query editor from the `graphiql` package into a hook `useQueryEditor` provided by `@graphiql/react` + +- [#2411](https://github.com/graphql/graphiql/pull/2411) [`ad448693`](https://github.com/graphql/graphiql/commit/ad4486934ba69247efd33ee500e30f8236ecd079) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move the logic of the result viewer from the `graphiql` package into a hook `useResponseEditor` provided by `@graphiql/react` + +* [#2370](https://github.com/graphql/graphiql/pull/2370) [`7f695b10`](https://github.com/graphql/graphiql/commit/7f695b104f9b25ba8c6d36f7827c475b297b7482) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Include the context provider for the explorer from `@graphiql/react` and replace the local state for the nav stack of the docs with methods provided by hooks from `@graphiql/react`. + +- [#2412](https://github.com/graphql/graphiql/pull/2412) [`c2e2f53d`](https://github.com/graphql/graphiql/commit/c2e2f53d3b2ae369feb68537f92c73bcfd962f29) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Move StorageAPI from `graphiql` package to `@graphiql/toolkit` + +* [#2404](https://github.com/graphql/graphiql/pull/2404) [`029ddf82`](https://github.com/graphql/graphiql/commit/029ddf82c29754ab8518ae7df66f9b25361a8247) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Add a context provider for editors and move the logic of the headers editor from the `graphiql` package into a hook `useHeaderEditor` provided by `@graphiql/react` + +### Patch Changes + +- [#2418](https://github.com/graphql/graphiql/pull/2418) [`6d7fb6e6`](https://github.com/graphql/graphiql/commit/6d7fb6e6fa4734e2274d8875971613a8254674e3) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - Fix persisting headers in tab state and avoid opening duplicate tabs when reloading + +- Updated dependencies [[`c2e2f53d`](https://github.com/graphql/graphiql/commit/c2e2f53d3b2ae369feb68537f92c73bcfd962f29), [`bc3dc64c`](https://github.com/graphql/graphiql/commit/bc3dc64c37478ba6170c49c25fb755b4f2e020b2), [`c2e2f53d`](https://github.com/graphql/graphiql/commit/c2e2f53d3b2ae369feb68537f92c73bcfd962f29), [`f2025ba0`](https://github.com/graphql/graphiql/commit/f2025ba06c5aa8e8ac68d29538ff135f3efc8e46), [`d825bb75`](https://github.com/graphql/graphiql/commit/d825bb7569ca6b1ebbe534b893354645c790e003), [`ad448693`](https://github.com/graphql/graphiql/commit/ad4486934ba69247efd33ee500e30f8236ecd079), [`7f695b10`](https://github.com/graphql/graphiql/commit/7f695b104f9b25ba8c6d36f7827c475b297b7482), [`c2e2f53d`](https://github.com/graphql/graphiql/commit/c2e2f53d3b2ae369feb68537f92c73bcfd962f29), [`029ddf82`](https://github.com/graphql/graphiql/commit/029ddf82c29754ab8518ae7df66f9b25361a8247)]: + - @graphiql/toolkit@0.5.0 + - @graphiql/react@0.1.0 + +## 1.8.10 + +### Patch Changes + +- [#2397](https://github.com/graphql/graphiql/pull/2397) [`a63ff958`](https://github.com/graphql/graphiql/commit/a63ff958838cf4fcf31f7eaa3e3b022d02838f65) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - upgrade to React v17 + +* [#2401](https://github.com/graphql/graphiql/pull/2401) [`60a744b1`](https://github.com/graphql/graphiql/commit/60a744b1d73d1021afb7abeea1573f26178102b5) Thanks [@thomasheyenbrock](https://github.com/thomasheyenbrock)! - move async helper functions and formatting functions over into the @graphiql/toolkit package + +* Updated dependencies [[`60a744b1`](https://github.com/graphql/graphiql/commit/60a744b1d73d1021afb7abeea1573f26178102b5), [`60a744b1`](https://github.com/graphql/graphiql/commit/60a744b1d73d1021afb7abeea1573f26178102b5)]: + - @graphiql/toolkit@0.4.5 + +## 1.8.9 + +### Patch Changes + +- [#2387](https://github.com/graphql/graphiql/pull/2387) [`e823697b`](https://github.com/graphql/graphiql/commit/e823697b5d47565671d5919be84f69919e70977f) Thanks [@benjie](https://github.com/benjie)! - Add 'children' type definition to various component props + +* [#2388](https://github.com/graphql/graphiql/pull/2388) [`d3ae074c`](https://github.com/graphql/graphiql/commit/d3ae074c9b9dae6ed4f69b0a79efaa0353dcea2d) Thanks [@benjie](https://github.com/benjie)! - Add 'pointer-events: none' to SVG style for dropdown arrow in GraphiQL.Menu component + +- [#2373](https://github.com/graphql/graphiql/pull/2373) [`5b2c1b20`](https://github.com/graphql/graphiql/commit/5b2c1b2054a70e8dca173f380f44766438cb5597) Thanks [@benjie](https://github.com/benjie)! - Fix TypeScript definition of FetcherParams to reflect that operationName is optional + +- Updated dependencies [[`5b2c1b20`](https://github.com/graphql/graphiql/commit/5b2c1b2054a70e8dca173f380f44766438cb5597)]: + - @graphiql/toolkit@0.4.4 + +## 1.8.8 + +### Patch Changes + +- Updated dependencies [[`2dec55f2`](https://github.com/graphql/graphiql/commit/2dec55f2c5e979cc7bb1adadff4fb063775b088c), [`d22f6111`](https://github.com/graphql/graphiql/commit/d22f6111a60af25727d8dbc1058c79607df76af2)]: + - codemirror-graphql@1.3.0 + - graphql-language-service@5.0.4 + +## 1.8.7 + +### Patch Changes + +- [#2316](https://github.com/graphql/graphiql/pull/2316) [`3d8510c8`](https://github.com/graphql/graphiql/commit/3d8510c87b9f0cc73f747ed4cd88e112f9fe65f7) Thanks [@AlirezaHaghshenas](https://github.com/AlirezaHaghshenas)! - Fix: With tabs enabled, if a subscription is restored from storage, a query request is sent instead + +## 1.8.6 + +### Patch Changes + +- [#2312](https://github.com/graphql/graphiql/pull/2312) [`3c97cf63`](https://github.com/graphql/graphiql/commit/3c97cf63f0d6a8c27265905af1a2da243925ff01) Thanks [@AlirezaHaghshenas](https://github.com/AlirezaHaghshenas)! - Fix: After changing to a tab with a subscription, graphiql sends a query request + +- Updated dependencies [[`45cbc759`](https://github.com/graphql/graphiql/commit/45cbc759c732999e8b1eb4714d6047ab77c17902)]: + - graphql-language-service@5.0.3 + - codemirror-graphql@1.2.17 + +## 1.8.5 + +### Patch Changes + +- Updated dependencies [[`c36504a8`](https://github.com/graphql/graphiql/commit/c36504a804d8cc54a5136340152999b4a1a2c69f)]: + - graphql-language-service@5.0.2 + - codemirror-graphql@1.2.16 + +## 1.8.4 + +### Patch Changes + +- [#2274](https://github.com/graphql/graphiql/pull/2274) [`12950380`](https://github.com/graphql/graphiql/commit/12950380e92c38f6eec23499e7fca5dc9dcd8216) Thanks [@B2o5T](https://github.com/B2o5T)! - turn `valid-typeof` as `error`, SSR fix + +- Updated dependencies [[`12950380`](https://github.com/graphql/graphiql/commit/12950380e92c38f6eec23499e7fca5dc9dcd8216)]: + - @graphiql/toolkit@0.4.3 + +## 1.8.3 + +### Patch Changes + +- [#2268](https://github.com/graphql/graphiql/pull/2268) [`b1886822`](https://github.com/graphql/graphiql/commit/b188682296ee04a87fbf09dc51385f127bffcec0) Thanks [@acao](https://github.com/acao)! - remove dependency on `global` for esbuild/etc users! + +* [#2265](https://github.com/graphql/graphiql/pull/2265) [`9458e10b`](https://github.com/graphql/graphiql/commit/9458e10ba24a6c919142ea1cebb409c7d055baf9) Thanks [@acao](https://github.com/acao)! - fix `codemirror` import bug for `onHasCompletion` for #2263. for esm/cjs users on autocomplete (umd bundle users not impacted) + +## 1.8.2 + +### Patch Changes + +- Updated dependencies [[`261f2044`](https://github.com/graphql/graphiql/commit/261f2044066412e40f9962bef55295f7c9c35aec)]: + - codemirror-graphql@1.2.15 + +## 1.8.1 + +### Patch Changes + +- [#2257](https://github.com/graphql/graphiql/pull/2257) [`6cc95851`](https://github.com/graphql/graphiql/commit/6cc9585119f33ba80f960da310f7ef2747b7bc38) Thanks [@acao](https://github.com/acao)! - _security fix:_ replace the vulnerable `dset` dependency with `set-value` + + `dset` is vulnerable to prototype pollution attacks. this is only possible if you are doing all of the following: + + 1. running graphiql with an experimental graphql-js release tag that supports @stream and @defer + 2. executing a properly @streamed or @deferred query ala IncrementalDelivery spec, with multipart chunks + 3. consuming a malicious schema that contains field names like proto, prototype, or constructor that return malicious data designed to exploit a prototype pollution attack + +## 1.8.0 + +### Minor Changes + +- [#2197](https://github.com/graphql/graphiql/pull/2197) [`3137a6c4`](https://github.com/graphql/graphiql/commit/3137a6c4333dad8db8a0eb980d6c6464c7292946) Thanks [@n1ru4l](https://github.com/n1ru4l)! - Now featuring: tabs! πŸ₯³ 🍾 just opt-in with new prop ``. You can also both opt-in and provide a handler via ``! + +### Patch Changes + +- [#2249](https://github.com/graphql/graphiql/pull/2249) [`1540fd3d`](https://github.com/graphql/graphiql/commit/1540fd3d0df553798e41a153c5f0386d9d52be01) Thanks [@acao](https://github.com/acao)! - Finally remove inline `require()` for codemirror addon imports, replace with modern dynamic `import()` (which enables `esbuild`, `vite`, etc). + + This change should allow your bundler to code split codemirror-graphql and the codemirror addons based on which you import. For SSR support, GraphiQL must load these modules dynamically. + + If you want to use other codemirror addons (vim, etc) for non-ssr you can just import them top level, or for SSR, you can just dynamically import them. + +## 1.7.2 + +### Patch Changes + +- Updated dependencies [[`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa), [`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa)]: + - graphql-language-service@5.0.1 + - codemirror-graphql@1.2.14 + +## 1.7.1 + +### Patch Changes + +- Updated dependencies [[`2502a364`](https://github.com/graphql/graphiql/commit/2502a364b74dc754d92baa1579b536cf42139958)]: + - graphql-language-service@5.0.0 + - codemirror-graphql@1.2.13 + +## 1.7.0 + +### Minor Changes + +- [#2221](https://github.com/graphql/graphiql/pull/2221) [`64826c87`](https://github.com/graphql/graphiql/commit/64826c8776dfc8394a65c98663d47cc3c9d397b9) Thanks [@dwwoelfel](https://github.com/dwwoelfel)! - Fix to trigger codemirror update when externalFragments prop changes [#2220](https://github.com/graphql/graphiql/pull/2220) + +* [#2213](https://github.com/graphql/graphiql/pull/2213) [`ba85bc24`](https://github.com/graphql/graphiql/commit/ba85bc242b8271cbd09ade9d69a93d86e4e1a49f) Thanks [@hatappi](https://github.com/hatappi)! - remove IE7 CSS star property hack + +### Patch Changes + +- [#2205](https://github.com/graphql/graphiql/pull/2205) [`91500d4e`](https://github.com/graphql/graphiql/commit/91500d4eba8b99bf779ff6ac899c814070c6dff3) Thanks [@francisu](https://github.com/francisu)! - Fixed problem where 'global' variable is referenced when it might not be present (#2155) + +## 1.6.0 + +### Minor Changes + +- [#2191](https://github.com/graphql/graphiql/pull/2191) [`eb8af7b5`](https://github.com/graphql/graphiql/commit/eb8af7b5666e7ed01497a862127011524fc400f5) Thanks [@n1ru4l](https://github.com/n1ru4l)! - Allow inserting content before the topBar element via the `beforeTopBarContent` property. + + ```jsx + } /> + ``` + +* [#2189](https://github.com/graphql/graphiql/pull/2189) [`96d47267`](https://github.com/graphql/graphiql/commit/96d4726716b782fcafa9d6c1671f3a3050ebe0b7) Thanks [@n1ru4l](https://github.com/n1ru4l)! - Apply variable editor title text styles via class `variable-editor-title-text` instead of using inline-styles. This allows better customization of styles. An active element also has the class `active`. This allows overriding the inactive state color using the selector `.graphiql-container .variable-editor-title-text` and overriding the active state color using the selector `.graphiql-container .variable-editor-title-text.active`. + +- [#2190](https://github.com/graphql/graphiql/pull/2190) [`d5179899`](https://github.com/graphql/graphiql/commit/d517989996cf6f33ef7e08d18a870e2bed565cca) Thanks [@n1ru4l](https://github.com/n1ru4l)! - New callback property `onSchemaChange` for `GraphiQL`. + + The callback is invoked with the successfully fetched schema from the remote. + + **Usage example:** + + ```tsx + console.log(schema)} /> + ``` + +## 1.5.20 + +### Patch Changes + +- Updated dependencies [[`484c0523`](https://github.com/graphql/graphiql/commit/484c0523cdd529f9e261d61a38616b6745075c7f), [`5852ba47`](https://github.com/graphql/graphiql/commit/5852ba47c720a2577817aed512bef9a262254f2c), [`48c5df65`](https://github.com/graphql/graphiql/commit/48c5df654e323cee3b8c57d7414247465235d1b5)]: + - graphql-language-service@4.1.5 + - codemirror-graphql@1.2.12 + +## 1.5.19 + +### Patch Changes + +- [#2167](https://github.com/graphql/graphiql/pull/2167) [`bc81f0ee`](https://github.com/graphql/graphiql/commit/bc81f0ee6d382fe996d92e55f90cdc3be10910a7) Thanks [@acao](https://github.com/acao)! - Fix legacy bug where global is expected + +## 1.5.18 + +### Patch Changes + +- [#2156](https://github.com/graphql/graphiql/pull/2156) [`ae5ea77b`](https://github.com/graphql/graphiql/commit/ae5ea77b4c2ec2a25e25c542ae72b2c3dabbe256) Thanks [@francisu](https://github.com/francisu)! - Fixed problem where 'global' variable is referenced when it might not be present (#2155) + +## 1.5.17 + +### Patch Changes + +- [#2138](https://github.com/graphql/graphiql/pull/2138) [`8700b4bb`](https://github.com/graphql/graphiql/commit/8700b4bbaadb17136f649f504c9575a8c853cd0b) Thanks [@danielleletarte](https://github.com/danielleletarte)! - Correctly render line breaks for Descriptions in Doc Explorer - #2137 - @danielleletarte + +## 1.5.16 + +### Patch Changes + +- Updated dependencies []: + - graphql-language-service@4.1.4 + - codemirror-graphql@1.2.11 + +## 1.5.15 + +### Patch Changes + +- Updated dependencies [[`a44772d6`](https://github.com/graphql/graphiql/commit/a44772d6af97254c4f159ea7237e842a3e3719e8)]: + - graphql-language-service@4.1.3 + - codemirror-graphql@1.2.10 + +## 1.5.14 + +### Patch Changes + +- Updated dependencies [[`e20760fb`](https://github.com/graphql/graphiql/commit/e20760fbd95c13d6d549cba3faa15a59aee9a2c0)]: + - graphql-language-service@4.1.2 + - codemirror-graphql@1.2.9 + +## 1.5.13 + +### Patch Changes + +- [#2097](https://github.com/graphql/graphiql/pull/2097) [`4d3eeaa4`](https://github.com/graphql/graphiql/commit/4d3eeaa4446c84e92cd77f213e454059602a72e5) Thanks [@acao](https://github.com/acao)! - Disable introspection of schema.description by default + +## 1.5.12 + +### Patch Changes + +- [#2091](https://github.com/graphql/graphiql/pull/2091) [`ff9cebe5`](https://github.com/graphql/graphiql/commit/ff9cebe515a3539f85b9479954ae644dfeb68b63) Thanks [@acao](https://github.com/acao)! - Fix graphql 15 related issues. Should now build & test interchangeably. + +- Updated dependencies [[`ff9cebe5`](https://github.com/graphql/graphiql/commit/ff9cebe515a3539f85b9479954ae644dfeb68b63)]: + - codemirror-graphql@1.2.8 + - graphql-language-service@4.1.1 + +## 1.5.11 + +### Patch Changes + +- Updated dependencies [[`0f1f90ce`](https://github.com/graphql/graphiql/commit/0f1f90ce8f4a25ddebdaf7a9ddbe136214aa64a3)]: + - graphql-language-service@4.1.0 + - codemirror-graphql@1.2.7 + +## 1.5.10 + +### Patch Changes + +- [#2087](https://github.com/graphql/graphiql/pull/2087) [`45a9075d`](https://github.com/graphql/graphiql/commit/45a9075d718046e0f17c930162fa9752dfe052ec) Thanks [@acao](https://github.com/acao)! - Fix issue with introspection in servers which don't support `inputValueDeprecation`. make `inputValueDeprecation` an opt-in prop for DocExplorer features + +## 1.5.9 + +### Patch Changes + +- [#2077](https://github.com/graphql/graphiql/pull/2077) [`701ca13f`](https://github.com/graphql/graphiql/commit/701ca13f625735564d71931e6d917e5bf69c8aa5) Thanks [@acao](https://github.com/acao)! - Include schema description in DocExplorer for schema introspection requests. Enables the `schemaDescription` option for `getIntrospectionQuery()`. Also includes `deprecationReason` support in DocExplorer for arguments! Enables `inputValueDeprecation` in `getIntrospectionQuery()` and displays deprecation section on field doc view. +- Updated dependencies [[`9df315b4`](https://github.com/graphql/graphiql/commit/9df315b44896efa313ed6744445fc8f9e702ebc3)]: + - graphql-language-service@4.0.0 + - codemirror-graphql@1.2.6 + +## 1.5.8 + +### Patch Changes + +- Updated dependencies [[`df57cd25`](https://github.com/graphql/graphiql/commit/df57cd2556302d6aa5dd140e7bee3f7bdab4deb1)]: + - graphql-language-service@3.2.5 + - codemirror-graphql@1.2.5 + +## 1.5.7 + +### Patch Changes + +- [`49bce429`](https://github.com/graphql/graphiql/commit/49bce429f0780a5e2856cfb7ccda50d10d38f724) [#2051](https://github.com/graphql/graphiql/pull/2051) Thanks [@willstott101](https://github.com/willstott101)! - Include source maps for minified JS and CSS in the graphiql package. + +## 1.5.6 + +### Patch Changes + +- Updated dependencies []: + - graphql-language-service@3.2.4 + - codemirror-graphql@1.2.4 + +## 1.5.5 + +### Patch Changes + +- Updated dependencies [[`c42b145f`](https://github.com/graphql/graphiql/commit/c42b145fffeaefbd1103bc7addee1873e939bc83)]: + - codemirror-graphql@1.2.3 + +## 1.5.4 + +### Patch Changes + +- [`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf) [#2047](https://github.com/graphql/graphiql/pull/2047) Thanks [@willstott101](https://github.com/willstott101)! - Source code included in all packages to fix source maps. codemirror-graphql includes esm build in package. + +- Updated dependencies [[`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf), [`8b486555`](https://github.com/graphql/graphiql/commit/8b486555e2aa4d90891070a1bbc52b59d9c670c4)]: + - codemirror-graphql@1.2.2 + - graphql-language-service@3.2.3 + +## 1.5.3 + +### Patch Changes + +- [`c83d1d4c`](https://github.com/graphql/graphiql/commit/c83d1d4c518ad1b0862aae5f46359dfaee00dda1) Thanks [@kikkupico](https://github.com/kikkupico)! - fix `schema` type nullability for #2028 + +* [`858907d2`](https://github.com/graphql/graphiql/commit/858907d2106742a65ec52eb017f2e91268cc37bf) [#2045](https://github.com/graphql/graphiql/pull/2045) Thanks [@acao](https://github.com/acao)! - fix graphql-js peer dependencies - [#2044](https://github.com/graphql/graphiql/pull/2044) + +* Updated dependencies [[`858907d2`](https://github.com/graphql/graphiql/commit/858907d2106742a65ec52eb017f2e91268cc37bf)]: + - codemirror-graphql@1.2.1 + - @graphiql/toolkit@0.4.2 + - graphql-language-service@3.2.2 + +## 1.5.2 + +### Patch Changes + +- Updated dependencies [[`dec207e7`](https://github.com/graphql/graphiql/commit/dec207e74f0506db069482cc30f8cd1f045d8107), [`b79bf304`](https://github.com/graphql/graphiql/commit/b79bf304045add4b5c3b2539dd6b551a64e6ed87), [`d0c22c4f`](https://github.com/graphql/graphiql/commit/d0c22c4fce5ea39611c7ecee553943fdf27fd03e)]: + - @graphiql/toolkit@0.4.1 + - codemirror-graphql@1.2.0 + +## 1.5.1 + +### Patch Changes + +- [`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce) [#2013](https://github.com/graphql/graphiql/pull/2013) Thanks [@PabloSzx](https://github.com/PabloSzx)! - Update utils + +- Updated dependencies [[`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce)]: + - graphql-language-service@3.2.1 + +## 1.5.0 + +### Minor Changes + +- [`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a) [#2010](https://github.com/graphql/graphiql/pull/2010) Thanks [@acao](https://github.com/acao)! - upgrade to `graphql@16.0.0-experimental-stream-defer.5`. thanks @saihaj! + +### Patch Changes + +- Updated dependencies [[`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a)]: + - codemirror-graphql@1.1.0 + - @graphiql/toolkit@0.4.0 + - graphql-language-service@3.2.0 + +## 1.4.8 + +### Patch Changes + +- [`e63696de`](https://github.com/graphql/graphiql/commit/e63696de57a85c34d937bfb53345e2e0d0b874a4) [#2005](https://github.com/graphql/graphiql/pull/2005) Thanks [@acao](https://github.com/acao)! - Correct the npm readme security fix version number and links, thanks [@glasser](https://github.com/glasser) & [@dotansimha](https://github.com/dotansimha)! + +## 1.4.7 + +### Patch Changes + +- [`130ddad6`](https://github.com/graphql/graphiql/commit/130ddad6d0394356ec32070a6fee1840450a4660) Thanks [@acao](https://github.com/acao)! - **CRITICAL SECURITY PATCH** for the [GraphiQL introspection schema template injection attack](https://github.com/graphql/graphiql/security/advisories/GHSA-x4r7-m2q9-69c8) + +## 1.4.6 + +### Patch Changes + +- [`d3a88283`](https://github.com/graphql/graphiql/commit/d3a88283c7b618376ad4a06c7db20e60b066d1a0) [#1934](https://github.com/graphql/graphiql/pull/1934) Thanks [@tonyfromundefined](https://github.com/tonyfromundefined)! - add react 17, 18 in peerDependencies + +* [`afaa36c1`](https://github.com/graphql/graphiql/commit/afaa36c198648e84f305986a0b1dfefa97e70221) [#1883](https://github.com/graphql/graphiql/pull/1883) Thanks [@Sweetabix1](https://github.com/Sweetabix1)! - Updating font colors for line numbers, comments & brackets from #999 to #666 for accessibility purposes. #666 passes AA accessibility standards for small text, with a contrast ratio of over 5:1. + +- [`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8) [#1777](https://github.com/graphql/graphiql/pull/1777) Thanks [@dwwoelfel](https://github.com/dwwoelfel)! - adopt block string parsing for variables in language parser + +- Updated dependencies [[`0e2c1a02`](https://github.com/graphql/graphiql/commit/0e2c1a020cc2761155f7c9467d3ed4cb45941aeb), [`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8)]: + - graphql-language-service@3.1.6 + - codemirror-graphql@1.0.3 + +## 1.4.5 + +### Patch Changes + +- [`86795d5f`](https://github.com/graphql/graphiql/commit/86795d5ffa2d3e6c8aee74f761d02f054b428d46) Thanks [@acao](https://github.com/acao)! - Remove bad type definition from `subscriptions-transport-ws` #1992 closes #1989 + +- Updated dependencies [[`86795d5f`](https://github.com/graphql/graphiql/commit/86795d5ffa2d3e6c8aee74f761d02f054b428d46)]: + - @graphiql/toolkit@0.3.2 + +## 1.4.4 + +### Patch Changes + +- [`62e786b5`](https://github.com/graphql/graphiql/commit/62e786b57cc5748eccac59814dfc8ecd0104c748) [#1990](https://github.com/graphql/graphiql/pull/1990) Thanks [@acao](https://github.com/acao)! - Remove type definition from `subscriptions-transport-ws` + +- Updated dependencies [[`62e786b5`](https://github.com/graphql/graphiql/commit/62e786b57cc5748eccac59814dfc8ecd0104c748)]: + - @graphiql/toolkit@0.3.1 + +## 1.4.3 + +### Patch Changes + +- [`6a459f4c`](https://github.com/graphql/graphiql/commit/6a459f4c235bb0d70725ae6ad7fc1cfa34f49dca) [#1968](https://github.com/graphql/graphiql/pull/1968) Thanks [@acao](https://github.com/acao)! - Remove `optionalDependencies` entirely, remove `subscriptions-transport-ws` which introduces vulnerabilities, upgrade `@n1ru4l/push-pull-async-iterable-iterator` to 3.0.0, upgrade `graphql-ws` several minor versions - the `graphql-ws@5.x` upgrade will come in a later minor release. + +* [`eb2d91fa`](https://github.com/graphql/graphiql/commit/eb2d91fa8e4a03cb5663f27f724db2c95989a40f) [#1914](https://github.com/graphql/graphiql/pull/1914) Thanks [@harshithpabbati](https://github.com/harshithpabbati)! - fix: history can now be saved even when query history panel is not opened feat: create a new maxHistoryLength prop to allow more than 20 queries in history panel + +- [`04fad79c`](https://github.com/graphql/graphiql/commit/04fad79c094318d4b4c9e0250c5cff55d9fc5116) [#1889](https://github.com/graphql/graphiql/pull/1889) Thanks [@henryqdineen](https://github.com/henryqdineen)! - feat: export ToolbarSelectOption and ToolbarMenuItem + +* [`cd685435`](https://github.com/graphql/graphiql/commit/cd6854352ac6beff57af76db7de38e8157ff13aa) [#1923](https://github.com/graphql/graphiql/pull/1923) Thanks [@cgarnier](https://github.com/cgarnier)! - Fix result window theme + +* Updated dependencies [[`6a459f4c`](https://github.com/graphql/graphiql/commit/6a459f4c235bb0d70725ae6ad7fc1cfa34f49dca), [`2fd5bf72`](https://github.com/graphql/graphiql/commit/2fd5bf7239edb78339e5ac7211f09c245e47c3bb)]: + - @graphiql/toolkit@0.3.0 + - graphql-language-service@3.1.5 + +## 1.4.2 + +### Patch Changes + +- [`5b8a057d`](https://github.com/graphql/graphiql/commit/5b8a057dd64ebecc391be32176a2403bb9d9ff92) [#1838](https://github.com/graphql/graphiql/pull/1838) Thanks [@acao](https://github.com/acao)! - Set all cross-runtime build targets to es6 + +## 1.4.1 + +### Patch Changes + +- [`9f8c78ce`](https://github.com/graphql/graphiql/commit/9f8c78ce8c72a9dcf35b3e82bd3129ac17d845e6) [#1821](https://github.com/graphql/graphiql/pull/1821) Thanks [@harshithpabbati](https://github.com/harshithpabbati)! - fix: render query history panel only when it's toggled, instead of hiding with CSS + +* [`dd9397e4`](https://github.com/graphql/graphiql/commit/dd9397e4c693b5ceadbd26d6fa92aa6246aac9c3) [#1819](https://github.com/graphql/graphiql/pull/1819) Thanks [@acao](https://github.com/acao)! - `GraphiQL.createClient()` accepts custom `legacyClient`, exports typescript types, fixes #1800. + + `createGraphiQLFetcher` now only attempts an `graphql-ws` connection when only `subscriptionUrl` is provided. In order to use `graphql-transport-ws`, you'll need to provide the `legacyClient` option only, and no `subscriptionUrl` or `wsClient` option. + +- [`1f92d1dc`](https://github.com/graphql/graphiql/commit/1f92d1dcc0102bdec078263b87ca20cd670a1c86) [#1804](https://github.com/graphql/graphiql/pull/1804) Thanks [@maraisr](https://github.com/maraisr)! - Fixes issue where with IncrementalDelivery directives objects wouldn't deep-merge. + +* [`6869ce77`](https://github.com/graphql/graphiql/commit/6869ce7767050787db5f1017abf82fa5a52fc97a) [#1816](https://github.com/graphql/graphiql/pull/1816) Thanks [@acao](https://github.com/acao)! - improve peer resolutions for graphql 14 & 15. `14.5.0` minimum is for built-in typescript types, and another method only available in `14.4.0` + +* Updated dependencies [[`dd9397e4`](https://github.com/graphql/graphiql/commit/dd9397e4c693b5ceadbd26d6fa92aa6246aac9c3), [`6869ce77`](https://github.com/graphql/graphiql/commit/6869ce7767050787db5f1017abf82fa5a52fc97a)]: + - @graphiql/toolkit@0.2.0 + +## 1.4.0 + +### Patch Changes + +- Updated dependencies [[`b4fc16c0`](https://github.com/graphql/graphiql/commit/b4fc16c025da6f466727dc17cab6026d14c6e7fe)]: + - codemirror-graphql@1.0.0 + +## 1.4.0 + +### Bugfixes + +- Fixes the search icon misalignment. (#1776) by [@iifawzi](https://github.com/iifawzi) +- run `onToggleDocs` when setting `docExplorerOpen` to false (#1768) by [@ChiragKasat](https://github.com/ChiragKasat) + +### Minor Changes + +- 1c119386: `@defer`, `@stream`, and `graphql-ws` support in a `createGraphiQLFetcher` utility (#1770) + + - support for `@defer` and `@stream` in `GraphiQL` itself on fetcher execution and when handling stream payloads + - introduce `@graphiql/toolkit` for types and utilities used to compose `GraphiQL` and other related libraries + - introduce `@graphiql/create-fetcher` to accept simplified parameters to generate a `fetcher` that covers the most commonly used `graphql-over-http` transport spec proposals. using `meros` for multipart http, and `graphql-ws` for websockets subscriptions. + - use `graphql` and `graphql-express` `experimental-defer-stream` branch in development until it's merged + - add cypress e2e tests for `@stream` in different scenarios + - add some unit tests for `createGraphiQLFetcher` + +### Patch Changes + +- Updated dependencies [1c119386] + - @graphiql/create-fetcher@0.1.0 + - @graphiql/toolkit@0.1.0 + +## [1.3.2](https://github.com/graphql/graphiql/compare/graphiql@1.3.1...graphiql@1.3.2) (2021-01-07) + +**Note:** Version bump only for package graphiql + +## [1.3.1](https://github.com/graphql/graphiql/compare/graphiql@1.3.0...graphiql@1.3.1) (2021-01-07) + +**Note:** Version bump only for package graphiql + +## [1.3.0](https://github.com/graphql/graphiql/compare/graphiql@1.2.2...graphiql@1.3.0) (2021-01-07) + +### Features + +- also support fetcher functions that return Promise or Promise ([#1739](https://github.com/graphql/graphiql/issues/1739)) ([a804f3c](https://github.com/graphql/graphiql/commit/a804f3c011e7cafb4f8a48a1ba101b875be3540d)) +- implied or external fragments, for [#612](https://github.com/graphql/graphiql/issues/612) ([#1750](https://github.com/graphql/graphiql/issues/1750)) ([cfed265](https://github.com/graphql/graphiql/commit/cfed265e3cf31875b39ea517781a217fcdfcadc2)) + +## [1.2.2](https://github.com/graphql/graphiql/compare/graphiql@1.2.1...graphiql@1.2.2) (2021-01-03) + +**Note:** Version bump only for package graphiql + +## [1.2.1](https://github.com/graphql/graphiql/compare/graphiql@1.2.0...graphiql@1.2.1) (2020-12-28) + +### Bug Fixes + +- display schema description if available ([050c506](https://github.com/graphql/graphiql/commit/050c506ed4ed2852bf9a5b099f967928d9856156)) +- fix linting issue ([7117b7c](https://github.com/graphql/graphiql/commit/7117b7ccd2a2872e0051c8751252040d4042e190)) + +## [1.2.0](https://github.com/graphql/graphiql/compare/graphiql@1.1.0...graphiql@1.2.0) (2020-12-08) + +### Features + +- add AsyncIterable support to fetcher function ([#1724](https://github.com/graphql/graphiql/issues/1724)) ([a568af3](https://github.com/graphql/graphiql/commit/a568af3674404b8a15055792c2c35128b2bd711c)) +- provide validation rules via props ([#1716](https://github.com/graphql/graphiql/issues/1716)) ([0c5785c](https://github.com/graphql/graphiql/commit/0c5785c82adbd4affb25300ae2d128b42c9b81fe)) + +## [1.1.0](https://github.com/graphql/graphiql/compare/graphiql@1.0.6...graphiql@1.1.0) (2020-11-28) + +### Bug Fixes + +- improve props in GraphiQL readme ([b9b2c8d](https://github.com/graphql/graphiql/commit/b9b2c8d8bde6064a4cdcb01911b024602fcdbe9f)) + +### Features + +- **graphiql:** add prop for adding toolbar content while preserving the default buttons ([ea81056](https://github.com/graphql/graphiql/commit/ea81056e09b0a95e1536c79fab27e027739808c4)) +- deeper fragment merging ([238d0b5](https://github.com/graphql/graphiql/commit/238d0b5e52cfa9354757c9d52050692d152aae21)) + +## [1.0.6](https://github.com/graphql/graphiql/compare/graphiql@1.0.5...graphiql@1.0.6) (2020-10-20) + +### Bug Fixes + +- enable variable editor when header editor is not enabled ([#1682](https://github.com/graphql/graphiql/issues/1682)) ([205fbad](https://github.com/graphql/graphiql/commit/205fbad84806d175d66a6f5598e0a0f521129a16)) + +## [1.0.5](https://github.com/graphql/graphiql/compare/graphiql@1.0.4...graphiql@1.0.5) (2020-09-18) + +**Note:** Version bump only for package graphiql + +## [1.0.4](https://github.com/graphql/graphiql/compare/graphiql@2.0.0-alpha.5...graphiql@1.0.4) (2020-09-11) + +### Bug Fixes + +- don't use initial query on every re-render ([#1663](https://github.com/graphql/graphiql/issues/1663)) ([5aa890f](https://github.com/graphql/graphiql/commit/5aa890f6e145a7ad49f82cc122e209a291060709)) + +## [1.0.3](https://github.com/graphql/graphiql/compare/graphiql@1.0.2...graphiql@1.0.3) (2020-06-24) + +### Bug Fixes + +- headers tab - highlighting and schema fetch ([#1593](https://github.com/graphql/graphiql/issues/1593)) ([0d050ca](https://github.com/graphql/graphiql/commit/0d050caeb5278799f2b1c206d0c61f3ac768e7cd)) + +## [1.0.2](https://github.com/graphql/graphiql/compare/graphiql@1.0.1...graphiql@1.0.2) (2020-06-19) + +**Note:** Version bump only for package graphiql + +## [1.0.1](https://github.com/graphql/graphiql/compare/graphiql@1.0.0...graphiql@1.0.1) (2020-06-17) + +### Bug Fixes + +- more server side rendering fixes ([#1581](https://github.com/graphql/graphiql/issues/1581)) ([881a19f](https://github.com/graphql/graphiql/commit/881a19fbd5fbe5f65678de8074e593be7deb2ede)), closes [#1573](https://github.com/graphql/graphiql/issues/1573) +- network cancellation for 1.0 ([#1582](https://github.com/graphql/graphiql/issues/1582)) ([ad3cc0d](https://github.com/graphql/graphiql/commit/ad3cc0d1567ea49ff5677d4cd8524e5e072b605e)) +- Set headers to localStorage ([#1578](https://github.com/graphql/graphiql/issues/1578)) ([cc7a7e2](https://github.com/graphql/graphiql/commit/cc7a7e2f6d25d7e8150dc89c6984e6a04b01566b)) + +## [1.0.0](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.13...graphiql@1.0.0) (2020-06-11) + +### Bug Fixes + +- call debounce statements as they are functions ([#1571](https://github.com/graphql/graphiql/issues/1571)) ([8541250](https://github.com/graphql/graphiql/commit/85412501307ccfffe258b7fbca74bb9309726a73)) +- fix server side rendering by using type only codemirror import ([#1573](https://github.com/graphql/graphiql/issues/1573)) ([1ee60a6](https://github.com/graphql/graphiql/commit/1ee60a6db87d54c7a1e8f1089e52a65f335351b6)), closes [#118](https://github.com/graphql/graphiql/issues/118) +- Move all componentWillUnMount functionality to respective events ([#1544](https://github.com/graphql/graphiql/issues/1544)) ([046b09f](https://github.com/graphql/graphiql/commit/046b09f541e6a9f2ce4b46de590d49c04c916716)) + +## [1.0.0-alpha.13](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.12...graphiql@1.0.0-alpha.13) (2020-06-04) + +**Note:** Version bump only for package graphiql + +## [1.0.0-alpha.12](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.11...graphiql@1.0.0-alpha.12) (2020-06-04) + +### Bug Fixes + +- cleanup cache entry from lerna publish ([4a26218](https://github.com/graphql/graphiql/commit/4a2621808a1aea8b30d5d27b8d86a60bf2b44b01)) +- display variable editor when headers are not enabled ([ce7b2e2](https://github.com/graphql/graphiql/commit/ce7b2e2b45d530b61e916112e864074cf3a6ddc7)) + +## [1.0.0-alpha.11](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.10...graphiql@1.0.0-alpha.11) (2020-05-28) + +### Bug Fixes + +- Safe setState ([#1547](https://github.com/graphql/graphiql/issues/1547)) ([f85969c](https://github.com/graphql/graphiql/commit/f85969c7e77e8fd269e026be36cc5065d6d33237)) +- trigger edit variables on first render ([#1545](https://github.com/graphql/graphiql/issues/1545)) ([e54e1a8](https://github.com/graphql/graphiql/commit/e54e1a8691483f1d336231314130d9822481b3be)) + +### Features + +- Add Headers Editor to GraphiQL ([#1543](https://github.com/graphql/graphiql/issues/1543)) ([3faa1ac](https://github.com/graphql/graphiql/commit/3faa1ac46514252e90abf2b2bda0841edf6115ea)) + +## [1.0.0-alpha.10](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.9...graphiql@1.0.0-alpha.10) (2020-05-19) + +### Bug Fixes + +- graphiql non-relative import issues ([#1534](https://github.com/graphql/graphiql/issues/1534)) fixes [#1530](https://github.com/graphql/graphiql/issues/1530) ([0ac9fa0](https://github.com/graphql/graphiql/commit/0ac9fa0a8dcdf8464c8ce31c487ebcfd6b9536a8)) + +## [1.0.0-alpha.9](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.8...graphiql@1.0.0-alpha.9) (2020-05-17) + +### Bug Fixes + +- remove problematic file resolution module from webpack sco… ([#1489](https://github.com/graphql/graphiql/issues/1489)) ([8dab038](https://github.com/graphql/graphiql/commit/8dab0385772f443f73b559e2c668080733168236)) + +### Features + +- introduce proper vscode completion kinds ([#1488](https://github.com/graphql/graphiql/issues/1488)) ([f19aa0d](https://github.com/graphql/graphiql/commit/f19aa0ddde6109526c101c8a487f43bbb8238394)) +- Monaco Mode - Phase 2 - Mode & Worker ([#1459](https://github.com/graphql/graphiql/issues/1459)) ([bc95fb4](https://github.com/graphql/graphiql/commit/bc95fb46459a4437ff9471ff43c98e1c5c50f51e)) + +## [1.0.0-alpha.8](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.7...graphiql@1.0.0-alpha.8) (2020-04-10) + +**Note:** Version bump only for package graphiql + +## [1.0.0-alpha.7](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.6...graphiql@1.0.0-alpha.7) (2020-04-10) + +**Note:** Version bump only for package graphiql + +## [1.0.0-alpha.6](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.5...graphiql@1.0.0-alpha.6) (2020-04-10) + +**Note:** Version bump only for package graphiql + +## [1.0.0-alpha.5](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.4...graphiql@1.0.0-alpha.5) (2020-04-06) + +### Features + +- upgrade to graphql@15.0.0 for [#1191](https://github.com/graphql/graphiql/issues/1191) ([#1204](https://github.com/graphql/graphiql/issues/1204)) ([f13c8e9](https://github.com/graphql/graphiql/commit/f13c8e9d0e66df4b051b332c7d02f4bb83e07ffd)) + +## [1.0.0-alpha.4](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.3...graphiql@1.0.0-alpha.4) (2020-04-03) + +### Bug Fixes + +- fix query argument missing from onEditQuery call ([#1440](https://github.com/graphql/graphiql/issues/1440)) ([6c335a8](https://github.com/graphql/graphiql/commit/6c335a813f6101afded00c0e869c337a7ca44020)) + +## [1.0.0-alpha.3](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.2...graphiql@1.0.0-alpha.3) (2020-03-20) + +**Note:** Version bump only for package graphiql + +## [1.0.0-alpha.2](https://github.com/graphql/graphiql/compare/graphiql@1.0.0-alpha.0...graphiql@1.0.0-alpha.2) (2020-03-20) + +### Bug Fixes + +- Fix typo in documentation (comments) ([#1431](https://github.com/graphql/graphiql/issues/1431)) ([fdda8f0](https://github.com/graphql/graphiql/commit/fdda8f04479412d22e9a3e9215c7caa5369e7d83)) +- initial request cache set, import tsc bugs ([#1266](https://github.com/graphql/graphiql/issues/1266)) ([6b98f8a](https://github.com/graphql/graphiql/commit/6b98f8a442d4a8ea160fb90a29acf33f5382db2e)) + +## [1.0.0-alpha.1](https://github.com/graphql/graphiql/compare/graphiql@0.17.5...graphiql@1.0.0-alpha.1) (2020-01-18) + +### Bug Fixes + +- hmr, file resolution warnings ([69bf701](https://github.com/graphql/graphiql/commit/69bf701)) +- prefer displayName over type equality for children overrides ([e4cec0a](https://github.com/graphql/graphiql/commit/e4cec0a)) + - remove use of `findDOMNode` ([0b12323](https://github.com/graphql/graphiql/commit/0b12323)) by [@ryan-m-walker](https://github.com/ryan-m-walker) + +### Features + +- deprecate support for 15, support react 16 features ([#1107](https://github.com/graphql/graphiql/issues/1107)) ([bc4b6fc](https://github.com/graphql/graphiql/commit/bc4b6fc)) +- **graphiql-theming:** Toolbar component ([#1203](https://github.com/graphql/graphiql/issues/1203)) by [@walaura](https://github.com/walaura) ([adb73f5](https://github.com/graphql/graphiql/commit/adb73f5)) +- [new-ui] Tabs & Tab-bars ([#1198](https://github.com/graphql/graphiql/issues/1198)) ([033f971](https://github.com/graphql/graphiql/commit/033f971)) by [@walaura](https://github.com/walaura) +- replace use of enzyme with react-testing-library ([#1144](https://github.com/graphql/graphiql/issues/1144)) by [@ryan-m-walker](https://github.com/ryan-m-walker) ([de73d6c](https://github.com/graphql/graphiql/commit/de73d6c)) +- storybook+theme-ui for the new design ([#1145](https://github.com/graphql/graphiql/issues/1145)) ([7f97c0c](https://github.com/graphql/graphiql/commit/7f97c0c)) by [@walaura](https://github.com/walaura) + +### BREAKING CHANGES + +- Deprecate support for React 15. Please use React 16.8 or greater for hooks support. Co-authored-by: @ryan-m-walker, @acao Reviewed-by: @benjie + +## [0.17.5](https://github.com/graphql/graphiql/compare/graphiql@0.17.4...graphiql@0.17.5) (2019-12-09) + +**Note:** Version bump only for package graphiql + +## [0.17.4](https://github.com/graphql/graphiql/compare/graphiql@0.17.3...graphiql@0.17.4) (2019-12-09) + +### Bug Fixes + +- graphiql babel test ignore paths ([e1588d9](https://github.com/graphql/graphiql/commit/e1588d9)) + +## [0.17.3](https://github.com/graphql/graphiql/compare/graphiql@0.17.2...graphiql@0.17.3) (2019-12-09) + +### Bug Fixes + +- express-graphql version ([e9848b0](https://github.com/graphql/graphiql/commit/e9848b0)) +- test output, webpack resolution, clean build ([3b1c2c1](https://github.com/graphql/graphiql/commit/3b1c2c1)) + +## [0.17.2](https://github.com/graphql/graphiql/compare/graphiql@0.17.1...graphiql@0.17.2) (2019-12-03) + +### Bug Fixes + +- ensure css files move with babel dist ([ca95547](https://github.com/graphql/graphiql/commit/ca95547)) +- remove css from downstream components. soon to be replaced w styled ([e765543](https://github.com/graphql/graphiql/commit/e765543)) + +## [0.17.1](https://github.com/graphql/graphiql/compare/graphiql@0.17.0...graphiql@0.17.1) (2019-12-03) + +### Bug Fixes + +- **graphiql:** duplicate query history key issue, fixes [#988](https://github.com/graphql/graphiql/issues/988) ([#1035](https://github.com/graphql/graphiql/issues/1035)) ([69c6826](https://github.com/graphql/graphiql/commit/69c6826)) +- convert browserify build to webpack, fixes [#976](https://github.com/graphql/graphiql/issues/976) ([#1001](https://github.com/graphql/graphiql/issues/1001)) ([3caf041](https://github.com/graphql/graphiql/commit/3caf041)) +- hints vertical scroll ([216eaeb](https://github.com/graphql/graphiql/commit/216eaeb)) + +## [0.17.0](https://github.com/graphql/graphiql/compare/graphiql@0.16.0...graphiql@0.17.0) (2019-11-26) + +### Bug Fixes + +- security bump, resolves [#1004](https://github.com/graphql/graphiql/issues/1004), SNYK-JS-MARKDOWNIT-459438 ([89c83db](https://github.com/graphql/graphiql/commit/89c83db)) +- webpack resolutions for [#882](https://github.com/graphql/graphiql/issues/882), add webpack example ([ea9df3e](https://github.com/graphql/graphiql/commit/ea9df3e)) + +### Features + +- **graphiql:** Prettify also formats query variables ([b7d0bfd](https://github.com/graphql/graphiql/commit/b7d0bfd)) + +## [0.16.0](https://github.com/graphql/graphiql/compare/graphiql@0.15.1...graphiql@0.16.0) (2019-10-19) + +### Bug Fixes + +- **accessibility:** improve accessibility of all components ([#967](https://github.com/graphql/graphiql/issues/967)) ([73a3f90](https://github.com/graphql/graphiql/commit/73a3f90)) +- **css:** added minimum width for result panel in GraphiQL ([#980](https://github.com/graphql/graphiql/issues/980)) ([0c8b7ad](https://github.com/graphql/graphiql/commit/0c8b7ad)) +- **graphiql:** better quota management ([#764](https://github.com/graphql/graphiql/issues/764)) ([7efed6c](https://github.com/graphql/graphiql/commit/7efed6c)) + +### Features + +- **css:** beautify code tag in doc explorer ([#959](https://github.com/graphql/graphiql/issues/959)) resolves [#949](https://github.com/graphql/graphiql/issues/949) ([30810a2](https://github.com/graphql/graphiql/commit/30810a2)) + +### [0.15.1](https://github.com/graphql/graphiql/compare/graphiql@0.15.0...graphiql@0.15.1) (2019-10-04) + +### Bug Fixes + +- build tweaks ([0bc6a7c](https://github.com/graphql/graphiql/commit/0bc6a7c)) + +## 0.15.0 (2019-10-04) + +### Bug Fixes + +- check `window` is defined before using it ([#962](https://github.com/graphql/graphiql/issues/962)) ([e4866ad](https://github.com/graphql/graphiql/commit/e4866ad)) +- **graphiql:** prettify keybinding bug for Firefox. Fixes [#905](https://github.com/graphql/graphiql/issues/905) ([fdf98ba](https://github.com/graphql/graphiql/commit/fdf98ba)) +- check `this.editor` exist before `this.editor.off` in QueryEditor ([#669](https://github.com/graphql/graphiql/issues/669)) ([ca226ee](https://github.com/graphql/graphiql/commit/ca226ee)), closes [#665](https://github.com/graphql/graphiql/issues/665) +- extraKeys bugfix window regression ([f3d0427](https://github.com/graphql/graphiql/commit/f3d0427)) +- preserve ctrl-f key for macOS ([7c381f9](https://github.com/graphql/graphiql/commit/7c381f9)) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 0.13.2 (2019-06-21) + +## 0.14.3 (2019-09-01) + +### Bug Fixes + +- check `this.editor` exist before `this.editor.off` in QueryEditor ([#669](https://github.com/graphql/graphiql/issues/669)) ([ca226ee](https://github.com/graphql/graphiql/commit/ca226ee)), closes [#665](https://github.com/graphql/graphiql/issues/665) +- extraKeys bugfix window regression ([f3d0427](https://github.com/graphql/graphiql/commit/f3d0427)) +- preserve ctrl-f key for macOS ([7c381f9](https://github.com/graphql/graphiql/commit/7c381f9)) +- remove newline ([19f5d1d](https://github.com/graphql/graphiql/commit/19f5d1d)) + +## 0.13.2 (2019-06-21) + +## 0.13.2 (2019-06-21) diff --git a/packages/graphiql/LICENSE b/packages/graphiql/LICENSE new file mode 100644 index 00000000000..7802f239a32 --- /dev/null +++ b/packages/graphiql/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 GraphQL Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/graphiql/README.md b/packages/graphiql/README.md new file mode 100644 index 00000000000..0a22f7216c2 --- /dev/null +++ b/packages/graphiql/README.md @@ -0,0 +1,209 @@ +# GraphiQL + +> **Security Notice:** All versions of `graphiql` < `1.4.3` are vulnerable to an +> XSS attack in cases where the GraphQL server to which the GraphiQL web app +> connects is not trusted. Learn more in +> [our security advisory](https://github.com/graphql/graphiql/tree/main/docs/security/2021-introspection-schema-xss.md). + +[![NPM](https://img.shields.io/npm/v/graphiql.svg)](https://npmjs.com/graphiql) +![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hm/graphiql) +![npm downloads](https://img.shields.io/npm/dm/graphiql?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/graphiql) +![npm bundle size (version)](https://img.shields.io/bundlephobia/min/graphiql/latest) +![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/graphiql/latest) +[![License](https://img.shields.io/npm/l/graphiql.svg?style=flat-square)](LICENSE) +[![](https://dcbadge.vercel.app/api/server/NP5vbPeUFp?style=flat)](https://discord.gg/NP5vbPeUFp) + +_/ˈɑrafΙ™k(Ι™)l/_ A graphical interactive in-browser GraphQL IDE. +[Try the live demo](https://graphql.github.io/swapi-graphql). + +[![](resources/graphiql.png)](https://graphql.github.io/swapi-graphql) + +## Features + +- Full language support of the latest + [GraphQL Specification](https://spec.graphql.org/draft/#sec-Language): +- Syntax highlighting +- Intelligent type ahead of fields, arguments, types, and more +- Real-time error highlighting and reporting for queries and variables +- Automatic query and variables completion +- Automatic leaf node insertion for non-scalar fields +- Documentation explorer with search and markdown support +- Persisted state using `localStorage` +- Simple API for adding custom plugins + +## Live Demos + +- The [latest stable version](https://graphql.github.io/swapi-graphql) +- The current state of the `main` branch: + - Using the [minified bundles](https://graphiql-test.netlify.com) + - Using the [development bundles](https://graphiql-test.netlify.com/dev) (good + for inspecting, debugging, etc) +- Each pull request will also get its own preview deployment on Netlify, you'll + find a link in the GitHub checks + +## Examples + +- [`Unpkg (CDN)`](../../examples/graphiql-cdn) - A single HTML file using CDN + assets and a script tag +- [`Webpack`](../../examples/graphiql-webpack) - A starter for Webpack +- [`Create React App`](../../examples/graphiql-create-react-app) - An example + using [Create React App](https://create-react-app.dev) +- [`Parcel`](../../examples/graphiql-parcel) - An example using + [Parcel](https://parceljs.org) + +## Getting started + +> If you're looking to upgrade from `graphiql@1.x` to `graphiql@2`, check out +> the [migration guide](../../docs/migration/graphiql-2.0.0.md)! + +### UMD + +With `unpkg`/`jsdelivr`, etc.: + +```html + + +``` + +(see: Usage UMD Bundle below for more required script tags) + +## Usage + +### Using as package + +The `graphiql` package can be installed using your favorite package manager. You +also need to have `react`,`react-dom` and `graphql` installed which are peer +dependencies of `graphiql`. + +```sh +npm install graphiql react react-dom graphql +``` + +The package exports a bunch of React components: + +- The `GraphiQLProvider` components renders multiple context providers that + encapsulate all state management +- The `GraphiQLInterface` component renders the UI that makes up GraphiQL +- The `GraphiQL` component is a combination of both the above components + +There is a single prop that is required for the `GraphiQL` component called +fetcher. A fetcher is a function that performs a request to a GraphQL API. It +may return a `Promise` for queries or mutations, but also an `Observable` or an +`AsyncIterable` in order to handle subscriptions or multipart responses. + +An easy way to get create such a function is the +[`createGraphiQLFetcher`](../graphiql-toolkit/src/create-fetcher/createFetcher.ts) +method exported from the `@graphiql/toolkit` package. If you want to implement +your own fetcher function you can use the `Fetcher` type from +`@graphiql/toolkit` to make sure the signature matches what GraphiQL expects. + +The following is everything you need to render GraphiQL in your React +application: + +```jsx +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { GraphiQL } from 'graphiql'; +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import 'graphiql/graphiql.css'; + +const fetcher = createGraphiQLFetcher({ url: 'https://my.backend/graphql' }); + +const root = createRoot(document.getElementById('root')); +root.render(); +``` + +### Using as UMD bundle over CDN (Unpkg, JSDelivr, etc) + +There exist pre-bundled static assets that allow you to easily render GraphiQL +just by putting together a single HTML file. Check out the `index.html` file in +the [example project](../../examples/graphiql-cdn) in this repository. + +## Customize + +GraphiQL supports customization in UI and behavior by accepting React props and +children. + +### Props + +For props documentation, see the +[API Docs](https://graphiql-test.netlify.app/typedoc/modules/graphiql.html#graphiqlprops-1) + +### Children + +Parts of the UI can be customized by passing children to the `GraphiQL` or the +`GraphiQLInterface` component. + +- ``: Replace the GraphiQL logo with your own. + +- ``: Add a custom toolbar below the execution button. Pass + the empty `` if an empty toolbar is desired. Use the + components provided by `@graphiql/react` to create toolbar buttons with proper + styles. + +- ``: Add a custom footer shown below the response editor. + +### Plugins + +Starting with `graphiql@2` there exists a simple plugin API that allows you to +build your own custom tools right into GraphiQL. + +There are two built-in plugins that come with GraphiQL: The documentation +explorer and the query history. Both can be toggled using icons in the sidebar +on the left side of the screen. When opened, they appear next to the sidebar in +a resizable portion of the screen. + +To define your own plugin, all you need is a JavaScript object with three +properties: + +- `title`: A unique title for the plugin (this will show up in a tooltip when + hovering over the sidebar icon) +- `icon`: A React component that renders an icon which will be included in the + sidebar +- `content`: A React component that renders the plugin contents which will be + shown next to the sidebar when opening the plugin + +You can pass a list of plugin objects to the `GraphiQL` component using the +`plugins` prop. You can also control the visibility state of plugins using the +`visiblePlugin` prop and react to changes of the plugin visibility state using +the `onTogglePluginVisibility` prop. + +Inside the component you pass to `content` you can interact with the GraphiQL +state using the hooks provided by `@graphiql/react`. For example, check out +how you can integrate the OneGraph Explorer in GraphiQL using the plugin API in +the [plugin package](../graphiql-plugin-explorer) in this repo. + +### Theming + +The GraphiQL interface uses CSS variables for theming, in particular for colors. +Check out the [`root.css`](../graphiql-react/src/style/root.css) file for the +available variables. + +Overriding these variables is the only officially supported way of customizing +the appearance of GraphiQL. Starting from version 2, class names are no longer +be considered stable and might change between minor or patch version updates. + +### Editor Theme + +The colors inside the editor can also be altered using +[CodeMirror editor themes](https://codemirror.net/demo/theme.html). You can use +the `editorTheme` prop to pass in the name of the theme. The CSS for the theme +has to be loaded for the theme prop to work. + +```jsx +// In your document head: + +``` + +```jsx +// When rendering GraphiQL: + +``` + +You can also create your own theme in CSS. As a reference, the default +`graphiql` theme definition can be found +[here](../graphiql-react/src/editor/style/codemirror.css). diff --git a/packages/graphiql/__mocks__/codemirror.ts b/packages/graphiql/__mocks__/codemirror.ts new file mode 100644 index 00000000000..da0e7e838a6 --- /dev/null +++ b/packages/graphiql/__mocks__/codemirror.ts @@ -0,0 +1,77 @@ +function CodeMirror(node: HTMLElement, { value, ...options }) { + let _eventListeners = {}; + const mockWrapper = document.createElement('div'); + const mockGutter = document.createElement('div'); + mockGutter.className = 'CodeMirror-gutter'; + const mockTextArea = document.createElement('textarea'); + mockTextArea.className = 'mockCodeMirror'; + mockTextArea.addEventListener('change', e => { + _emit('change', e); + }); + mockTextArea.value = value; + mockWrapper.append(mockGutter, mockTextArea); + node.append(mockWrapper); + + function _emit(event, data) { + if (_eventListeners[event]) { + _eventListeners[event](data); + } + } + + return { + options: { + ...options, + lint: { + linterOptions: {}, + }, + }, + state: { + lint: { + linterOptions: {}, + }, + }, + + on(event, handler) { + _eventListeners[event] = handler; + }, + + off(event) { + if (!Object.prototype.hasOwnProperty.call(_eventListeners, event)) { + return; + } + const updatedEventListeners = {}; + for (const e in _eventListeners) { + if (e !== event) { + updatedEventListeners[e] = _eventListeners[e]; + } + } + _eventListeners = updatedEventListeners; + }, + + getValue() { + return mockTextArea.value; + }, + + setValue(newValue) { + mockTextArea.value = newValue; + }, + addKeyMap() {}, + removeKeyMap() {}, + setOption() {}, + refresh() {}, + setSize() {}, + + emit: _emit, + }; +} + +CodeMirror.defineExtension = () => {}; +CodeMirror.registerHelper = () => {}; +CodeMirror.defineOption = () => {}; +CodeMirror.defineMode = () => {}; + +CodeMirror.signal = (mockCodeMirror, event, ...args) => { + mockCodeMirror.emit(event, ...args); +}; + +module.exports = CodeMirror; diff --git a/packages/graphiql/__mocks__/svg.jsx b/packages/graphiql/__mocks__/svg.jsx new file mode 100644 index 00000000000..47d2425836d --- /dev/null +++ b/packages/graphiql/__mocks__/svg.jsx @@ -0,0 +1,7 @@ +export default function MockedIcon(props) { + return ( + + mocked icon + + ); +} diff --git a/packages/graphiql/babel.config.js b/packages/graphiql/babel.config.js new file mode 100644 index 00000000000..4c05afbee70 --- /dev/null +++ b/packages/graphiql/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); diff --git a/packages/graphiql/cypress.config.ts b/packages/graphiql/cypress.config.ts new file mode 100644 index 00000000000..a2b7771ac3a --- /dev/null +++ b/packages/graphiql/cypress.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:8080', + }, + video: false, + viewportWidth: 1920, + viewportHeight: 1080, +}); diff --git a/packages/graphiql/cypress/e2e/docs.cy.ts b/packages/graphiql/cypress/e2e/docs.cy.ts new file mode 100644 index 00000000000..ba83e95de8c --- /dev/null +++ b/packages/graphiql/cypress/e2e/docs.cy.ts @@ -0,0 +1,122 @@ +import { version } from 'graphql'; + +beforeEach(() => { + cy.visit('/'); +}); + +describe('GraphiQL DocExplorer - button', () => { + beforeEach(() => { + cy.get('.graphiql-sidebar button').eq(0).click(); + }); + it('Toggles doc pane on', () => { + cy.get('.graphiql-doc-explorer').should('be.visible'); + }); + + it('Toggles doc pane back off', () => { + cy.get('.graphiql-sidebar button').eq(0).click(); + cy.get('.graphiql-doc-explorer').should('not.exist'); + }); +}); + +describe('GraphiQL DocExplorer - search', () => { + beforeEach(() => { + cy.get('.graphiql-sidebar button').eq(0).click(); + cy.dataCy('doc-explorer-input').type('test'); + cy.dataCy('doc-explorer-option').should('have.length', 7); + }); + + it('Searches docs for values', () => { + cy.dataCy('doc-explorer-list').should('not.have.attr', 'hidden'); + }); + + it('Navigates to a docs entry on selecting a search result', () => { + cy.dataCy('doc-explorer-option').eq(4).children().click(); + cy.get('.graphiql-doc-explorer-title').should('have.text', 'TestInput'); + }); + + it('Allows searching fields within a type', () => { + cy.dataCy('doc-explorer-option').eq(4).children().click(); + cy.dataCy('doc-explorer-input').type('list'); + cy.dataCy('doc-explorer-option').should('have.length', 14); + cy.get('.graphiql-doc-explorer-search-divider').should( + 'have.text', + 'Other results', + ); + cy.dataCy('doc-explorer-option').contains('hasArgs'); + }); + + it('Closes popover when blurring input', () => { + cy.dataCy('doc-explorer-input').blur(); + cy.dataCy('doc-explorer-list').should('not.exist'); + }); + + it('Navigates back', () => { + cy.dataCy('doc-explorer-option').eq(4).children().click(); + cy.get('.graphiql-doc-explorer-back').click(); + cy.get('.graphiql-doc-explorer-title').should('have.text', 'Docs'); + }); + + it('Type fields link to their own docs entry', () => { + cy.dataCy('doc-explorer-option').last().click(); + cy.get('.graphiql-doc-explorer-title').should('have.text', 'isTest'); + cy.get('.graphiql-markdown-description').should( + 'have.text', + 'Is this a test schema? Sure it is.\n', + ); + }); +}); + +describe('GraphQL DocExplorer - deprecated fields', () => { + it('should show deprecated fields details when expanding', () => { + // Open doc explorer + cy.get('.graphiql-sidebar button').eq(0).click(); + + // Select query type + cy.get('.graphiql-doc-explorer-type-name').first().click(); + + // Show deprecated fields + cy.contains('Show Deprecated Fields').click(); + + // Assert that title is shown + cy.get('.graphiql-doc-explorer-section-title').contains( + 'Deprecated Fields', + ); + + // Assert that the deprecated field is shown correctly + cy.get('.graphiql-doc-explorer-field-name') + .contains('deprecatedField') + .closest('.graphiql-doc-explorer-item') + .should('contain.text', 'This field is an example of a deprecated field') + .and( + 'contain.html', + '

    No longer in use, try test instead.

    ', + ); + }); +}); + +let describeOrSkip = describe.skip; + +// TODO: disable when defer/stream is merged to graphql +if (!version.includes('15.5')) { + describeOrSkip = describe; +} + +describeOrSkip('GraphQL DocExplorer - deprecated arguments', () => { + it('should show deprecated arguments category title', () => { + // Open doc explorer + cy.get('.graphiql-sidebar button').eq(0).click(); + + // Select query type + cy.get('.graphiql-doc-explorer-type-name').first().click(); + + cy.get('.graphiql-doc-explorer-field-name').contains('hasArgs').click(); + cy.contains('Show Deprecated Arguments').click(); + cy.get('.graphiql-doc-explorer-section-title').contains( + 'Deprecated Arguments', + ); + cy.get('.graphiql-markdown-deprecation').should( + 'have.text', + 'deprecated argument\n', + ); + }); +}); diff --git a/packages/graphiql/cypress/e2e/errors.cy.ts b/packages/graphiql/cypress/e2e/errors.cy.ts new file mode 100644 index 00000000000..0509e320bee --- /dev/null +++ b/packages/graphiql/cypress/e2e/errors.cy.ts @@ -0,0 +1,78 @@ +import { version } from 'graphql'; + +describe('Errors', () => { + it('Should show an error when the HTTP request fails', () => { + cy.visit('/?http-error=true'); + cy.assertQueryResult({ + errors: [ + { + /** + * The exact error message can differ depending on the browser and + * its JSON parser. This is the error you get in Electron (which + * we use to run the tests headless), the error in the latest Chrome + * version is different! + */ + message: 'Unexpected token \'B\', "Bad Gateway" is not valid JSON', + stack: + 'SyntaxError: Unexpected token \'B\', "Bad Gateway" is not valid JSON', + }, + ], + }); + }); + + it('Should show an error when introspection fails', () => { + cy.visit('/?graphql-error=true'); + cy.assertQueryResult({ + errors: [{ message: 'Something unexpected happened...' }], + }); + }); + + it('Should show an error when the schema is invalid', () => { + cy.visit('/?bad=true'); + /** + * We can't use `cy.assertQueryResult` here because the stack contains line + * and column numbers of the `graphiql.min.js` bundle which are not stable. + */ + cy.get('section.result-window').should(element => { + expect(element.get(0).innerText).to.contain( + version.startsWith('16.') + ? 'Names must only contain [_a-zA-Z0-9] but \\"\\" does not.' + : 'Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \\"\\" does not.', + ); + }); + }); + + it('Should show an error when sending an invalid query', () => { + cy.visitWithOp({ query: '{thisDoesNotExist}' }); + cy.clickExecuteQuery(); + cy.assertQueryResult({ + errors: [ + { + message: 'Cannot query field "thisDoesNotExist" on type "Test".', + locations: [{ line: 1, column: 2 }], + }, + ], + }); + }); + + it('Should show an error when sending an invalid subscription', () => { + cy.visitWithOp({ query: 'subscription {thisDoesNotExist}' }); + cy.clickExecuteQuery(); + cy.assertQueryResult({ + errors: [ + { + message: + 'Cannot query field "thisDoesNotExist" on type "SubscriptionType".', + locations: [{ line: 1, column: 15 }], + }, + ], + }); + + cy.on('uncaught:exception', () => { + // TODO: should GraphiQL doesn't throw an unhandled promise rejection for subscriptions ? + + // return false to prevent the error from failing this test + return false; + }); + }); +}); diff --git a/packages/graphiql/cypress/e2e/graphql-ws.cy.ts b/packages/graphiql/cypress/e2e/graphql-ws.cy.ts new file mode 100644 index 00000000000..89bb792aeb6 --- /dev/null +++ b/packages/graphiql/cypress/e2e/graphql-ws.cy.ts @@ -0,0 +1,20 @@ +describe('IncrementalDelivery support via fetcher', () => { + describe('When operation contains @stream', () => { + const testSubscription = /* GraphQL */ ` + subscription TestSubscription($delay: Int) { + message(delay: $delay) + } + `; + const mockSubscriptionSuccess = { + data: { + message: 'Zdravo', + }, + }; + + it('Expects a subscription to resolve', () => { + cy.visitWithOp({ query: testSubscription, variables: { delay: 0 } }); + cy.clickExecuteQuery(); + cy.assertQueryResult(mockSubscriptionSuccess); + }); + }); +}); diff --git a/packages/graphiql/cypress/e2e/headers.cy.ts b/packages/graphiql/cypress/e2e/headers.cy.ts new file mode 100644 index 00000000000..da453b052d3 --- /dev/null +++ b/packages/graphiql/cypress/e2e/headers.cy.ts @@ -0,0 +1,26 @@ +const DEFAULT_HEADERS = '{"foo":2}'; + +describe('Headers', () => { + describe('`defaultHeaders`', () => { + it('should have default headers while open new tabs', () => { + cy.visit(`/?query={test}&defaultHeaders=${DEFAULT_HEADERS}`); + cy.assertHasValues({ query: '{test}', headersString: DEFAULT_HEADERS }); + cy.get('.graphiql-tab-add').click(); + cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); + cy.get('.graphiql-tab-add').click(); + cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); + }); + + it('in case `headers` and `defaultHeaders` are set, `headers` should be on 1st tab and `defaultHeaders` for other opened tabs', () => { + const HEADERS = '{"bar":true}'; + cy.visit( + `/?query={test}&defaultHeaders=${DEFAULT_HEADERS}&headers=${HEADERS}`, + ); + cy.assertHasValues({ query: '{test}', headersString: HEADERS }); + cy.get('.graphiql-tab-add').click(); + cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); + cy.get('.graphiql-tab-add').click(); + cy.assertHasValues({ query: '', headersString: DEFAULT_HEADERS }); + }); + }); +}); diff --git a/packages/graphiql/cypress/e2e/history.cy.ts b/packages/graphiql/cypress/e2e/history.cy.ts new file mode 100644 index 00000000000..60e0b772d66 --- /dev/null +++ b/packages/graphiql/cypress/e2e/history.cy.ts @@ -0,0 +1,146 @@ +import { + mockQuery1, + mockVariables1, + mockBadQuery, + mockQuery2, + mockVariables2, + mockHeaders1, + mockHeaders2, +} from '../fixtures/fixtures'; + +describe('history', () => { + beforeEach(() => {}); + it('defaults to closed history panel', () => { + cy.visit('/'); + + cy.get('.graphiql-history').should('not.exist'); + }); + + it('will save history item even when history panel is closed', () => { + cy.visit('/?query={test}'); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items').should('have.length', 1); + }); + + it('will save history item even when history panel is closed', () => { + cy.visit('/?query={test}'); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items li').should('have.length', 1); + }); + + it('will not save invalid queries', () => { + cy.visit(`?query=${mockBadQuery}`); + cy.get('button[aria-label="Show History"]').click(); + cy.clickExecuteQuery(); + cy.get('ul.graphiql-history-items li').should('have.length', 0); + }); + + it('will save if new query is different than previous query', () => { + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders1}`); + cy.get('button[aria-label="Show History"]').click(); + cy.clickExecuteQuery(); + cy.get('ul.graphiql-history-items li').should('have.length', 1); + + cy.visit(`?query=${mockQuery2}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items li').should('have.length', 2); + }); + + it('will not save if new query is the same as previous query', () => { + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders1}`); + cy.get('button[aria-label="Show History"]').click(); + cy.clickExecuteQuery(); + cy.get('ul.graphiql-history-items li').should('have.length', 1); + + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items li').should('have.length', 1); + }); + + it('will save query if the variables change', () => { + cy.visit( + `?query=${mockQuery1}&headers=${mockHeaders1}&variables=${mockVariables1}`, + ); + cy.get('button[aria-label="Show History"]').click(); + cy.clickExecuteQuery(); + cy.get('ul.graphiql-history-items li').should('have.length', 1); + + cy.visit( + `?query=${mockQuery1}&headers=${mockHeaders1}&variables=${mockVariables2}`, + ); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items li').should('have.length', 2); + }); + + it('will save query if the headers change', () => { + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders1}`); + cy.get('button[aria-label="Show History"]').click(); + cy.clickExecuteQuery(); + cy.get('ul.graphiql-history-items li').should('have.length', 1); + + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders2}`); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items li').should('have.length', 2); + }); + + it('should remove individual item', () => { + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.visit(`?query=${mockQuery2}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + + cy.get('ul.graphiql-history-items li').should('have.length', 2); + + cy.get( + '.graphiql-history-item:nth-child(2) > button[aria-label="Delete from history"]', + ).click(); + cy.get('.graphiql-history-item').should('have.length', 1); + }); + + it('should remove all items', () => { + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.visit(`?query=${mockQuery2}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items li').should('have.length', 2); + + cy.get('.graphiql-history-header > button:last-child').click(); + cy.get('.graphiql-history-item').should('have.length', 0); + }); + + it('should add/remove item to favorite', () => { + cy.visit(`?query=${mockQuery1}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.visit(`?query=${mockQuery2}&headers=${mockHeaders1}`); + cy.clickExecuteQuery(); + cy.get('button[aria-label="Show History"]').click(); + cy.get('ul.graphiql-history-items li').should('have.length', 2); + cy.get('.graphiql-history-item-label').eq(0).should('have.text', 'Test2'); + + const favorites = + '.graphiql-history ul:first-of-type .graphiql-history-item'; + const items = '.graphiql-history ul:last-of-type .graphiql-history-item'; + + cy.get( + '.graphiql-history-item:nth-child(2) > button[aria-label="Add favorite"]', + ).click(); + cy.get('.graphiql-history ul').should('have.length', 2); // favorites and items + cy.get(favorites).should('have.length', 1); + cy.get(items).should('have.length', 1); + cy.get('.graphiql-history-item-label').eq(0).should('have.text', 'Test'); // favorite so now at top of list + + cy.get( + '.graphiql-history-item:nth-child(1) > button[aria-label="Remove favorite"]', + ).click(); + cy.get('.graphiql-history ul').should('have.length', 1); // just items + cy.get(items).should('have.length', 2); + }); +}); diff --git a/packages/graphiql/cypress/e2e/incremental-delivery.cy.ts b/packages/graphiql/cypress/e2e/incremental-delivery.cy.ts new file mode 100644 index 00000000000..0ab5710b5ea --- /dev/null +++ b/packages/graphiql/cypress/e2e/incremental-delivery.cy.ts @@ -0,0 +1,171 @@ +import { version } from 'graphql'; + +let describeOrSkip = describe.skip; + +// TODO: disable when defer/stream is merged to graphql +if (version.includes('stream')) { + describeOrSkip = describe; +} + +describeOrSkip('IncrementalDelivery support via fetcher', () => { + describe('When operation contains @stream', () => { + const testStreamQuery = /* GraphQL */ ` + query StreamQuery($delay: Int) { + streamable(delay: $delay) @stream(initialCount: 2) { + text + } + } + `; + + const mockStreamSuccess = { + data: { + streamable: [ + { + text: 'Hi', + }, + { + text: 'δ½ ε₯½', + }, + { + text: 'Hola', + }, + { + text: 'Ψ£Ω‡Ω„Ψ§Ω‹', + }, + { + text: 'Bonjour', + }, + { + text: 'Ψ³Ω„Ψ§Ω…', + }, + { + text: 'μ•ˆλ…•', + }, + { + text: 'Ciao', + }, + { + text: 'ΰ€Ήΰ₯‡ΰ€²ΰ₯‹', + }, + { + text: 'Π—Π΄ΠΎΡ€ΠΎΠ²ΠΎ', + }, + ], + }, + hasNext: false, + }; + + it('Expects slower streams to resolve in several increments, and the payloads to patch properly', () => { + const delay = 100; + const timeout = mockStreamSuccess.data.streamable.length * (delay * 1.5); + + cy.visitWithOp({ query: testStreamQuery, variables: { delay } }); + cy.clickExecuteQuery(); + cy.wait(timeout); + cy.assertQueryResult(mockStreamSuccess); + }); + + it('Expects a quick stream to resolve in a single increment', () => { + cy.visitWithOp({ query: testStreamQuery, variables: { delay: 0 } }); + cy.clickExecuteQuery(); + cy.assertQueryResult(mockStreamSuccess); + }); + }); + + describe('When operating with @defer', () => { + it('Excepts to see a slow response but path properly', () => { + const delay = 1000; + const timeout = delay * 1.5; + + const testQuery = /* GraphQL */ ` + query DeferQuery($delay: Int) { + deferrable { + normalString + ... @defer { + deferredString(delay: $delay) + } + } + } + `; + + cy.visitWithOp({ query: testQuery, variables: { delay } }); + cy.clickExecuteQuery(); + cy.wait(timeout); + cy.assertQueryResult({ + data: { + deferrable: { + normalString: 'Nice', + deferredString: + 'Oops, this took 1 seconds longer than I thought it would!', + }, + }, + hasNext: false, + }); + }); + + it('Expects to merge types when members arrive at different times', () => { + /* + This tests that; + 1. user ({name}) => { name } + 2. user ({age}) => { name, age } + 3. user.friends.0 ({name}) => { name, age, friends: [{name}] } <- can sometimes happen before 4, due the the promise race + 4. user.friends.0 ({age}) => { name, age, friends: [{name, age}] } + + This shows us that we can deep merge defers, deep merge streams, and also deep merge defers inside streams + */ + + const delay = 1000; + const timeout = 4 /* friends */ * (delay * 1.5); + + const testQuery = /* GraphQL */ ` + query DeferQuery($delay: Int) { + person { + name + ... @defer { + age(delay: $delay) + } + friends @stream(initialCount: 0) { + ... @defer { + name + } + ... @defer { + age(delay: $delay) + } + } + } + } + `; + + cy.visitWithOp({ query: testQuery, variables: { delay } }); + cy.clickExecuteQuery(); + cy.wait(timeout); + cy.assertQueryResult({ + data: { + person: { + name: 'Mark', + friends: [ + { + name: 'James', + age: 1000, + }, + { + name: 'Mary', + age: 1000, + }, + { + name: 'John', + age: 1000, + }, + { + name: 'Patrica', + age: 1000, + }, + ], + age: 1000, + }, + }, + hasNext: false, + }); + }); + }); +}); diff --git a/packages/graphiql/cypress/e2e/init.cy.ts b/packages/graphiql/cypress/e2e/init.cy.ts new file mode 100644 index 00000000000..960faecd627 --- /dev/null +++ b/packages/graphiql/cypress/e2e/init.cy.ts @@ -0,0 +1,57 @@ +const testQuery = `{ +longDescriptionType { + id + image + hasArgs + test { + id + isTest + __typename + } + } +}`; + +const mockSuccess = { + data: { + longDescriptionType: { + id: 'abc123', + image: '/images/logo.svg', + hasArgs: '{"defaultValue":"test default value"}', + test: { + id: 'abc123', + isTest: true, + __typename: 'Test', + }, + }, + }, +}; + +describe('GraphiQL On Initialization', () => { + it('Renders default value without error', () => { + const containers = [ + '#graphiql', + '.graphiql-container', + '.graphiql-sessions', + '.graphiql-editors', + '.graphiql-response', + '.graphiql-editor-tool', + ]; + cy.visit('/'); + cy.get('.graphiql-query-editor').contains('# Welcome to GraphiQL'); + for (const cSelector of containers) { + cy.get(cSelector).should('be.visible'); + } + }); + + it('Executes a GraphQL query over HTTP that has the expected result', () => { + cy.visitWithOp({ query: testQuery }); + cy.clickExecuteQuery(); + cy.assertQueryResult(mockSuccess); + }); + it('Shows the expected error when the schema is invalid', () => { + cy.visit('/?bad=true'); + cy.get('section.result-window').should(element => { + expect(element.get(0).innerText).to.contain('Names must'); + }); + }); +}); diff --git a/packages/graphiql/cypress/e2e/keyboard.cy.ts b/packages/graphiql/cypress/e2e/keyboard.cy.ts new file mode 100644 index 00000000000..0cc2ec0578b --- /dev/null +++ b/packages/graphiql/cypress/e2e/keyboard.cy.ts @@ -0,0 +1,34 @@ +describe('GraphiQL keyboard interactions', () => { + it('Does not prevent the escape key from being handled outside the editor', () => { + cy.visit('/'); + const mockFn = cy.stub().as('escapeHandler'); + cy.document().then(doc => { + doc.addEventListener('keydown', event => { + if (event.key === 'Escape') { + mockFn(); + } + }); + }); + + cy.get('.graphiql-query-editor textarea').type('{esc}', { force: true }); + + cy.get('@escapeHandler').should('have.been.called'); + }); + + it('Does prevent the escape key from being handled outside the editor if closing the autocomplete dialog', () => { + cy.visit('/'); + const mockFn = cy.stub().as('escapeHandler'); + cy.document().then(doc => { + doc.addEventListener('keydown', event => { + if (event.key === 'Escape') { + mockFn(); + } + }); + }); + + cy.get('.graphiql-query-editor textarea').type('{\n t', { force: true }); + cy.get('.graphiql-query-editor textarea').type('{esc}'); + + cy.get('@escapeHandler').should('not.have.been.called'); + }); +}); diff --git a/packages/graphiql/cypress/e2e/lint.cy.ts b/packages/graphiql/cypress/e2e/lint.cy.ts new file mode 100644 index 00000000000..64b10dd4306 --- /dev/null +++ b/packages/graphiql/cypress/e2e/lint.cy.ts @@ -0,0 +1,160 @@ +import { version as graphqlVersion } from 'graphql'; + +describe('Linting', () => { + it('Does not mark valid fields', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + { + myAlias: id + test { + id + } + } + `, + }) + .contains('myAlias') + .should('not.have.class', 'CodeMirror-lint-mark') + .and('not.have.class', 'CodeMirror-lint-mark-error'); + }); + + it('Marks invalid fields as error', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + { + doesNotExist + test { + id + } + } + `, + }).assertLinterMarkWithMessage( + 'doesNotExist', + 'error', + 'Cannot query field "doesNotExist" on type "Test".', + ); + }); + + it('Marks deprecated fields as warning', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + { + id + deprecatedField { + id + } + } + `, + }).assertLinterMarkWithMessage( + 'deprecatedField', + 'warning', + 'The field Test.deprecatedField is deprecated.', + ); + }); + + it('Marks syntax errors in variables JSON as error', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + query WithVariables($stringArg: String) { + hasArgs(string: $stringArg) + } + `, + variablesString: JSON.stringify({ stringArg: '42' }, null, 2).slice( + 0, + -1, + ), + }).assertLinterMarkWithMessage( + '"42"', + 'error', + 'Expected } but found [end of file].', + ); + }); + + it('Marks unused variables as error', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + query WithVariables($stringArg: String) { + hasArgs(string: $stringArg) + } + `, + variables: { + stringArg: '42', + unusedVariable: 'whoops', + }, + }).assertLinterMarkWithMessage( + 'unusedVariable', + 'error', + 'Variable "$unusedVariable" does not appear in any GraphQL query.', + ); + }); + + it('Marks invalid variable type as error', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + query WithVariables($stringArg: String) { + hasArgs(string: $stringArg) + } + `, + variables: { + stringArg: 42, + }, + }).assertLinterMarkWithMessage( + '42', + 'error', + 'Expected value of type "String".', + ); + }); + + it('Marks variables with null values for a non-nullable type as error', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + query WithVariables($stringArg: String!) { + hasArgs(string: $stringArg) + } + `, + variables: { + stringArg: null, + }, + }).assertLinterMarkWithMessage( + 'null', + 'error', + 'Type "String!" is non-nullable and cannot be null.', + ); + }); + + it('Marks variables with non-object values for a input object type as error', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + query WithVariables($objectArg: TestInput) { + hasArgs(object: $objectArg) + } + `, + variables: { + objectArg: '42', + }, + }).assertLinterMarkWithMessage( + '"42"', + 'error', + 'Type "TestInput" must be an Object.', + ); + }); + + it('Marks GraphQL syntax errors as error', () => { + cy.visitWithOp({ + query: /* GraphQL */ ` + { + doesNotExist + test { + id + } + +++ + } + `, + }).assertLinterMarkWithMessage( + '+++', + 'error', + graphqlVersion.startsWith('15.') + ? 'Syntax Error: Cannot parse the unexpected character "+".' + : 'Syntax Error: Unexpected character: "+".', + ); + }); +}); diff --git a/packages/graphiql/cypress/e2e/prettify.cy.ts b/packages/graphiql/cypress/e2e/prettify.cy.ts new file mode 100644 index 00000000000..d82fd5c50bb --- /dev/null +++ b/packages/graphiql/cypress/e2e/prettify.cy.ts @@ -0,0 +1,74 @@ +import { version } from 'graphql'; +let describeOrSkip = describe.skip; + +// hard to account for the extra \n between 15/16 so these only run for 16 for now +if (version.includes('16')) { + describeOrSkip = describe; +} + +const prettifiedQuery = `{ + longDescriptionType { + id + } +}`; + +const prettifiedVariables = `{ + "a": 1 +}`; + +const uglyQuery = '{longDescriptionType {id}}'; + +const uglyVariables = '{"a": 1}'; + +const brokenQuery = 'longDescriptionType {id}}'; + +const brokenVariables = '"a": 1}'; + +describeOrSkip('GraphiQL Prettify', () => { + it('Regular prettification', () => { + cy.visitWithOp({ query: uglyQuery, variablesString: uglyVariables }); + + cy.clickPrettify(); + + cy.assertHasValues({ + query: prettifiedQuery, + variablesString: prettifiedVariables, + }); + }); + + it('Noop prettification', () => { + cy.visitWithOp({ + query: prettifiedQuery, + variablesString: prettifiedVariables, + }); + + cy.clickPrettify(); + + cy.assertHasValues({ + query: prettifiedQuery, + variablesString: prettifiedVariables, + }); + }); + + it('No crash on bad query', () => { + cy.visitWithOp({ query: brokenQuery, variablesString: uglyVariables }); + + cy.clickPrettify(); + + cy.assertHasValues({ + query: brokenQuery, + variablesString: prettifiedVariables, + }); + }); + + it('No crash on bad variablesString', () => { + cy.visitWithOp({ query: uglyQuery, variablesString: brokenVariables }); + + cy.clickPrettify(); + + cy.assertHasValues({ + query: prettifiedQuery, + variablesString: brokenVariables, + }); + }); +}); diff --git a/packages/graphiql/cypress/e2e/tabs.cy.ts b/packages/graphiql/cypress/e2e/tabs.cy.ts new file mode 100644 index 00000000000..4ad14f76d14 --- /dev/null +++ b/packages/graphiql/cypress/e2e/tabs.cy.ts @@ -0,0 +1,81 @@ +describe('Tabs', () => { + it('Should store editor contents when switching between tabs', () => { + cy.visit('/?query='); + + // Assert that no tab visible when there's only one session + cy.get('#graphiql-session-tab-0').should('not.exist'); + + // Enter a query without operation name + cy.get('.graphiql-query-editor textarea').type('{id', { force: true }); + + // Run the query + cy.clickExecuteQuery(); + + // Open a new tab + cy.get('.graphiql-tab-add').click(); + + // Enter a query + cy.get('.graphiql-query-editor textarea').type('query Foo {image', { + force: true, + }); + cy.get('#graphiql-session-tab-1').should('have.text', 'Foo'); + + // Enter variables + cy.get('.graphiql-editor-tool textarea') + .eq(0) + .type('{"someVar":42', { force: true }); + + // Enter headers + cy.contains('Headers').click(); + cy.get('.graphiql-editor-tool textarea') + .eq(1) + .type('{"someHeader":"someValue"', { force: true }); + + // Run the query + cy.clickExecuteQuery(); + + // Switch back to the first tab + cy.get('#graphiql-session-tab-0').click(); + + // Assert tab titles + cy.get('#graphiql-session-tab-0').should('have.text', ''); + cy.get('#graphiql-session-tab-1').should('have.text', 'Foo'); + + // Assert editor values + cy.assertHasValues({ + query: '{id}', + variablesString: '', + headersString: '', + response: { data: { id: 'abc123' } }, + }); + + // Switch back to the second tab + cy.get('#graphiql-session-tab-1').click(); + + // Assert tab titles + cy.get('#graphiql-session-tab-0').should('have.text', ''); + cy.get('#graphiql-session-tab-1').should('have.text', 'Foo'); + + // Assert editor values + cy.assertHasValues({ + query: 'query Foo {image}', + variablesString: '{"someVar":42}', + headersString: '{"someHeader":"someValue"}', + response: { data: { image: '/images/logo.svg' } }, + }); + + // Close tab + cy.get('#graphiql-session-tab-1 + .graphiql-tab-close').click(); + + // Assert that no tab visible when there's only one session + cy.get('#graphiql-session-tab-0').should('not.exist'); + + // Assert editor values + cy.assertHasValues({ + query: '{id}', + variablesString: '', + headersString: '', + response: { data: { id: 'abc123' } }, + }); + }); +}); diff --git a/packages/graphiql/cypress/fixtures/example.json b/packages/graphiql/cypress/fixtures/example.json new file mode 100644 index 00000000000..02e4254378e --- /dev/null +++ b/packages/graphiql/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/packages/graphiql/cypress/fixtures/fixtures.ts b/packages/graphiql/cypress/fixtures/fixtures.ts new file mode 100644 index 00000000000..2f039f3e99c --- /dev/null +++ b/packages/graphiql/cypress/fixtures/fixtures.ts @@ -0,0 +1,28 @@ +export const mockBadQuery = 'bad {} query'; + +export const mockQuery1 = /* GraphQL */ ` + query Test($string: String) { + test { + hasArgs(string: $string) + } + } +`; + +export const mockQuery2 = /* GraphQL */ ` + query Test2 { + test { + id + } + } +`; + +export const mockVariables1 = JSON.stringify({ string: 'string' }); +export const mockVariables2 = JSON.stringify({ string: 'string2' }); + +export const mockHeaders1 = JSON.stringify({ foo: 'bar' }); +export const mockHeaders2 = JSON.stringify({ foo: 'baz' }); + +export const mockOperationName1 = 'Test'; +export const mockOperationName2 = 'Test2'; + +export const mockHistoryLabel1 = 'Test'; diff --git a/packages/graphiql/cypress/plugins/index.ts b/packages/graphiql/cypress/plugins/index.ts new file mode 100644 index 00000000000..5d60c1025b3 --- /dev/null +++ b/packages/graphiql/cypress/plugins/index.ts @@ -0,0 +1,3 @@ +export default (on, config) => { + return config; +}; diff --git a/packages/graphiql/cypress/support/commands.ts b/packages/graphiql/cypress/support/commands.ts new file mode 100644 index 00000000000..0f2254afbf9 --- /dev/null +++ b/packages/graphiql/cypress/support/commands.ts @@ -0,0 +1,156 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** + +/// + +type Op = { + query: string; + variables?: Record; + variablesString?: string; + headersString?: string; + response?: Record; +}; +declare namespace Cypress { + type MockResult = + | { data: any } + | { data: any; hasNext?: boolean } + | { error: any[] } + | { errors: any[] }; + + interface Chainable { + /** + * Custom command to select DOM element by data-cy attribute. + * @example cy.dataCy('greeting') + */ + dataCy(value: string): Chainable; + + clickExecuteQuery(): Chainable; + + visitWithOp(op: Op): Chainable; + + clickPrettify(): Chainable; + + assertHasValues(op: Op): Chainable; + + assertQueryResult(expectedResult: MockResult): Chainable; + + assertLinterMarkWithMessage( + text: string, + severity: 'error' | 'warning', + message?: string, + ): Chainable; + } +} + +Cypress.Commands.add('dataCy', value => { + return cy.get(`[data-cy="${value}"]`); +}); + +Cypress.Commands.add('clickExecuteQuery', () => { + return cy.get('.graphiql-execute-button').click(); +}); + +Cypress.Commands.add('clickPrettify', () => { + return cy.get('[aria-label="Prettify query (Shift-Ctrl-P)"]').click(); +}); + +Cypress.Commands.add('visitWithOp', ({ query, variables, variablesString }) => { + let url = `/?query=${encodeURIComponent(query)}`; + if (variables || variablesString) { + url += `&variables=${encodeURIComponent( + JSON.stringify(variables, null, 2) || variablesString, + )}`; + } + return cy.visit(url); +}); + +Cypress.Commands.add( + 'assertHasValues', + ({ query, variables, variablesString, headersString, response }: Op) => { + cy.get('.graphiql-query-editor').should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(query), + ); + }); + if (variables !== undefined) { + cy.contains('Variables').click(); + cy.get('.graphiql-editor-tool .graphiql-editor') + .eq(0) + .should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(JSON.stringify(variables, null, 2)), + ); + }); + } + if (variablesString !== undefined) { + cy.contains('Variables').click(); + cy.get('.graphiql-editor-tool .graphiql-editor') + .eq(0) + .should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(variablesString), + ); + }); + } + if (headersString !== undefined) { + cy.contains('Headers').click(); + cy.get('.graphiql-editor-tool .graphiql-editor') + .eq(1) + .should(element => { + expect(normalize(element.get(0).innerText)).to.equal( + codeWithLineNumbers(headersString), + ); + }); + } + if (response !== undefined) { + cy.get('.result-window').should(element => { + expect(normalizeWhitespace(element.get(0).innerText)).to.equal( + JSON.stringify(response, null, 2), + ); + }); + } + }, +); + +Cypress.Commands.add('assertQueryResult', expectedResult => { + cy.get('section.result-window').should(element => { + expect(normalizeWhitespace(element.get(0).innerText)).to.equal( + JSON.stringify(expectedResult, null, 2), + ); + }); +}); + +function codeWithLineNumbers(code: string): string { + return code + .split('\n') + .map((line, i) => `${i + 1}\n${line}`) + .join('\n'); +} + +function normalize(str: string) { + return str.replaceAll('​', ''); +} + +function normalizeWhitespace(str: string) { + return str.replaceAll('\xA0', ' '); +} + +Cypress.Commands.add( + 'assertLinterMarkWithMessage', + (text, severity, message) => { + cy.contains(text) + .should('have.class', 'CodeMirror-lint-mark') + .and('have.class', `CodeMirror-lint-mark-${severity}`); + if (message) { + cy.contains(text).trigger('mouseover'); + cy.contains(message); + } + }, +); diff --git a/packages/graphiql/cypress/support/e2e.ts b/packages/graphiql/cypress/support/e2e.ts new file mode 100644 index 00000000000..f9e2ea7fdcb --- /dev/null +++ b/packages/graphiql/cypress/support/e2e.ts @@ -0,0 +1,17 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +/// + +import './commands'; diff --git a/packages/graphiql/cypress/tsconfig.json b/packages/graphiql/cypress/tsconfig.json new file mode 100644 index 00000000000..d103d9147dc --- /dev/null +++ b/packages/graphiql/cypress/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es2021", "dom"], + "types": ["cypress", "node"] + }, + "include": ["**/*.ts"] +} diff --git a/packages/graphiql/jest.config.js b/packages/graphiql/jest.config.js new file mode 100644 index 00000000000..22b3f1af305 --- /dev/null +++ b/packages/graphiql/jest.config.js @@ -0,0 +1,9 @@ +const base = require('../../jest.config.base')(__dirname); + +module.exports = { + ...base, + moduleNameMapper: { + '\\.svg$': `${__dirname}/__mocks__/svg`, + ...base.moduleNameMapper, + }, +}; diff --git a/packages/graphiql/package.json b/packages/graphiql/package.json new file mode 100644 index 00000000000..faa67a0fe26 --- /dev/null +++ b/packages/graphiql/package.json @@ -0,0 +1,104 @@ +{ + "name": "graphiql", + "version": "3.0.1", + "description": "An graphical interactive in-browser GraphQL IDE.", + "contributors": [ + "Hyohyeon Jeong ", + "Lee Byron (http://leebyron.com/)" + ], + "repository": { + "type": "git", + "url": "http://github.com/graphql/graphiql", + "directory": "packages/graphiql" + }, + "homepage": "http://github.com/graphql/graphiql/tree/master/packages/graphiql#readme", + "bugs": { + "url": "https://github.com/graphql/graphiql/issues?q=issue+label:graphiql" + }, + "license": "MIT", + "main": "dist/index.js", + "module": "esm/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "esm", + "src", + "graphiql.js", + "graphiql.min.js", + "graphiql.min.js.map", + "graphiql.css", + "graphiql.min.css", + "graphiql.min.css.map" + ], + "scripts": { + "analyze-bundle": "cross-env NODE_ENV=production ANALYZE=1 yarn webpack -p", + "build": "yarn build-clean && yarn build-cjs && yarn build-esm", + "build-bundles": "yarn build-bundles-clean && yarn build-bundles-webpack", + "build-bundles-clean": "rimraf 'graphiql.*{js,css}' *.html", + "build-bundles-webpack": "cross-env yarn webpack --mode development --bail", + "build-cjs": "tsc", + "build-clean": "rimraf esm dist webpack *.html", + "build-esm": "tsc --project ./tsconfig.esm.json", + "check": "tsc --noEmit", + "cypress-open": "yarn e2e-server 'cypress open'", + "dev": "cross-env NODE_ENV=development webpack-dev-server --config resources/webpack.config.js", + "e2e": "yarn e2e-server 'cypress run'", + "e2e-server": "start-server-and-test 'cross-env PORT=8080 node test/e2e-server' 'http-get://localhost:8080/graphql?query={test { id }}'", + "webpack": "webpack-cli --config resources/webpack.config.js" + }, + "dependencies": { + "@graphiql/react": "^0.19.0", + "@graphiql/toolkit": "^0.8.4", + "graphql-language-service": "^5.1.7", + "markdown-it": "^12.2.0" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + }, + "devDependencies": { + "@cypress/webpack-preprocessor": "^5.5.0", + "@testing-library/jest-dom": "5.16.5", + "@testing-library/react": "14.0.0", + "@types/codemirror": "^5.60.5", + "@types/markdown-it": "^12.2.3", + "@types/node": "^16.18.4", + "@types/testing-library__jest-dom": "5.14.5", + "babel-loader": "^9.1.2", + "babel-plugin-macros": "^3.1.0", + "cross-env": "^7.0.2", + "css-loader": "^6.7.3", + "cssnano": "^5.1.15", + "cypress": "^12.6.0", + "express": "^4.18.2", + "graphql-http": "^1.19.0", + "fork-ts-checker-webpack-plugin": "7.3.0", + "graphql": "^16.4.0", + "graphql-subscriptions": "^2.0.0", + "html-webpack-plugin": "^5.5.0", + "identity-obj-proxy": "^3.0.0", + "mini-css-extract-plugin": "^2.7.2", + "postcss": "8.4.21", + "postcss-loader": "7.0.2", + "postcss-import": "15.1.0", + "postcss-preset-env": "^8.0.1", + "prop-types": "15.7.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hot-loader": "^4.12.20", + "react-test-renderer": "^18.2.0", + "require-context.macro": "^1.2.2", + "rimraf": "^3.0.2", + "serve": "^11.3.0", + "start-server-and-test": "^1.10.11", + "style-loader": "^3.3.1", + "subscriptions-transport-ws": "0.11.0", + "typescript": "^4.6.3", + "webpack": "5.76.0", + "webpack-bundle-analyzer": "^3.6.1", + "webpack-cli": "^5.0.1", + "webpack-dev-server": "^4.11.1", + "ws": "^8.3.0" + } +} diff --git a/packages/graphiql/postcss.config.js b/packages/graphiql/postcss.config.js new file mode 100644 index 00000000000..0212e4d5f0a --- /dev/null +++ b/packages/graphiql/postcss.config.js @@ -0,0 +1,9 @@ +module.exports = ({ options, webpackLoaderContext }) => ({ + plugins: { + // https://github.com/postcss/postcss-import/issues/442#issuecomment-822427606 + 'postcss-import': { root: webpackLoaderContext.context }, + // contains autoprefixer, etc + 'postcss-preset-env': options['postcss-preset-env'] || false, + cssnano: process.env.NODE_ENV === 'production' ? options.cssnano : false, + }, +}); diff --git a/resources/build.sh b/packages/graphiql/resources/build.sh similarity index 51% rename from resources/build.sh rename to packages/graphiql/resources/build.sh index 8213f0e0afe..e112482842c 100644 --- a/resources/build.sh +++ b/packages/graphiql/resources/build.sh @@ -1,19 +1,20 @@ -#!/bin/sh +#!/bin/bash set -e +set -o pipefail if [ ! -d "node_modules/.bin" ]; then - echo "Be sure to run \`npm install\` before building GraphiQL." + echo "Be sure to run \`yarn install\` before building GraphiQL." exit 1 fi -rm -rf dist/ && mkdir -p dist/ babel src --ignore __tests__ --out-dir dist/ +ESM=true babel src --ignore __tests__ --out-dir esm/ echo "Bundling graphiql.js..." browserify -g browserify-shim -s GraphiQL dist/index.js > graphiql.js echo "Bundling graphiql.min.js..." -browserify -g browserify-shim -g uglifyify -s GraphiQL dist/index.js 2> /dev/null | uglifyjs -c --screw-ie8 > graphiql.min.js 2> /dev/null +browserify -g browserify-shim -t uglifyify -s GraphiQL graphiql.js | uglifyjs -c > graphiql.min.js echo "Bundling graphiql.css..." -postcss --use autoprefixer css/*.css -d dist/ +postcss --no-map --use autoprefixer -d dist/ css/*.css cat dist/*.css > graphiql.css echo "Done" diff --git a/resources/checkgit.sh b/packages/graphiql/resources/checkgit.sh similarity index 100% rename from resources/checkgit.sh rename to packages/graphiql/resources/checkgit.sh diff --git a/packages/graphiql/resources/graphiql.png b/packages/graphiql/resources/graphiql.png new file mode 100644 index 00000000000..de073bc9931 Binary files /dev/null and b/packages/graphiql/resources/graphiql.png differ diff --git a/packages/graphiql/resources/index.html.ejs b/packages/graphiql/resources/index.html.ejs new file mode 100644 index 00000000000..f3512006ed6 --- /dev/null +++ b/packages/graphiql/resources/index.html.ejs @@ -0,0 +1,51 @@ + + + + + GraphiQL + + + + + + + + + +
    Loading...
    + + + + diff --git a/packages/graphiql/resources/renderExample.js b/packages/graphiql/resources/renderExample.js new file mode 100644 index 00000000000..8e8e44ca096 --- /dev/null +++ b/packages/graphiql/resources/renderExample.js @@ -0,0 +1,106 @@ +/* global React, ReactDOM, GraphiQL, GraphQLVersion */ + +/** + * UMD GraphiQL Example + * + * This is a simple example that provides a primitive query string parser on top of GraphiQL props + * It assumes a global umd GraphiQL, which would be provided by an index.html in the default example + * + * It is used by: + * - the netlify demo + * - end to end tests + * - webpack dev server + */ + +// Parse the search string to get url parameters. +const parameters = {}; +for (const entry of window.location.search.slice(1).split('&')) { + const eq = entry.indexOf('='); + if (eq >= 0) { + parameters[decodeURIComponent(entry.slice(0, eq))] = decodeURIComponent( + entry.slice(eq + 1), + ); + } +} + +// When the query and variables string is edited, update the URL bar so +// that it can be easily shared. +function onEditQuery(newQuery) { + parameters.query = newQuery; + updateURL(); +} + +function onEditVariables(newVariables) { + parameters.variables = newVariables; + updateURL(); +} + +function onEditHeaders(newHeaders) { + parameters.headers = newHeaders; + updateURL(); +} + +function onTabChange(tabsState) { + const activeTab = tabsState.tabs[tabsState.activeTabIndex]; + parameters.query = activeTab.query; + parameters.variables = activeTab.variables; + parameters.headers = activeTab.headers; + updateURL(); +} + +function updateURL() { + const newSearch = Object.entries(parameters) + .filter(([_key, value]) => value) + .map( + ([key, value]) => + encodeURIComponent(key) + '=' + encodeURIComponent(value), + ) + .join('&'); + history.replaceState(null, null, `?${newSearch}`); +} + +function getSchemaUrl() { + const isDev = window.location.hostname.match(/localhost$/); + + if (isDev) { + // This supports an e2e test which ensures that invalid schemas do not load. + if (parameters.bad === 'true') { + return '/bad/graphql'; + } + if (parameters['http-error'] === 'true') { + return '/http-error/graphql'; + } + if (parameters['graphql-error'] === 'true') { + return '/graphql-error/graphql'; + } + return '/graphql'; + } + return '/.netlify/functions/schema-demo'; +} + +// Render into the body. +// See the README in the top level of this module to learn more about +// how you can customize GraphiQL by providing different values or +// additional child elements. +const root = ReactDOM.createRoot(document.getElementById('graphiql')); + +root.render( + React.createElement(GraphiQL, { + fetcher: GraphiQL.createFetcher({ + url: getSchemaUrl(), + subscriptionUrl: 'ws://localhost:8081/subscriptions', + }), + query: parameters.query, + variables: parameters.variables, + headers: parameters.headers, + defaultHeaders: parameters.defaultHeaders, + onEditQuery, + onEditVariables, + onEditHeaders, + defaultEditorToolsVisibility: true, + isHeadersEditorEnabled: true, + shouldPersistHeaders: true, + inputValueDeprecation: GraphQLVersion.includes('15.5') ? undefined : true, + onTabChange, + }), +); diff --git a/packages/graphiql/resources/webpack.config.js b/packages/graphiql/resources/webpack.config.js new file mode 100644 index 00000000000..20b85d82ba4 --- /dev/null +++ b/packages/graphiql/resources/webpack.config.js @@ -0,0 +1,133 @@ +const path = require('node:path'); +const webpack = require('webpack'); + +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); +const graphql = require('graphql'); +const rimraf = require('rimraf'); + +const relPath = (...args) => path.resolve(__dirname, ...args); +const rootPath = (...args) => relPath('../', ...args); + +const resultConfig = ({ isDev = false }) => { + const isHMR = Boolean(isDev && process.env.WEBPACK_DEV_SERVER); + + const config = { + mode: isDev ? 'development' : 'production', + entry: './cdn.ts', + context: rootPath('src'), + output: { + path: rootPath(), + library: 'GraphiQL', + libraryTarget: 'window', + libraryExport: 'default', + filename: isDev ? 'graphiql.js' : 'graphiql.min.js', + }, + devServer: { + hot: true, + // bypass simple localhost CORS restrictions by setting + // these to 127.0.0.1 in /etc/hosts + allowedHosts: ['local.example.com', 'graphiql.com'], + setupMiddlewares(middlewares, devServer) { + require('../test/beforeDevServer')(devServer.app); + + return middlewares; + }, + }, + devtool: isDev ? 'cheap-module-source-map' : 'source-map', + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + }, + + module: { + rules: [ + // for graphql module, which uses .mjs + { + type: 'javascript/auto', + test: /\.mjs$/, + use: [], + include: /node_modules/, + exclude: /\.(ts|d\.ts|d\.ts\.map)$/, + }, + // i think we need to add another rule for + // codemirror-graphql esm.js files to load + { + test: /\.(js|jsx|ts|tsx|mjs)$/, + use: [{ loader: 'babel-loader' }], + exclude: /\.(d\.ts|d\.ts\.map|spec\.tsx)$/, + }, + { + test: /\.css$/, + use: [{ loader: MiniCssExtractPlugin.loader }, 'css-loader'], + }, + { + test: /\.css$/, + exclude: /graphiql-react/, + use: ['postcss-loader'], + }, + ], + }, + + plugins: [ + // in order to prevent async modules for CDN builds + // until we can guarantee it will work with the CDN properly + // and so that graphiql.min.js can retain parity + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + + new HtmlWebpackPlugin({ + template: relPath('index.html.ejs'), + inject: 'head', + filename: isDev && !isHMR ? 'dev.html' : 'index.html', + graphqlVersion: JSON.stringify(graphql.version), + }), + new MiniCssExtractPlugin({ + // Options similar to the same options in webpackOptions.output + // both options are optional + filename: isDev ? 'graphiql.css' : 'graphiql.min.css', + chunkFilename: '[id].css', + }), + new ForkTsCheckerWebpackPlugin({ + async: isDev, + typescript: { + configFile: rootPath('tsconfig.json'), + }, + }), + new (class { + apply(compiler) { + compiler.hooks.done.tap('Remove LICENSE', () => { + console.log('Remove LICENSE.txt'); + rimraf.sync('./*.LICENSE.txt'); + }); + } + })(), + ], + resolve: { + extensions: ['.mjs', '.js', '.json', '.jsx', '.css', '.ts', '.tsx'], + modules: [ + rootPath('node_modules'), + rootPath('../', '../', 'node_modules'), + ], + }, + }; + + if (process.env.ANALYZE) { + config.plugins.push( + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + openAnalyzer: false, + reportFilename: rootPath('analyzer.html'), + }), + ); + } + return config; +}; + +module.exports = [ + resultConfig({ isDev: true }), + resultConfig({ isDev: false }), +]; diff --git a/packages/graphiql/src/cdn.ts b/packages/graphiql/src/cdn.ts new file mode 100644 index 00000000000..c4e231cc968 --- /dev/null +++ b/packages/graphiql/src/cdn.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2021 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import * as GraphiQLReact from '@graphiql/react'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import * as GraphQL from 'graphql'; +import { GraphiQL } from './components/GraphiQL'; + +import '@graphiql/react/font/roboto.css'; +import '@graphiql/react/font/fira-code.css'; +import '@graphiql/react/dist/style.css'; +import './style.css'; + +/** + * For the CDN bundle we add some static properties to the component function + * so that they can be accessed in the inline-script in the HTML file. + */ + +/** + * This function is needed in order to easily create a fetcher function. + */ +// @ts-expect-error +GraphiQL.createFetcher = createGraphiQLFetcher; + +/** + * We also add the complete `graphiql-js` exports so that this instance of + * `graphiql-js` can be reused from plugin CDN bundles. + */ +// @ts-expect-error +GraphiQL.GraphQL = GraphQL; + +/** + * We also add the complete `@graphiql/react` exports. These will be included + * in the bundle anyway since they make up the `GraphiQL` component, so by + * doing this we can reuse them from plugin CDN bundles. + */ +// @ts-expect-error +GraphiQL.React = GraphiQLReact; + +export default GraphiQL; diff --git a/packages/graphiql/src/components/GraphiQL.tsx b/packages/graphiql/src/components/GraphiQL.tsx new file mode 100644 index 00000000000..0bb9cd1c5f2 --- /dev/null +++ b/packages/graphiql/src/components/GraphiQL.tsx @@ -0,0 +1,937 @@ +/** + * Copyright (c) 2020 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { + ComponentType, + Fragment, + MouseEventHandler, + PropsWithChildren, + ReactNode, + ReactElement, + useCallback, + useState, +} from 'react'; + +import { + Button, + ButtonGroup, + ChevronDownIcon, + ChevronUpIcon, + CopyIcon, + Dialog, + ExecuteButton, + GraphiQLProvider, + GraphiQLProviderProps, + HeaderEditor, + KeyboardShortcutIcon, + MergeIcon, + PlusIcon, + PrettifyIcon, + QueryEditor, + ReloadIcon, + ResponseEditor, + SettingsIcon, + Spinner, + Tab, + Tabs, + ToolbarButton, + Tooltip, + UnStyledButton, + useCopyQuery, + useDragResize, + useEditorContext, + useExecutionContext, + UseHeaderEditorArgs, + useMergeQuery, + usePluginContext, + usePrettifyEditors, + UseQueryEditorArgs, + UseResponseEditorArgs, + useSchemaContext, + useStorageContext, + useTheme, + UseVariableEditorArgs, + VariableEditor, + WriteableEditorProps, +} from '@graphiql/react'; + +const majorVersion = parseInt(React.version.slice(0, 2), 10); + +if (majorVersion < 16) { + throw new Error( + [ + 'GraphiQL 0.18.0 and after is not compatible with React 15 or below.', + 'If you are using a CDN source (jsdelivr, unpkg, etc), follow this example:', + 'https://github.com/graphql/graphiql/blob/master/examples/graphiql-cdn/index.html#L49', + ].join('\n'), + ); +} + +export type GraphiQLToolbarConfig = { + /** + * This content will be rendered after the built-in buttons of the toolbar. + * Note that this will not apply if you provide a completely custom toolbar + * (by passing `GraphiQL.Toolbar` as child to the `GraphiQL` component). + */ + additionalContent?: React.ReactNode; +}; + +/** + * API docs for this live here: + * + * https://graphiql-test.netlify.app/typedoc/modules/graphiql.html#graphiqlprops + */ +export type GraphiQLProps = Omit & + GraphiQLInterfaceProps; + +/** + * The top-level React component for GraphiQL, intended to encompass the entire + * browser viewport. + * + * @see https://github.com/graphql/graphiql#usage + */ + +export function GraphiQL({ + dangerouslyAssumeSchemaIsValid, + defaultQuery, + defaultTabs, + externalFragments, + fetcher, + getDefaultFieldNames, + headers, + inputValueDeprecation, + introspectionQueryName, + maxHistoryLength, + onEditOperationName, + onSchemaChange, + onTabChange, + onTogglePluginVisibility, + operationName, + plugins, + query, + response, + schema, + schemaDescription, + shouldPersistHeaders, + storage, + validationRules, + variables, + visiblePlugin, + defaultHeaders, + ...props +}: GraphiQLProps) { + // Ensure props are correct + if (typeof fetcher !== 'function') { + throw new TypeError( + 'The `GraphiQL` component requires a `fetcher` function to be passed as prop.', + ); + } + + return ( + + + + ); +} + +// Export main windows/panes to be used separately if desired. +GraphiQL.Logo = GraphiQLLogo; +GraphiQL.Toolbar = GraphiQLToolbar; +GraphiQL.Footer = GraphiQLFooter; + +type AddSuffix, Suffix extends string> = { + [Key in keyof Obj as `${string & Key}${Suffix}`]: Obj[Key]; +}; + +export type GraphiQLInterfaceProps = WriteableEditorProps & + AddSuffix, 'Query'> & + Pick & + AddSuffix, 'Variables'> & + AddSuffix, 'Headers'> & + Pick & { + children?: ReactNode; + /** + * Set the default state for the editor tools. + * - `false` hides the editor tools + * - `true` shows the editor tools + * - `'variables'` specifically shows the variables editor + * - `'headers'` specifically shows the headers editor + * By default the editor tools are initially shown when at least one of the + * editors has contents. + */ + defaultEditorToolsVisibility?: boolean | 'variables' | 'headers'; + /** + * Toggle if the headers editor should be shown inside the editor tools. + * @default true + */ + isHeadersEditorEnabled?: boolean; + /** + * An object that allows configuration of the toolbar next to the query + * editor. + */ + toolbar?: GraphiQLToolbarConfig; + /** + * Indicates if settings for persisting headers should appear in the + * settings modal. + */ + showPersistHeadersSettings?: boolean; + }; + +export function GraphiQLInterface(props: GraphiQLInterfaceProps) { + const isHeadersEditorEnabled = props.isHeadersEditorEnabled ?? true; + const editorContext = useEditorContext({ nonNull: true }); + const executionContext = useExecutionContext({ nonNull: true }); + const schemaContext = useSchemaContext({ nonNull: true }); + const storageContext = useStorageContext(); + const pluginContext = usePluginContext(); + + const copy = useCopyQuery({ onCopyQuery: props.onCopyQuery }); + const merge = useMergeQuery(); + const prettify = usePrettifyEditors(); + + const { theme, setTheme } = useTheme(); + + const PluginContent = pluginContext?.visiblePlugin?.content; + + const pluginResize = useDragResize({ + defaultSizeRelation: 1 / 3, + direction: 'horizontal', + initiallyHidden: pluginContext?.visiblePlugin ? undefined : 'first', + onHiddenElementChange(resizableElement) { + if (resizableElement === 'first') { + pluginContext?.setVisiblePlugin(null); + } + }, + sizeThresholdSecond: 200, + storageKey: 'docExplorerFlex', + }); + const editorResize = useDragResize({ + direction: 'horizontal', + storageKey: 'editorFlex', + }); + const editorToolsResize = useDragResize({ + defaultSizeRelation: 3, + direction: 'vertical', + initiallyHidden: (() => { + if ( + props.defaultEditorToolsVisibility === 'variables' || + props.defaultEditorToolsVisibility === 'headers' + ) { + return; + } + + if (typeof props.defaultEditorToolsVisibility === 'boolean') { + return props.defaultEditorToolsVisibility ? undefined : 'second'; + } + + return editorContext.initialVariables || editorContext.initialHeaders + ? undefined + : 'second'; + })(), + sizeThresholdSecond: 60, + storageKey: 'secondaryEditorFlex', + }); + + const [activeSecondaryEditor, setActiveSecondaryEditor] = useState< + 'variables' | 'headers' + >(() => { + if ( + props.defaultEditorToolsVisibility === 'variables' || + props.defaultEditorToolsVisibility === 'headers' + ) { + return props.defaultEditorToolsVisibility; + } + return !editorContext.initialVariables && + editorContext.initialHeaders && + isHeadersEditorEnabled + ? 'headers' + : 'variables'; + }); + const [showDialog, setShowDialog] = useState< + 'settings' | 'short-keys' | null + >(null); + const [clearStorageStatus, setClearStorageStatus] = useState< + 'success' | 'error' | null + >(null); + + const children = React.Children.toArray(props.children); + + const logo = children.find(child => + isChildComponentType(child, GraphiQL.Logo), + ) || ; + + const toolbar = children.find(child => + isChildComponentType(child, GraphiQL.Toolbar), + ) || ( + <> + + + + + + + {props.toolbar?.additionalContent} + + ); + + const footer = children.find(child => + isChildComponentType(child, GraphiQL.Footer), + ); + + const onClickReference = useCallback(() => { + if (pluginResize.hiddenElement === 'first') { + pluginResize.setHiddenElement(null); + } + }, [pluginResize]); + + const handleClearData = useCallback(() => { + try { + storageContext?.clear(); + setClearStorageStatus('success'); + } catch { + setClearStorageStatus('error'); + } + }, [storageContext]); + + const handlePersistHeaders: MouseEventHandler = + useCallback( + event => { + editorContext.setShouldPersistHeaders( + event.currentTarget.dataset.value === 'true', + ); + }, + [editorContext], + ); + + const handleChangeTheme: MouseEventHandler = useCallback( + event => { + const selectedTheme = event.currentTarget.dataset.theme as + | 'light' + | 'dark' + | undefined; + setTheme(selectedTheme || null); + }, + [setTheme], + ); + + const handleAddTab = editorContext.addTab; + const handleRefetchSchema = schemaContext.introspect; + const handleReorder = editorContext.moveTab; + + const handleShowDialog: MouseEventHandler = useCallback( + event => { + setShowDialog( + event.currentTarget.dataset.value as 'short-keys' | 'settings', + ); + }, + [], + ); + + const handlePluginClick: MouseEventHandler = useCallback( + e => { + const context = pluginContext!; + const pluginIndex = Number(e.currentTarget.dataset.index!); + const plugin = context.plugins.find((_, index) => pluginIndex === index)!; + const isVisible = plugin === context.visiblePlugin; + if (isVisible) { + context.setVisiblePlugin(null); + pluginResize.setHiddenElement('first'); + } else { + context.setVisiblePlugin(plugin); + pluginResize.setHiddenElement(null); + } + }, + [pluginContext, pluginResize], + ); + + const handleToolsTabClick: MouseEventHandler = useCallback( + event => { + if (editorToolsResize.hiddenElement === 'second') { + editorToolsResize.setHiddenElement(null); + } + setActiveSecondaryEditor( + event.currentTarget.dataset.name as 'variables' | 'headers', + ); + }, + [editorToolsResize], + ); + + const toggleEditorTools: MouseEventHandler = + useCallback(() => { + editorToolsResize.setHiddenElement( + editorToolsResize.hiddenElement === 'second' ? null : 'second', + ); + }, [editorToolsResize]); + + const handleOpenShortKeysDialog = useCallback((isOpen: boolean) => { + if (!isOpen) { + setShowDialog(null); + } + }, []); + + const handleOpenSettingsDialog = useCallback((isOpen: boolean) => { + if (!isOpen) { + setShowDialog(null); + setClearStorageStatus(null); + } + }, []); + + const addTab = ( + + + + + ); + + return ( + +
    +
    +
    + {pluginContext?.plugins.map((plugin, index) => { + const isVisible = plugin === pluginContext.visiblePlugin; + const label = `${isVisible ? 'Hide' : 'Show'} ${plugin.title}`; + const Icon = plugin.icon; + return ( + + + + + ); + })} +
    +
    + + + + + + + + + + + + +
    +
    +
    +
    +
    + {PluginContent ? : null} +
    +
    + {pluginContext?.visiblePlugin && ( +
    + )} +
    +
    + + {editorContext.tabs.length > 1 && ( + <> + {editorContext.tabs.map((tab, index) => ( + + { + executionContext.stop(); + editorContext.changeTab(index); + }} + > + {tab.title} + + { + if (editorContext.activeTabIndex === index) { + executionContext.stop(); + } + editorContext.closeTab(index); + }} + /> + + ))} + {addTab} + + )} + +
    + {editorContext.tabs.length === 1 && addTab} + {logo} +
    +
    +
    +
    +
    +
    +
    + +
    + + {toolbar} +
    +
    +
    + +
    +
    + + Variables + + {isHeadersEditorEnabled && ( + + Headers + + )} + + + + {editorToolsResize.hiddenElement === 'second' ? ( + + +
    +
    + +
    +
    + + {isHeadersEditorEnabled && ( + + )} +
    +
    +
    +
    + +
    + +
    +
    + {executionContext.isFetching ? : null} + + {footer} +
    +
    +
    +
    +
    + +
    + + Short Keys + + +
    +
    + +
    +
    + +
    + + Settings + + +
    + {props.showPersistHeadersSettings ? ( +
    +
    +
    + Persist headers +
    +
    + Save headers upon reloading.{' '} + + Only enable if you trust this device. + +
    +
    + + + + +
    + ) : null} +
    +
    +
    Theme
    +
    + Adjust how the interface looks like. +
    +
    + + + + + +
    + {storageContext ? ( +
    +
    +
    + Clear storage +
    +
    + Remove all locally stored data and start fresh. +
    +
    + +
    + ) : null} +
    +
    + + ); +} + +const modifier = + typeof window !== 'undefined' && + window.navigator.platform.toLowerCase().indexOf('mac') === 0 + ? 'Cmd' + : 'Ctrl'; + +const SHORT_KEYS = Object.entries({ + 'Search in editor': [modifier, 'F'], + 'Search in documentation': [modifier, 'K'], + 'Execute query': [modifier, 'Enter'], + 'Prettify editors': ['Ctrl', 'Shift', 'P'], + 'Merge fragments definitions into operation definition': [ + 'Ctrl', + 'Shift', + 'M', + ], + 'Copy query': ['Ctrl', 'Shift', 'C'], + 'Re-fetch schema using introspection': ['Ctrl', 'Shift', 'R'], +}); + +function ShortKeys({ keyMap }: { keyMap: string }): ReactElement { + return ( +
    + + + + + + + + + {SHORT_KEYS.map(([title, keys]) => ( + + + + + ))} + +
    Short KeyFunction
    + {keys.map((key, index, array) => ( + + {key} + {index !== array.length - 1 && ' + '} + + ))} + {title}
    +

    + The editors use{' '} + + CodeMirror Key Maps + {' '} + that add more short keys. This instance of GraphiQL uses{' '} + {keyMap}. +

    +
    + ); +} + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLLogo(props: PropsWithChildren) { + return ( +
    + {props.children || ( + + Graph + i + QL + + )} +
    + ); +} + +GraphiQLLogo.displayName = 'GraphiQLLogo'; + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLToolbar(props: PropsWithChildren) { + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{props.children}; +} + +GraphiQLToolbar.displayName = 'GraphiQLToolbar'; + +// Configure the UI by providing this Component as a child of GraphiQL. +function GraphiQLFooter(props: PropsWithChildren) { + return
    {props.children}
    ; +} + +GraphiQLFooter.displayName = 'GraphiQLFooter'; + +// Determines if the React child is of the same type of the provided React component +function isChildComponentType( + child: any, + component: T, +): child is T { + if ( + child?.type?.displayName && + child.type.displayName === component.displayName + ) { + return true; + } + + return child.type === component; +} diff --git a/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx new file mode 100644 index 00000000000..8eacd3b6719 --- /dev/null +++ b/packages/graphiql/src/components/__tests__/GraphiQL.spec.tsx @@ -0,0 +1,720 @@ +/** + * Copyright (c) 2021 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +import '@testing-library/jest-dom'; +import { act, render, waitFor, fireEvent } from '@testing-library/react'; +import React from 'react'; +import { GraphiQL } from '../GraphiQL'; +import { Fetcher } from '@graphiql/toolkit'; +import { ToolbarButton } from '@graphiql/react'; + +// The smallest possible introspection result that builds a schema. +const simpleIntrospection = { + data: { + __schema: { + queryType: { name: 'Q' }, + types: [ + { + kind: 'OBJECT', + name: 'Q', + interfaces: [], + fields: [{ name: 'q', args: [], type: { name: 'Q' } }], + }, + ], + }, + }, +}; + +beforeEach(() => { + window.localStorage.clear(); +}); + +describe('GraphiQL', () => { + const noOpFetcher: Fetcher = () => {}; + + describe('fetcher', () => { + it('should throw error without fetcher', () => { + const spy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // @ts-expect-error fetcher is a required prop to GraphiQL + expect(() => render()).toThrow( + 'The `GraphiQL` component requires a `fetcher` function to be passed as prop.', + ); + spy.mockRestore(); + }); + + it('should construct correctly with fetcher', async () => { + await act(async () => { + expect(() => render()).not.toThrow(); + }); + }); + + it('should refetch schema with new fetcher', async () => { + let firstCalled = false; + + function firstFetcher() { + firstCalled = true; + return Promise.resolve(simpleIntrospection); + } + + let secondCalled = false; + + function secondFetcher() { + secondCalled = true; + return Promise.resolve(simpleIntrospection); + } + + // Initial render calls fetcher + const { rerender } = render(); + + await waitFor(() => { + expect(firstCalled).toEqual(true); + }); + + // Re-render does not call fetcher again + firstCalled = false; + await act(async () => { + rerender(); + }); + + await waitFor(() => { + expect(firstCalled).toEqual(false); + }); + + // Re-render with new fetcher is called. + await act(async () => { + rerender(); + }); + + await waitFor(() => { + expect(secondCalled).toEqual(true); + }); + }); + + it('should refresh schema with new fetcher after a fetchError', async () => { + function firstFetcher() { + return Promise.reject('Schema Error'); + } + function secondFetcher() { + return Promise.resolve(simpleIntrospection); + } + + // Use a bad fetcher for our initial render + const { rerender, container, getByLabelText } = render( + , + ); + + const showDocExplorerButton = getByLabelText( + 'Show Documentation Explorer', + ); + + await waitFor(() => { + expect(showDocExplorerButton).not.toBe(null); + }); + + act(() => { + fireEvent.click(showDocExplorerButton); + }); + + await waitFor(() => { + expect( + container.querySelector('.graphiql-doc-explorer-error'), + ).not.toBe(null); + }); + + // Re-render with valid fetcher + await act(async () => { + rerender(); + }); + + await waitFor(() => { + expect(container.querySelector('.graphiql-doc-explorer-error')).toBe( + null, + ); + }); + }); + }); // fetcher + + describe('schema', () => { + it('should not throw error if schema missing and query provided', async () => { + await act(async () => { + expect(() => + render(), + ).not.toThrow(); + }); + }); + }); // schema + + describe('default query', () => { + it('defaults to the built-in default query', async () => { + const { container } = render(); + + await waitFor(() => { + const mockEditor = container.querySelector( + '.graphiql-query-editor .mockCodeMirror', + ); + expect(mockEditor.value).toContain('# Welcome to GraphiQL'); + }); + }); + + it('accepts a custom default query', async () => { + const { container } = render( + , + ); + + await waitFor(() => { + expect( + container.querySelector('.graphiql-query-editor .mockCodeMirror'), + ).toHaveValue('GraphQL Party!!'); + }); + }); + }); // default query + + // TODO: rewrite these plugin tests after plugin API has more structure + describe('plugins', () => { + it('displays correct plugin when visiblePlugin prop is used', async () => { + const { container } = render( + , + ); + await waitFor(() => { + expect( + container.querySelector('.graphiql-doc-explorer'), + ).toBeInTheDocument(); + }); + }); + + it('defaults to not displaying plugin pane', async () => { + const { container } = render(); + + await waitFor(() => { + expect(container.querySelector('.graphiql-plugin')).not.toBeVisible(); + }); + }); + }); // plugins + + describe('editor tools', () => { + it('can control the default editor tools visibility', async () => { + const { container } = render(); + + const editorToolTabPanelWrap = container.querySelector( + '.graphiql-editor-tool', + ); + + await waitFor(() => { + expect(editorToolTabPanelWrap).not.toBeVisible(); + }); + + const secondaryEditorTitle = container.querySelector( + '.graphiql-editor-tools', + ); + + // drag the editor tools handle up + act(() => { + fireEvent.mouseDown(secondaryEditorTitle); + fireEvent.mouseMove(secondaryEditorTitle, { buttons: 1, clientY: 50 }); + }); + + await waitFor(() => { + expect(editorToolTabPanelWrap).toBeVisible(); + }); + }); + + it('correctly displays variables editor when using defaultEditorToolsVisibility prop', async () => { + const { container } = render( + , + ); + await waitFor(() => { + expect( + container.querySelector('[aria-label="Variables"]'), + ).toBeVisible(); + }); + }); + + it('correctly displays headers editor when using defaultEditorToolsVisibility prop', async () => { + const { container } = render( + , + ); + await waitFor(() => { + expect(container.querySelector('[aria-label="Headers"]')).toBeVisible(); + }); + }); + + it('correctly hides editor tools when using defaultEditorToolsVisibility prop is false but either of the editors has a value', async () => { + const { container } = render( + , + ); + + const editorToolTabPanelWrap = container.querySelector( + '.graphiql-editor-tool', + ); + + await waitFor(() => { + expect(editorToolTabPanelWrap).not.toBeVisible(); + }); + }); + }); // editor tools + + describe('panel resizing', () => { + it('readjusts the query wrapper flex style field when the result panel is resized', async () => { + // Mock the drag bar width + const clientWidthSpy = jest + .spyOn(Element.prototype, 'clientWidth', 'get') + .mockReturnValue(0); + // Mock the container width + const boundingClientRectSpy = jest + .spyOn(Element.prototype, 'getBoundingClientRect') + // @ts-expect-error missing properties from type 'DOMRect' + .mockReturnValue({ left: 0, right: 900 }); + + const { container } = render(); + + const dragBar = container.querySelector('.graphiql-horizontal-drag-bar'); + const editors = container.querySelector('.graphiql-editors'); + + act(() => { + fireEvent.mouseDown(dragBar, { + button: 0, + ctrlKey: false, + }); + + fireEvent.mouseMove(dragBar, { + buttons: 1, + clientX: 700, + }); + + fireEvent.mouseUp(dragBar); + }); + + await waitFor(() => { + // 700 / (900 - 700) = 3.5 + expect(editors.parentElement.style.flex).toEqual('3.5'); + }); + + clientWidthSpy.mockRestore(); + boundingClientRectSpy.mockRestore(); + }); + + it('allows for resizing the doc explorer correctly', async () => { + // Mock the drag bar width + const clientWidthSpy = jest + .spyOn(Element.prototype, 'clientWidth', 'get') + .mockReturnValue(0); + // Mock the container width + const boundingClientRectSpy = jest + .spyOn(Element.prototype, 'getBoundingClientRect') + // @ts-expect-error missing properties from type 'DOMRect' + .mockReturnValue({ left: 0, right: 1200 }); + + const { container } = render(); + + act(() => { + fireEvent.click( + container.querySelector('[aria-label="Show Documentation Explorer"]'), + ); + }); + + const dragBar = container.querySelectorAll( + '.graphiql-horizontal-drag-bar', + )[0]; + + act(() => { + fireEvent.mouseDown(dragBar, { + clientX: 3, + }); + + fireEvent.mouseMove(dragBar, { + buttons: 1, + clientX: 800, + }); + fireEvent.mouseUp(dragBar); + }); + + await waitFor(() => { + // 797 / (1200 - 797) = 1.977667493796526 + expect( + container.querySelector('.graphiql-plugin')?.parentElement.style.flex, + ).toBe('1.977667493796526'); + }); + + clientWidthSpy.mockRestore(); + boundingClientRectSpy.mockRestore(); + }); + }); // panel resizing + + it('allows the user to control persisting headers if it is true', async () => { + const { container, findByText } = render( + , + ); + + act(() => { + fireEvent.click( + container.querySelector('[aria-label="Open settings dialog"]')!, + ); + }); + + const element = await findByText('Persist headers'); + expect(element).toBeInTheDocument(); + }); + + it('allows the user to control persisting headers if it is not passed in', async () => { + const { container, findByText } = render( + , + ); + + act(() => { + fireEvent.click( + container.querySelector('[aria-label="Open settings dialog"]')!, + ); + }); + + const element = await findByText('Persist headers'); + expect(element).toBeInTheDocument(); + }); + + it('does not allow the user to control persisting headers is false', async () => { + const { container, findByText } = render( + , + ); + + act(() => { + fireEvent.click( + container.querySelector('[aria-label="Open settings dialog"]')!, + ); + }); + + const callback = async () => { + try { + await findByText('Persist headers'); + } catch { + // eslint-disable-next-line no-throw-literal + throw 'failed'; + } + }; + await expect(callback).rejects.toEqual('failed'); + }); + + describe('Tabs', () => { + it('show tabs if there are more than one', async () => { + const { container } = render(); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tabs .graphiql-tab'), + ).toHaveLength(0); + }); + + act(() => { + fireEvent.click(container.querySelector('.graphiql-tab-add')); + }); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tabs .graphiql-tab'), + ).toHaveLength(2); + }); + + act(() => { + fireEvent.click(container.querySelector('.graphiql-tab-add')); + }); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tabs .graphiql-tab'), + ).toHaveLength(3); + }); + }); + + it('each tab has a close button when multiple tabs are open', async () => { + const { container } = render(); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tab .graphiql-tab-close'), + ).toHaveLength(0); + }); + + act(() => { + fireEvent.click(container.querySelector('.graphiql-tab-add')); + }); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tab .graphiql-tab-close'), + ).toHaveLength(2); + }); + + act(() => { + fireEvent.click(container.querySelector('.graphiql-tab-add')); + }); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tab .graphiql-tab-close'), + ).toHaveLength(3); + }); + }); + + it('close button removes a tab', async () => { + const { container } = render(); + + act(() => { + fireEvent.click(container.querySelector('.graphiql-tab-add')); + }); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tab .graphiql-tab-close'), + ).toHaveLength(2); + }); + + act(() => { + fireEvent.click( + container.querySelector('.graphiql-tab .graphiql-tab-close'), + ); + }); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tabs .graphiql-tab'), + ).toHaveLength(0); + expect( + container.querySelectorAll('.graphiql-tab .graphiql-tab-close'), + ).toHaveLength(0); + }); + }); + + it('shows default tabs', async () => { + const { container } = render( + , + ); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-tabs .graphiql-tab'), + ).toHaveLength(2); + }); + }); + }); + + describe('children overrides', () => { + const MyFunctionalComponent = () => { + return null; + }; + + it('properly ignores fragments', async () => { + const myFragment = ( + + + + + ); + + const { container, getByRole } = render( + {myFragment}, + ); + + await waitFor(() => { + expect( + container.querySelector('.graphiql-container'), + ).toBeInTheDocument(); + expect(container.querySelector('.graphiql-logo')).toBeInTheDocument(); + expect(getByRole('toolbar')).toBeInTheDocument(); + }); + }); + + it('properly ignores non-override children components', async () => { + const { container, getByRole } = render( + + + , + ); + + await waitFor(() => { + expect( + container.querySelector('.graphiql-container'), + ).toBeInTheDocument(); + expect(container.querySelector('.graphiql-logo')).toBeInTheDocument(); + expect(getByRole('toolbar')).toBeInTheDocument(); + }); + }); + + it('properly ignores non-override class components', async () => { + // eslint-disable-next-line react/prefer-stateless-function + class MyClassComponent extends React.Component { + render() { + return null; + } + } + + const { container, getByRole } = render( + + + , + ); + + await waitFor(() => { + expect( + container.querySelector('.graphiql-container'), + ).toBeInTheDocument(); + expect(container.querySelector('.graphiql-logo')).toBeInTheDocument(); + expect(getByRole('toolbar')).toBeInTheDocument(); + }); + }); + + describe('GraphiQL.Logo', () => { + it('can be overridden using the exported type', async () => { + const { getByText } = render( + + My Exported Type Logo + , + ); + + await waitFor(() => { + expect(getByText('My Exported Type Logo')).toBeInTheDocument(); + }); + }); + + it('can be overridden using a named component', async () => { + const WrappedLogo = () => { + return ( +
    + My Named Component Logo +
    + ); + }; + WrappedLogo.displayName = 'GraphiQLLogo'; + + const { container, getByText } = render( + + + , + ); + + await waitFor(() => { + expect(container.querySelector('.test-wrapper')).toBeInTheDocument(); + expect(getByText('My Named Component Logo')).toBeInTheDocument(); + }); + }); + }); + + describe('GraphiQL.Toolbar', () => { + it('can be overridden using the exported type', async () => { + const { container } = render( + + + + + , + ); + + await waitFor(() => { + expect( + container.querySelectorAll( + '[role="toolbar"] .graphiql-toolbar-button', + ), + ).toHaveLength(1); + }); + }); + + it('can be overridden using a named component', async () => { + const WrappedToolbar = () => { + return ( +
    + + + + , +
    + ); + }; + WrappedToolbar.displayName = 'GraphiQLToolbar'; + + const { container } = render( + + + , + ); + + await waitFor(() => { + expect(container.querySelector('.test-wrapper')).toBeInTheDocument(); + expect( + container.querySelectorAll( + '[role="toolbar"] .graphiql-toolbar-button', + ), + ).toHaveLength(1); + }); + }); + }); + + describe('GraphiQL.Footer', () => { + it('can be overridden using the exported type', async () => { + const { container } = render( + + + + + , + ); + + await waitFor(() => { + expect( + container.querySelectorAll('.graphiql-footer button'), + ).toHaveLength(1); + }); + }); + + it('can be overridden using a named component', async () => { + const WrappedFooter = () => { + return ( +
    + + + + , +
    + ); + }; + WrappedFooter.displayName = 'GraphiQLFooter'; + + const { container } = render( + + + , + ); + + await waitFor(() => { + expect(container.querySelector('.test-wrapper')).toBeInTheDocument(); + expect( + container.querySelectorAll('.graphiql-footer button'), + ).toHaveLength(1); + }); + }); + }); + }); +}); diff --git a/packages/graphiql/src/index.ts b/packages/graphiql/src/index.ts new file mode 100644 index 00000000000..b4ea740e2e5 --- /dev/null +++ b/packages/graphiql/src/index.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2021 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * GraphiQL + */ + +export { GraphiQLProvider } from '@graphiql/react'; + +/** + * Definitions + */ +export type { + GraphiQLProps, + GraphiQLInterfaceProps, +} from './components/GraphiQL'; +export type { GraphiQLProviderProps } from '@graphiql/react'; + +export { + GraphiQLInterface, + GraphiQL, + GraphiQL as default, +} from './components/GraphiQL'; diff --git a/packages/graphiql/src/style.css b/packages/graphiql/src/style.css new file mode 100644 index 00000000000..aa3120e4a59 --- /dev/null +++ b/packages/graphiql/src/style.css @@ -0,0 +1,340 @@ +/* Everything */ +.graphiql-container { + background-color: hsl(var(--color-base)); + display: flex; + height: 100%; + margin: 0; + overflow: hidden; + width: 100%; +} + +/* The sidebar */ +.graphiql-container .graphiql-sidebar { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: var(--px-8); + width: var(--sidebar-width); +} + +.graphiql-container .graphiql-sidebar .graphiql-sidebar-section { + display: flex; + flex-direction: column; + gap: var(--px-8); +} + +.graphiql-container .graphiql-sidebar button { + display: flex; + align-items: center; + justify-content: center; + color: hsla(var(--color-neutral), var(--alpha-secondary)); + height: calc(var(--sidebar-width) - (2 * var(--px-8))); + width: calc(var(--sidebar-width) - (2 * var(--px-8))); +} + +.graphiql-container .graphiql-sidebar button.active { + color: hsla(var(--color-neutral), 1); +} + +.graphiql-container .graphiql-sidebar button:not(:first-child) { + margin-top: var(--px-4); +} + +.graphiql-container .graphiql-sidebar button > svg { + height: var(--px-20); + width: var(--px-20); +} + +/* The main content, i.e. everything except the sidebar */ +.graphiql-container .graphiql-main { + display: flex; + flex: 1; + min-width: 0; +} + +/* The current session and tabs */ +.graphiql-container .graphiql-sessions { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + /* Adding the 8px of padding to the inner border radius of the query editor */ + border-radius: calc(var(--border-radius-12) + var(--px-8)); + display: flex; + flex-direction: column; + flex: 1; + max-height: 100%; + margin: var(--px-16); + margin-left: 0; + min-width: 0; +} + +/* The session header containing tabs and the logo */ +.graphiql-container .graphiql-session-header { + align-items: center; + display: flex; + justify-content: space-between; + height: var(--session-header-height); +} + +/* The button to add a new tab */ +button.graphiql-tab-add { + height: 100%; + padding: var(--px-4); +} + +button.graphiql-tab-add > svg { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + display: block; + height: var(--px-16); + width: var(--px-16); +} + +/* The right-hand-side of the session header */ +.graphiql-container .graphiql-session-header-right { + align-items: center; + display: flex; +} + +/* The GraphiQL logo */ +.graphiql-container .graphiql-logo { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); + padding: var(--px-12) var(--px-16); +} + +/* Undo default link styling for the default GraphiQL logo link */ +.graphiql-container .graphiql-logo .graphiql-logo-link { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + text-decoration: none; +} + +/* The editor of the session */ +.graphiql-container .graphiql-session { + display: flex; + flex: 1; + padding: 0 var(--px-8) var(--px-8); +} + +/* All editors (query, variable, headers) */ +.graphiql-container .graphiql-editors { + background-color: hsl(var(--color-base)); + border-radius: calc(var(--border-radius-12)); + box-shadow: var(--popover-box-shadow); + display: flex; + flex: 1; + flex-direction: column; +} + +.graphiql-container .graphiql-editors.full-height { + margin-top: calc(var(--px-8) - var(--session-header-height)); +} + +/* The query editor and the toolbar */ +.graphiql-container .graphiql-query-editor { + border-bottom: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding: var(--px-16); + column-gap: var(--px-16); + display: flex; + width: 100%; +} + +/* The vertical toolbar next to the query editor */ +.graphiql-container .graphiql-toolbar { + width: var(--toolbar-width); +} + +.graphiql-container .graphiql-toolbar > * + * { + margin-top: var(--px-8); +} + +/* The toolbar icons */ +.graphiql-toolbar-icon { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + display: block; + height: calc(var(--toolbar-width) - (var(--px-8) * 2)); + width: calc(var(--toolbar-width) - (var(--px-8) * 2)); +} + +/* The tab bar for editor tools */ +.graphiql-container .graphiql-editor-tools { + cursor: row-resize; + display: flex; + width: 100%; + column-gap: var(--px-8); + padding: var(--px-8); +} + +.graphiql-container .graphiql-editor-tools button { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} + +.graphiql-container .graphiql-editor-tools button.active { + color: hsla(var(--color-neutral), 1); +} + +/* The tab buttons to switch between editor tools */ +.graphiql-container + .graphiql-editor-tools + > button:not(.graphiql-toggle-editor-tools) { + padding: var(--px-8) var(--px-12); +} + +.graphiql-container .graphiql-editor-tools .graphiql-toggle-editor-tools { + margin-left: auto; +} + +/* An editor tool, e.g. variable or header editor */ +.graphiql-container .graphiql-editor-tool { + flex: 1; + padding: var(--px-16); +} + +/** + * The way CodeMirror editors are styled they overflow their containing + * element. For some OS-browser-combinations this might cause overlap issues, + * setting the position of this to `relative` makes sure this element will + * always be on top of any editors. + */ +.graphiql-container .graphiql-toolbar, +.graphiql-container .graphiql-editor-tools, +.graphiql-container .graphiql-editor-tool { + position: relative; +} + +/* The response view */ +.graphiql-container .graphiql-response { + --editor-background: transparent; + display: flex; + width: 100%; + flex-direction: column; +} + +/* The results editor wrapping container */ +.graphiql-container .graphiql-response .result-window { + position: relative; + flex: 1; +} + +/* The footer below the response view */ +.graphiql-container .graphiql-footer { + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); +} + +/* The plugin container */ +.graphiql-container .graphiql-plugin { + border-left: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + flex: 1; + overflow-y: auto; + padding: var(--px-16); +} + +/* Generic drag bar for horizontal resizing */ +.graphiql-horizontal-drag-bar { + width: var(--px-12); + cursor: col-resize; +} + +.graphiql-horizontal-drag-bar:hover::after { + border: var(--px-2) solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-radius: var(--border-radius-2); + content: ''; + display: block; + height: 25%; + margin: 0 auto; + position: relative; + /* (100% - 25%) / 2 = 37.5% */ + top: 37.5%; + width: 0; +} + +.graphiql-container .graphiql-chevron-icon { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + display: block; + height: var(--px-12); + margin: var(--px-12); + width: var(--px-12); +} + +/* Generic spin animation */ +.graphiql-spin { + animation: spin 0.8s linear 0s infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +/* The header of the settings dialog */ +.graphiql-dialog .graphiql-dialog-header { + align-items: center; + display: flex; + justify-content: space-between; + padding: var(--px-24); +} + +/* The title of the settings dialog */ +.graphiql-dialog .graphiql-dialog-title { + font-size: var(--font-size-h3); + font-weight: var(--font-weight-medium); + margin: 0; +} + +/* A section inside the settings dialog */ +.graphiql-dialog .graphiql-dialog-section { + align-items: center; + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + display: flex; + justify-content: space-between; + padding: var(--px-24); +} + +.graphiql-dialog .graphiql-dialog-section > :not(:first-child) { + margin-left: var(--px-24); +} + +/* The section title in the settings dialog */ +.graphiql-dialog .graphiql-dialog-section-title { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} + +/* The section caption in the settings dialog */ +.graphiql-dialog .graphiql-dialog-section-caption { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} + +.graphiql-dialog .graphiql-warning-text { + color: hsl(var(--color-warning)); + font-weight: var(--font-weight-medium); +} + +.graphiql-dialog .graphiql-table { + border-collapse: collapse; + width: 100%; +} + +.graphiql-dialog .graphiql-table :is(th, td) { + border: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding: var(--px-8) var(--px-12); +} + +/* A single key the short-key dialog */ +.graphiql-dialog .graphiql-key { + background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); + border-radius: var(--border-radius-4); + padding: var(--px-4); +} + +/* Avoid showing native tooltips for icons with titles */ +.graphiql-container svg { + pointer-events: none; +} diff --git a/packages/graphiql/src/types.ts b/packages/graphiql/src/types.ts new file mode 100644 index 00000000000..ea4cc282e52 --- /dev/null +++ b/packages/graphiql/src/types.ts @@ -0,0 +1,22 @@ +export type Maybe = T | null | undefined; + +export type ReactComponentLike = + | string + | ((props: any, context?: any) => any) + | (new (props: any, context?: any) => any); + +export type ReactElementLike = { + type: ReactComponentLike; + props: any; + key: string | number | null; +}; + +export type ReactNodeLike = + | {} + | ReactElementLike + | Array + | string + | number + | boolean + | null + | undefined; diff --git a/packages/graphiql/test/README.md b/packages/graphiql/test/README.md new file mode 100644 index 00000000000..3d08557e2ce --- /dev/null +++ b/packages/graphiql/test/README.md @@ -0,0 +1,9 @@ +# Test GraphiQL Application + +This test folder serves as a basis for testing in-development changes and offers +watching/compiling of the `src` folder. To utilize this, simply: + +1. Run `npm install` +2. Run `npm run dev` +3. Open your browser to the address listed in your console. e.g. + `Started on http://localhost:49811/` diff --git a/packages/graphiql/test/afterDevServer.js b/packages/graphiql/test/afterDevServer.js new file mode 100644 index 00000000000..d47ef13f274 --- /dev/null +++ b/packages/graphiql/test/afterDevServer.js @@ -0,0 +1,13 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const { useServer } = require('graphql-ws/lib/use/ws'); +const { Server: WebSocketServer } = require('ws'); +const schema = require('./schema'); + +module.exports = function afterDevServer(_app, _server, _compiler) { + const wsServer = new WebSocketServer({ + path: '/subscriptions', + port: 8081, + }); + // eslint-disable-next-line react-hooks/rules-of-hooks + useServer({ schema }, wsServer); +}; diff --git a/packages/graphiql/test/bad-schema.js b/packages/graphiql/test/bad-schema.js new file mode 100644 index 00000000000..2ec51e0625c --- /dev/null +++ b/packages/graphiql/test/bad-schema.js @@ -0,0 +1,97 @@ +module.exports.schema = { + __schema: { + queryType: { + name: 'Query', + }, + mutationType: null, + subscriptionType: null, + types: [ + { + kind: 'OBJECT', + name: 'Query', + description: null, + fields: [ + { + name: 'user', + description: null, + args: [ + { + name: 'id', + description: null, + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'ID', + ofType: null, + }, + }, + defaultValue: null, + }, + ], + type: { + kind: 'OBJECT', + name: '', + ofType: null, + }, + isDeprecated: false, + deprecationReason: null, + }, + ], + inputFields: null, + interfaces: [], + enumValues: null, + possibleTypes: null, + }, + { + kind: 'SCALAR', + name: 'ID', + description: '', + fields: null, + inputFields: null, + interfaces: null, + enumValues: null, + possibleTypes: null, + }, + { + kind: 'OBJECT', + name: '', + description: null, + fields: [ + { + name: 'name', + description: null, + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, + ], + inputFields: null, + interfaces: [], + enumValues: null, + possibleTypes: null, + }, + { + kind: 'SCALAR', + name: 'String', + description: '', + fields: null, + inputFields: null, + interfaces: null, + enumValues: null, + possibleTypes: null, + }, + ], + directives: [], + }, +}; diff --git a/packages/graphiql/test/beforeDevServer.js b/packages/graphiql/test/beforeDevServer.js new file mode 100644 index 00000000000..d386ae47922 --- /dev/null +++ b/packages/graphiql/test/beforeDevServer.js @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const express = require('express'); +const path = require('node:path'); +const { createHandler } = require('graphql-http/lib/use/express'); +const schema = require('./schema'); +const { schema: badSchema } = require('./bad-schema'); + +module.exports = function beforeDevServer(app, _server, _compiler) { + // GraphQL Server + app.post('/graphql', createHandler({ schema })); + app.get('/graphql', createHandler({ schema })); + + app.post('/bad/graphql', (_req, res, next) => { + res.json({ data: badSchema }); + next(); + }); + + app.use('/images', express.static(path.join(__dirname, 'images'))); + + app.use( + '/resources/renderExample.js', + express.static(path.join(__dirname, '../resources/renderExample.js')), + ); +}; diff --git a/packages/graphiql/test/e2e-server.js b/packages/graphiql/test/e2e-server.js new file mode 100644 index 00000000000..a714e5be590 --- /dev/null +++ b/packages/graphiql/test/e2e-server.js @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2021 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* eslint-disable no-console */ +const express = require('express'); +const path = require('node:path'); +const { createHandler } = require('graphql-http/lib/use/express'); +const { GraphQLError } = require('graphql'); +const schema = require('./schema'); +const app = express(); +const { schema: badSchema } = require('./bad-schema'); +const WebSocketsServer = require('./afterDevServer'); + +// Server +app.post('/graphql', createHandler({ schema })); + +app.get( + '/graphql', + createHandler({ + schema, + }), +); + +app.post('/bad/graphql', (_req, res, next) => { + res.json({ data: badSchema }); + next(); +}); + +app.post('/http-error/graphql', (_req, res, next) => { + res.status(502).send('Bad Gateway'); + next(); +}); + +app.post('/graphql-error/graphql', (_req, res, next) => { + res.json({ errors: [new GraphQLError('Something unexpected happened...')] }); + next(); +}); + +app.use(express.static(path.resolve(__dirname, '../'))); +app.use('index.html', express.static(path.resolve(__dirname, '../dev.html'))); + +app.listen(process.env.PORT || 0, function () { + const { port } = this.address(); + + console.log(`Started on http://localhost:${port}/`); + console.log('PID', process.pid); + + process.once('SIGINT', () => { + process.exit(); + }); + process.once('SIGTERM', () => { + process.exit(); + }); +}); + +WebSocketsServer(); diff --git a/packages/graphiql/test/images/logo.svg b/packages/graphiql/test/images/logo.svg new file mode 100644 index 00000000000..337843aca18 --- /dev/null +++ b/packages/graphiql/test/images/logo.svg @@ -0,0 +1 @@ + diff --git a/packages/graphiql/test/schema.js b/packages/graphiql/test/schema.js new file mode 100644 index 00000000000..fcd648096f1 --- /dev/null +++ b/packages/graphiql/test/schema.js @@ -0,0 +1,391 @@ +/* eslint-disable no-await-in-loop */ +/** + * Copyright (c) 2021 GraphQL Contributors. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const { + GraphQLSchema, + GraphQLObjectType, + GraphQLUnionType, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLBoolean, + GraphQLInt, + GraphQLFloat, + GraphQLString, + GraphQLID, + GraphQLList, +} = require('graphql'); + +// Test Schema +const TestEnum = new GraphQLEnumType({ + name: 'TestEnum', + description: 'An enum of super cool colors.', + values: { + RED: { description: 'A rosy color' }, + GREEN: { description: 'The color of martians and slime' }, + BLUE: { description: "A feeling you might have if you can't use GraphQL" }, + GRAY: { + description: 'A really dull color', + deprecationReason: 'Colors are available now.', + }, + }, +}); + +const TestInputObject = new GraphQLInputObjectType({ + name: 'TestInput', + description: 'Test all sorts of inputs in this input object type.', + fields: () => ({ + string: { + type: GraphQLString, + description: 'Repeats back this string', + }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + defaultValueString: { + type: GraphQLString, + defaultValue: 'test default value', + }, + defaultValueBoolean: { + type: GraphQLBoolean, + defaultValue: false, + }, + defaultValueInt: { + type: GraphQLInt, + defaultValue: 5, + }, + // 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) }, + }), +}); + +const TestInterface = new GraphQLInterfaceType({ + name: 'TestInterface', + description: 'Test interface.', + fields: () => ({ + name: { + type: GraphQLString, + description: 'Common name string.', + }, + }), + resolveType(check) { + return check ? UnionFirst : UnionSecond; + }, +}); + +const UnionFirst = new GraphQLObjectType({ + name: 'First', + fields: () => ({ + name: { + type: GraphQLString, + description: 'Common name string for UnionFirst.', + }, + first: { + type: new GraphQLList(TestInterface), + resolve: () => true, + }, + }), + interfaces: [TestInterface], +}); + +const UnionSecond = new GraphQLObjectType({ + name: 'Second', + fields: () => ({ + name: { + type: GraphQLString, + description: 'Common name string for UnionFirst.', + }, + second: { + type: TestInterface, + resolve: () => false, + }, + }), + interfaces: [TestInterface], +}); + +const TestUnion = new GraphQLUnionType({ + name: 'TestUnion', + types: [UnionFirst, UnionSecond], + resolveType() { + return UnionFirst; + }, +}); + +const Greeting = new GraphQLObjectType({ + name: 'Greeting', + fields: { + text: { + type: GraphQLString, + }, + }, +}); + +const delayArgument = (defaultValue = 400) => ({ + description: + 'delay in milliseconds for subsequent results, for demonstration purposes', + type: GraphQLInt, + defaultValue, +}); + +const DeferrableObject = new GraphQLObjectType({ + name: 'Deferrable', + fields: { + normalString: { + type: GraphQLString, + resolve: () => 'Nice', + }, + deferredString: { + args: { + delay: delayArgument(600), + }, + type: GraphQLString, + resolve: async function lazilyReturnValue(_value, args) { + const seconds = args.delay / 1000; + await sleep(args.delay); + return `Oops, this took ${seconds} seconds longer than I thought it would!`; + }, + }, + }, +}); + +const Person = new GraphQLObjectType({ + name: 'Person', + fields: () => ({ + name: { + type: GraphQLString, + resolve: obj => obj.name, + }, + age: { + args: { + delay: delayArgument(600), + }, + type: GraphQLInt, + resolve: async function lazilyReturnValue(_value, args) { + await sleep(args.delay); + return Math.ceil(args.delay); + }, + }, + friends: { + type: new GraphQLList(Person), + async *resolve(_value, _args) { + const names = ['James', 'Mary', 'John', 'Patrica']; // Top 4 names https://www.ssa.gov/oact/babynames/decades/century.html + for (const name of names) { + await sleep(100); + yield { name }; + } + }, + }, + }), +}); + +const sleep = async timeout => new Promise(res => setTimeout(res, timeout)); + +const longDescription = ` +The \`longDescriptionType\` field on the \`Test\` type has a long, verbose, description to test inline field docs. + +> We want to test several \`markdown\` styles! + +Check out [Markdown](https://www.markdownguide.org/) by the way. + +Some notes: +- Lists +- work + - also nested + - and with very very very very very very very very very very long items that span multiple lines +- you get the gist + +To-Do's: +1. Open GraphiQL +2. Write a query + 1. Maybe add some variables + 2. Could also add headers +3. Send the request + +Example quey: +\`\`\`graphql +{ + test { + id + } + hasArgs(string: "very very very very very long string") +} +\`\`\` + +And we have a cool logo: + +![](/images/logo.svg) +`.trim(); + +const TestType = new GraphQLObjectType({ + name: 'Test', + description: 'Test type for testing\n New line works', + fields: () => ({ + test: { + type: TestType, + description: '`test` field from `Test` type.', + resolve: () => ({}), + }, + deferrable: { + type: DeferrableObject, + resolve: () => ({}), + }, + streamable: { + type: new GraphQLList(Greeting), + args: { + delay: delayArgument(300), + }, + resolve: async function* sayHiInSomeLanguages(_value, args) { + let i = 0; + for (const hi of [ + 'Hi', + 'δ½ ε₯½', + 'Hola', + 'Ψ£Ω‡Ω„Ψ§Ω‹', + 'Bonjour', + 'Ψ³Ω„Ψ§Ω…', + 'μ•ˆλ…•', + 'Ciao', + 'ΰ€Ήΰ₯‡ΰ€²ΰ₯‹', + 'Π—Π΄ΠΎΡ€ΠΎΠ²ΠΎ', + ]) { + if (i > 2) { + await sleep(args.delay); + } + i++; + yield { text: hi }; + } + }, + }, + person: { + type: Person, + resolve: () => ({ name: 'Mark' }), + }, + longDescriptionType: { + type: TestType, + description: longDescription, + resolve: () => ({}), + }, + union: { + type: TestUnion, + resolve: () => ({}), + }, + id: { + type: GraphQLID, + description: 'id field from Test type.', + resolve: () => 'abc123', + }, + isTest: { + type: GraphQLBoolean, + description: 'Is this a test schema? Sure it is.', + resolve: () => true, + }, + image: { + type: GraphQLString, + description: 'field that returns an image URI.', + resolve: () => '/images/logo.svg', + }, + deprecatedField: { + type: TestType, + description: 'This field is an example of a deprecated field', + deprecationReason: 'No longer in use, try `test` instead.', + }, + alsoDeprecated: { + type: TestType, + description: + 'This field is an example of a deprecated field with markdown in its deprecation reason', + deprecationReason: longDescription, + }, + hasArgs: { + type: GraphQLString, + resolve(_value, args) { + return JSON.stringify(args); + }, + args: { + string: { type: GraphQLString, description: 'A string' }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + defaultValue: { + type: GraphQLString, + defaultValue: 'test default value', + }, + // 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) }, + deprecatedArg: { + type: GraphQLString, + deprecationReason: 'deprecated argument', + description: 'Hello!', + }, + }, + }, + }), +}); + +const 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 TestSubscriptionType = new GraphQLObjectType({ + name: 'SubscriptionType', + description: + 'This is a simple subscription type. Learn more at https://www.npmjs.com/package/graphql-ws', + fields: { + message: { + type: GraphQLString, + description: 'Subscribe to a message', + args: { + delay: delayArgument(600), + }, + async *subscribe(root, args) { + for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) { + if (args?.delay) { + await sleep(args.delay); + } + yield { message: hi }; + } + }, + }, + }, +}); + +const myTestSchema = new GraphQLSchema({ + query: TestType, + mutation: TestMutationType, + subscription: TestSubscriptionType, + description: 'This is a test schema for GraphiQL', +}); + +module.exports = myTestSchema; diff --git a/packages/graphiql/tsconfig.esm.json b/packages/graphiql/tsconfig.esm.json new file mode 100644 index 00000000000..4c9526db8ef --- /dev/null +++ b/packages/graphiql/tsconfig.esm.json @@ -0,0 +1,31 @@ +{ + "extends": "../../resources/tsconfig.base.esm.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./esm", + "composite": false, + "jsx": "react", + "baseUrl": ".", + "target": "es5" + }, + "include": ["src"], + "exclude": [ + "**/__tests__/**", + "**/dist/**.*", + "**/*.spec.ts", + "**/*.spec.js", + "**/*-test.ts", + "**/*-test.js" + ], + "references": [ + { + "path": "../codemirror-graphql" + }, + { + "path": "../graphiql-toolkit" + }, + { + "path": "../graphql-language-service" + } + ] +} diff --git a/packages/graphiql/tsconfig.json b/packages/graphiql/tsconfig.json new file mode 100644 index 00000000000..2cf7d555c31 --- /dev/null +++ b/packages/graphiql/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../resources/tsconfig.base.cjs.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "composite": true, + "jsx": "react", + "target": "es5", + "strictPropertyInitialization": false + }, + "include": ["src"], + "exclude": ["**/__tests__/**", "**/dist/**.*", "cypress/**"], + "references": [ + { + "path": "../graphiql-toolkit" + }, + { + "path": "../codemirror-graphql" + }, + { + "path": "../graphql-language-service" + } + ] +} diff --git a/packages/graphql-language-service-cli/.npmignore b/packages/graphql-language-service-cli/.npmignore new file mode 100644 index 00000000000..1e433b22baa --- /dev/null +++ b/packages/graphql-language-service-cli/.npmignore @@ -0,0 +1,3 @@ +node_modules +src +yarn.lock diff --git a/packages/graphql-language-service-cli/CHANGELOG.md b/packages/graphql-language-service-cli/CHANGELOG.md new file mode 100644 index 00000000000..10e20bf5882 --- /dev/null +++ b/packages/graphql-language-service-cli/CHANGELOG.md @@ -0,0 +1,1026 @@ +# graphql-language-service-cli + +## 3.3.25 + +### Patch Changes + +- [#3322](https://github.com/graphql/graphiql/pull/3322) [`6939bac4`](https://github.com/graphql/graphiql/commit/6939bac4a9a849fe497260fd0702bdd95eefd943) Thanks [@acao](https://github.com/acao)! - Bypass babel typescript parsing errors to continue extracting graphql strings + +- Updated dependencies [[`6939bac4`](https://github.com/graphql/graphiql/commit/6939bac4a9a849fe497260fd0702bdd95eefd943)]: + - graphql-language-service-server@2.11.3 + +## 3.3.24 + +### Patch Changes + +- [#3224](https://github.com/graphql/graphiql/pull/3224) [`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9) Thanks [@acao](https://github.com/acao)! - try removing some packages from pre.json + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5), [`55135804`](https://github.com/graphql/graphiql/commit/551358045611a27551e5654c2b115295c35639d8)]: + - graphql-language-service-server@2.11.2 + - graphql-language-service@5.1.7 + +## 3.3.24-alpha.0 + +### Patch Changes + +- [#3224](https://github.com/graphql/graphiql/pull/3224) [`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9) Thanks [@acao](https://github.com/acao)! - try removing some packages from pre.json + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5), [`55135804`](https://github.com/graphql/graphiql/commit/551358045611a27551e5654c2b115295c35639d8)]: + - graphql-language-service-server@2.11.2-alpha.0 + - graphql-language-service@5.1.7-alpha.0 + +## 3.3.23 + +### Patch Changes + +- Updated dependencies [[`4c3a08b1`](https://github.com/graphql/graphiql/commit/4c3a08b1a99e0933362a1c93340b613730c90aa4)]: + - graphql-language-service-server@2.11.1 + +## 3.3.22 + +### Patch Changes + +- [#3148](https://github.com/graphql/graphiql/pull/3148) [`06007498`](https://github.com/graphql/graphiql/commit/06007498880528ed75dd4d705dcbcd7c9e775939) Thanks [@mskelton](https://github.com/mskelton)! - Use native LSP logger instead of manual file based logging. This fixes errors in Neovim when using the GraphQL LSP. + +- Updated dependencies [[`06007498`](https://github.com/graphql/graphiql/commit/06007498880528ed75dd4d705dcbcd7c9e775939), [`28b1b5a0`](https://github.com/graphql/graphiql/commit/28b1b5a016787ec4119d28f057a9d93814d4e310)]: + - graphql-language-service-server@2.11.0 + - graphql-language-service@5.1.6 + +## 3.3.21 + +### Patch Changes + +- Updated dependencies [[`f2040452`](https://github.com/graphql/graphiql/commit/f20404529677635f5d4792b328aa648641bf8d9c)]: + - graphql-language-service-server@2.10.0 + +## 3.3.20 + +### Patch Changes + +- Updated dependencies [[`4d33b221`](https://github.com/graphql/graphiql/commit/4d33b2214e941f171385a1b72a1fa995714bb284)]: + - graphql-language-service-server@2.9.10 + - graphql-language-service@5.1.5 + +## 3.3.19 + +### Patch Changes + +- Updated dependencies [[`632a7c6b`](https://github.com/graphql/graphiql/commit/632a7c6bb2959ef5d59236aeab218587578466e7)]: + - graphql-language-service-server@2.9.9 + +## 3.3.18 + +### Patch Changes + +- [#3109](https://github.com/graphql/graphiql/pull/3109) [`51007002`](https://github.com/graphql/graphiql/commit/510070028b7d8e98f2ba25f396519976aea5fa4b) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-floating-promises` eslint rule + +- Updated dependencies [[`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9), [`06d39823`](https://github.com/graphql/graphiql/commit/06d39823e093c8441fea469446c25f18a664e778), [`51007002`](https://github.com/graphql/graphiql/commit/510070028b7d8e98f2ba25f396519976aea5fa4b), [`15c26eb6`](https://github.com/graphql/graphiql/commit/15c26eb6d621a85df9eecb2b8a5fa009fa2fe040)]: + - graphql-language-service@5.1.4 + - graphql-language-service-server@2.9.8 + +## 3.3.17 + +### Patch Changes + +- [#3046](https://github.com/graphql/graphiql/pull/3046) [`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer .at() method for index access + +- Updated dependencies [[`9d9478ae`](https://github.com/graphql/graphiql/commit/9d9478aea7536d2957e4371cef4f30577db2113d), [`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d), [`881a2024`](https://github.com/graphql/graphiql/commit/881a202497d5a58eb5260a5aa54c0c88930d69a0)]: + - graphql-language-service-server@2.9.7 + - graphql-language-service@5.1.3 + +## 3.3.16 + +### Patch Changes + +- [#2940](https://github.com/graphql/graphiql/pull/2940) [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-node-protocol` rule + +- Updated dependencies [[`e68cb8bc`](https://github.com/graphql/graphiql/commit/e68cb8bcaf9baddf6fca747abab871ecd1bc7a4c), [`f788e65a`](https://github.com/graphql/graphiql/commit/f788e65aff267ec873237034831d1fd936222a9b), [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4), [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d), [`90350022`](https://github.com/graphql/graphiql/commit/90350022334d9fcce0f4b72b3b0f7a12d21f78f9), [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba)]: + - graphql-language-service@5.1.2 + - graphql-language-service-server@2.9.6 + +## 3.3.15 + +### Patch Changes + +- [#2922](https://github.com/graphql/graphiql/pull/2922) [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b) Thanks [@B2o5T](https://github.com/B2o5T)! - extends `plugin:import/recommended` and fix warnings + +- [#2966](https://github.com/graphql/graphiql/pull/2966) [`f9aa87dc`](https://github.com/graphql/graphiql/commit/f9aa87dc6a88ed8a8a0a94de520c7a41fff8ffde) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `sonarjs/no-small-switch` and `sonarjs/no-duplicated-branches` rules + +- [#2938](https://github.com/graphql/graphiql/pull/2938) [`6a9d913f`](https://github.com/graphql/graphiql/commit/6a9d913f0d1b847124286b3fa1f3a2649d315171) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/throw-new-error` rule + +- Updated dependencies [[`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147), [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b), [`4a8b2e17`](https://github.com/graphql/graphiql/commit/4a8b2e1766a38eb4828cf9a81bf9d767070041de), [`f9aa87dc`](https://github.com/graphql/graphiql/commit/f9aa87dc6a88ed8a8a0a94de520c7a41fff8ffde), [`10e97bbe`](https://github.com/graphql/graphiql/commit/10e97bbe6c9ff81bae73b11ba81ac2b69eca2772), [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9), [`c44ea4f1`](https://github.com/graphql/graphiql/commit/c44ea4f1917b97daac815c08299b934c8ca57ed9), [`d502a33b`](https://github.com/graphql/graphiql/commit/d502a33b4332f1025e947c02d7cfdc5799365c8d), [`0669767e`](https://github.com/graphql/graphiql/commit/0669767e1e2196a78cbefe3679a52bcbb341e913), [`18f8e80a`](https://github.com/graphql/graphiql/commit/18f8e80ae12edfd0c36adcb300cf9e06ac27ea49), [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215), [`6a9d913f`](https://github.com/graphql/graphiql/commit/6a9d913f0d1b847124286b3fa1f3a2649d315171), [`4ff2794c`](https://github.com/graphql/graphiql/commit/4ff2794c8b6032168e27252096cb276ce712878e)]: + - graphql-language-service@5.1.1 + - graphql-language-service-server@2.9.5 + +## 3.3.14 + +### Patch Changes + +- [#2901](https://github.com/graphql/graphiql/pull/2901) [`eff4fd6b`](https://github.com/graphql/graphiql/commit/eff4fd6b9087c2d9cdb260ee2502a31d23769c3f) Thanks [@acao](https://github.com/acao)! - Reload the language service when a legacy format .graphqlconfig file has changed + +- Updated dependencies [[`eff4fd6b`](https://github.com/graphql/graphiql/commit/eff4fd6b9087c2d9cdb260ee2502a31d23769c3f)]: + - graphql-language-service-server@2.9.4 + +## 3.3.13 + +### Patch Changes + +- [#2900](https://github.com/graphql/graphiql/pull/2900) [`8989ffce`](https://github.com/graphql/graphiql/commit/8989ffce7d6beca874e70f5a1ff066102580173a) Thanks [@acao](https://github.com/acao)! - use decorators-legacy @babel/parser plugin so that all styles of decorator usage are supported +- Updated dependencies [[`8989ffce`](https://github.com/graphql/graphiql/commit/8989ffce7d6beca874e70f5a1ff066102580173a)]: + - graphql-language-service-server@2.9.3 + +## 3.3.12 + +### Patch Changes + +- Updated dependencies [[`bdd1bd04`](https://github.com/graphql/graphiql/commit/bdd1bd045fc6610ccaae4745b8ecc10004594274), [`967006a6`](https://github.com/graphql/graphiql/commit/967006a68e56f8f3a605c69fee5f920afdb6d8cf)]: + - graphql-language-service-server@2.9.2 + +## 3.3.11 + +### Patch Changes + +- [#2829](https://github.com/graphql/graphiql/pull/2829) [`c835ca87`](https://github.com/graphql/graphiql/commit/c835ca87e93e00713fbbbb2f4448db03f6b97b10) Thanks [@acao](https://github.com/acao)! - svelte language support, using the vue sfc parser introduced for vue support + +- Updated dependencies [[`c835ca87`](https://github.com/graphql/graphiql/commit/c835ca87e93e00713fbbbb2f4448db03f6b97b10), [`c835ca87`](https://github.com/graphql/graphiql/commit/c835ca87e93e00713fbbbb2f4448db03f6b97b10)]: + - graphql-language-service-server@2.9.1 + +## 3.3.10 + +### Patch Changes + +- Updated dependencies [[`b422003c`](https://github.com/graphql/graphiql/commit/b422003c2403072e96d14f920a3f0f1dc1f4f708)]: + - graphql-language-service-server@2.9.0 + +## 3.3.9 + +### Patch Changes + +- Updated dependencies [[`929152f8`](https://github.com/graphql/graphiql/commit/929152f8ea076ffa3bf34b83445473331c3bdb67)]: + - graphql-language-service-server@2.8.9 + +## 3.3.8 + +### Patch Changes + +- [#2812](https://github.com/graphql/graphiql/pull/2812) [`cf2e3061`](https://github.com/graphql/graphiql/commit/cf2e3061f67ef5cf6b890e217d20915d0eaec1bd) Thanks [@acao](https://github.com/acao)! - fix a bundling bug for vscode, rolling back graphql-config upgrade + +- Updated dependencies [[`cf2e3061`](https://github.com/graphql/graphiql/commit/cf2e3061f67ef5cf6b890e217d20915d0eaec1bd)]: + - graphql-language-service-server@2.8.8 + +## 3.3.7 + +### Patch Changes + +- Updated dependencies [[`f688422e`](https://github.com/graphql/graphiql/commit/f688422ed87ddd411cf3552fa6d9a5a367cd8662)]: + - graphql-language-service-server@2.8.7 + +## 3.3.6 + +### Patch Changes + +- Updated dependencies [[`a2071504`](https://github.com/graphql/graphiql/commit/a20715046fe7684bb9b17fbc9f5637b44e5210d6)]: + - graphql-language-service-server@2.8.6 + +## 3.3.5 + +### Patch Changes + +- [#2616](https://github.com/graphql/graphiql/pull/2616) [`b0d7f06c`](https://github.com/graphql/graphiql/commit/b0d7f06cf9ec6fd6b1dcb61dd0273e37dd546ed5) Thanks [@acao](https://github.com/acao)! - support vscode multi-root workspaces! creates an LSP server instance for each workspace. + + WARNING: large-scale vscode workspaces usage, and this in tandem with `graphql.config.*` multi-project configs could lead to excessive system resource usage. Optimizations coming soon. + +- Updated dependencies [[`b0d7f06c`](https://github.com/graphql/graphiql/commit/b0d7f06cf9ec6fd6b1dcb61dd0273e37dd546ed5)]: + - graphql-language-service-server@2.8.5 + +## 3.3.4 + +### Patch Changes + +- Updated dependencies [[`d6ff4d7a`](https://github.com/graphql/graphiql/commit/d6ff4d7a5d535a0c43fe5914016bac9ef0c2b782)]: + - graphql-language-service@5.1.0 + - graphql-language-service-server@2.8.4 + +## 3.3.3 + +### Patch Changes + +- Updated dependencies [[`721425b3`](https://github.com/graphql/graphiql/commit/721425b3382e68dd4c7b883473e3eda38a9816ee)]: + - graphql-language-service-server@2.8.3 + +## 3.3.2 + +### Patch Changes + +- [#2660](https://github.com/graphql/graphiql/pull/2660) [`34d31fbc`](https://github.com/graphql/graphiql/commit/34d31fbce6c49c929b48bdf1a6b0cebc33d8bbbf) Thanks [@acao](https://github.com/acao)! - bump `ts-node` to 10.x, so that TypeScript based configs (i.e. `.graphqlrc.ts`) will continue to work. It also bumps to the latest patch releases of `graphql-config` fixed several issues with TypeScript loading ([v4.3.2](https://github.com/kamilkisiela/graphql-config/releases/tag/v4.3.2), [v4.3.3](https://github.com/kamilkisiela/graphql-config/releases/tag/v4.3.3)). We tested manually, but please open a bug if you encounter any with schema-as-url configs & schema introspection. + +- Updated dependencies [[`34d31fbc`](https://github.com/graphql/graphiql/commit/34d31fbce6c49c929b48bdf1a6b0cebc33d8bbbf)]: + - graphql-language-service-server@2.8.2 + +## 3.3.1 + +### Patch Changes + +- Updated dependencies [[`12cf4db0`](https://github.com/graphql/graphiql/commit/12cf4db006d1c058460bc04f51d8743fe1ac63bb)]: + - graphql-language-service-server@2.8.1 + +## 3.3.0 + +### Minor Changes + +- [#2557](https://github.com/graphql/graphiql/pull/2557) [`3304606d`](https://github.com/graphql/graphiql/commit/3304606d5130a745cbdab0e6c9182e75101ddde9) Thanks [@acao](https://github.com/acao)! - upgrades the `vscode-languageserver` and `vscode-jsonrpc` reference implementations for the lsp server to the latest. also upgrades `vscode-languageclient` in `vscode-graphql` to the latest 8.0.1. seems to work fine for IPC in `vscode-graphql` at least! + + hopefully this solves #2230 once and for all! + +### Patch Changes + +- Updated dependencies [[`3304606d`](https://github.com/graphql/graphiql/commit/3304606d5130a745cbdab0e6c9182e75101ddde9)]: + - graphql-language-service-server@2.8.0 + +## 3.2.30 + +### Patch Changes + +- [#2553](https://github.com/graphql/graphiql/pull/2553) [`edc1c964`](https://github.com/graphql/graphiql/commit/edc1c96477cc2fbc2b6ac5d6195b8f9766a8c5d4) Thanks [@acao](https://github.com/acao)! - Fix error with LSP crash for CLI users #2230. `vscode-graphql` not impacted - rather, `nvim.coc`, maybe other clients who use CLI directly). recreation of #2546 by [@xuanduc987](https://github.com/xuanduc987, thank you!) + +- Updated dependencies [[`edc1c964`](https://github.com/graphql/graphiql/commit/edc1c96477cc2fbc2b6ac5d6195b8f9766a8c5d4)]: + - graphql-language-service-server@2.7.29 + +## 3.2.29 + +### Patch Changes + +- [#2519](https://github.com/graphql/graphiql/pull/2519) [`de5d5a07`](https://github.com/graphql/graphiql/commit/de5d5a07891fd49241a5abbb17eaf377a015a0a8) Thanks [@acao](https://github.com/acao)! - enable graphql-config legacy mode by default in the LSP server + +* [#2509](https://github.com/graphql/graphiql/pull/2509) [`737d4184`](https://github.com/graphql/graphiql/commit/737d4184f3af1d8fe9d64eb1b7e23dfcfbe640ea) Thanks [@Chnapy](https://github.com/Chnapy)! - Add `gql(``)`, `graphql(``)` call expressions support for highlighting & language + +* Updated dependencies [[`de5d5a07`](https://github.com/graphql/graphiql/commit/de5d5a07891fd49241a5abbb17eaf377a015a0a8), [`737d4184`](https://github.com/graphql/graphiql/commit/737d4184f3af1d8fe9d64eb1b7e23dfcfbe640ea)]: + - graphql-language-service-server@2.7.28 + +## 3.2.28 + +### Patch Changes + +- Updated dependencies [[`cccefa70`](https://github.com/graphql/graphiql/commit/cccefa70c0466d60e8496e1df61aeb1490af723c)]: + - graphql-language-service-server@2.7.27 + - graphql-language-service@5.0.6 + +## 3.2.27 + +### Patch Changes + +- [#2486](https://github.com/graphql/graphiql/pull/2486) [`c9c51b8a`](https://github.com/graphql/graphiql/commit/c9c51b8a98e1f0427272d3e9ad60989b32f1a1aa) Thanks [@stonexer](https://github.com/stonexer)! - definition support for operation fields ✨ + + you can now jump to the applicable object type definition for query/mutation/subscription fields! + +- Updated dependencies [[`c9c51b8a`](https://github.com/graphql/graphiql/commit/c9c51b8a98e1f0427272d3e9ad60989b32f1a1aa)]: + - graphql-language-service-server@2.7.26 + - graphql-language-service@5.0.5 + +## 3.2.26 + +### Patch Changes + +- Updated dependencies [[`cf092f59`](https://github.com/graphql/graphiql/commit/cf092f5960eae250bb193b9011b2fb883f797a99)]: + - graphql-language-service-server@2.7.25 + +## 3.2.25 + +### Patch Changes + +- Updated dependencies [[`d0017a93`](https://github.com/graphql/graphiql/commit/d0017a93b818cf3119e51c2b6c4a19004f98e29b)]: + - graphql-language-service-server@2.7.24 + +## 3.2.24 + +### Patch Changes + +- Updated dependencies [[`6ca6a92d`](https://github.com/graphql/graphiql/commit/6ca6a92d0fd12af974683de9706c8e8e06c751c2)]: + - graphql-language-service-server@2.7.23 + +## 3.2.23 + +### Patch Changes + +- Updated dependencies [[`6db28447`](https://github.com/graphql/graphiql/commit/6db284479a14873fea3e359efd71be0b15ab3ee8), [`1bea864d`](https://github.com/graphql/graphiql/commit/1bea864d05dee04bb20c06dc3c3d68675b87a50a)]: + - graphql-language-service-server@2.7.22 + +## 3.2.22 + +### Patch Changes + +- Updated dependencies [[`d22f6111`](https://github.com/graphql/graphiql/commit/d22f6111a60af25727d8dbc1058c79607df76af2)]: + - graphql-language-service@5.0.4 + - graphql-language-service-server@2.7.21 + +## 3.2.21 + +### Patch Changes + +- [#2291](https://github.com/graphql/graphiql/pull/2291) [`45cbc759`](https://github.com/graphql/graphiql/commit/45cbc759c732999e8b1eb4714d6047ab77c17902) Thanks [@retrodaredevil](https://github.com/retrodaredevil)! - Target es6 for the languages services + +- Updated dependencies [[`45cbc759`](https://github.com/graphql/graphiql/commit/45cbc759c732999e8b1eb4714d6047ab77c17902)]: + - graphql-language-service@5.0.3 + - graphql-language-service-server@2.7.20 + +## 3.2.20 + +### Patch Changes + +- Updated dependencies [[`c36504a8`](https://github.com/graphql/graphiql/commit/c36504a804d8cc54a5136340152999b4a1a2c69f)]: + - graphql-language-service@5.0.2 + - graphql-language-service-server@2.7.19 + +## 3.2.19 + +### Patch Changes + +- Updated dependencies [[`e15d1dae`](https://github.com/graphql/graphiql/commit/e15d1dae399a7d43d8d98f2ce431a9a1f0ba84ae)]: + - graphql-language-service-server@2.7.18 + +## 3.2.18 + +### Patch Changes + +- [#2267](https://github.com/graphql/graphiql/pull/2267) [`fe441272`](https://github.com/graphql/graphiql/commit/fe44127296f808e58407855c7f8806e04c8ddf03) Thanks [@elken](https://github.com/elken)! - Re-add `graphql-language-service-server` as a dep to `graphql-language-service-cli` + +## 3.2.17 + +### Patch Changes + +- [`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa) Thanks [@acao](https://github.com/acao)! - fix lockfile and imports from LSP merge + +- Updated dependencies [[`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa), [`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa)]: + - graphql-language-service@5.0.1 + +## 3.2.16 + +### Patch Changes + +- Updated dependencies [[`2502a364`](https://github.com/graphql/graphiql/commit/2502a364b74dc754d92baa1579b536cf42139958)]: + - graphql-language-service@5.0.0 + - graphql-language-service-server@2.7.16 + +## 3.2.15 + +### Patch Changes + +- Updated dependencies [[`ab83198f`](https://github.com/graphql/graphiql/commit/ab83198fa8b3c5453d3733982ee9ca8a2d6bca7a)]: + - graphql-language-service-server@2.7.15 + +## 3.2.14 + +### Patch Changes + +- Updated dependencies [[`484c0523`](https://github.com/graphql/graphiql/commit/484c0523cdd529f9e261d61a38616b6745075c7f), [`5852ba47`](https://github.com/graphql/graphiql/commit/5852ba47c720a2577817aed512bef9a262254f2c), [`48c5df65`](https://github.com/graphql/graphiql/commit/48c5df654e323cee3b8c57d7414247465235d1b5)]: + - graphql-language-service-server@2.7.14 + - graphql-language-service@4.1.5 + +## 3.2.13 + +### Patch Changes + +- Updated dependencies [[`08ff6dce`](https://github.com/graphql/graphiql/commit/08ff6dce0625f7ab58a45364aed9ca04c7862fa7)]: + - graphql-language-service-server@2.7.13 + - graphql-language-service@4.1.4 + +## 3.2.12 + +### Patch Changes + +- Updated dependencies [[`a44772d6`](https://github.com/graphql/graphiql/commit/a44772d6af97254c4f159ea7237e842a3e3719e8)]: + - graphql-language-service@4.1.3 + - graphql-language-service-server@2.7.12 + +## 3.2.11 + +### Patch Changes + +- Updated dependencies [[`e20760fb`](https://github.com/graphql/graphiql/commit/e20760fbd95c13d6d549cba3faa15a59aee9a2c0)]: + - graphql-language-service@4.1.2 + - graphql-language-service-server@2.7.11 + +## 3.2.10 + +### Patch Changes + +- Updated dependencies [[`ff9cebe5`](https://github.com/graphql/graphiql/commit/ff9cebe515a3539f85b9479954ae644dfeb68b63)]: + - graphql-language-service-server@2.7.10 + - graphql-language-service-utils@2.7.1 + - graphql-language-service@4.1.1 + +## 3.2.9 + +### Patch Changes + +- Updated dependencies [[`0f1f90ce`](https://github.com/graphql/graphiql/commit/0f1f90ce8f4a25ddebdaf7a9ddbe136214aa64a3)]: + - graphql-language-service@4.1.0 + - graphql-language-service-server@2.7.9 + +## 3.2.8 + +### Patch Changes + +- Updated dependencies [[`9df315b4`](https://github.com/graphql/graphiql/commit/9df315b44896efa313ed6744445fc8f9e702ebc3)]: + - graphql-language-service-utils@2.7.0 + - graphql-language-service@4.0.0 + - graphql-language-service-server@2.7.8 + +## 3.2.7 + +### Patch Changes + +- Updated dependencies [[`c4236190`](https://github.com/graphql/graphiql/commit/c4236190f91adedaf4f4a54cd0400a6b42c3c407), [`df57cd25`](https://github.com/graphql/graphiql/commit/df57cd2556302d6aa5dd140e7bee3f7bdab4deb1)]: + - graphql-language-service-server@2.7.7 + - graphql-language-service@3.2.5 + +## 3.2.6 + +### Patch Changes + +- Updated dependencies [[`4286185c`](https://github.com/graphql/graphiql/commit/4286185cdc6119175e23d66b8e177ba32693a63a)]: + - graphql-language-service-server@2.7.6 + +## 3.2.5 + +### Patch Changes + +- [`f82bd7a9`](https://github.com/graphql/graphiql/commit/f82bd7a931eb5fa9a33e59d417303706844c9063) [#2055](https://github.com/graphql/graphiql/pull/2055) Thanks [@acao](https://github.com/acao)! - this fixes the URI scheme related bugs and make sure schema as sdl config works again. + + `fileURLToPath` had been introduced by a contributor and I didn't test properly, it broke sdl file loading! + + definitions, autocomplete, diagnostics, etc should work again also hides the more verbose logging output for now + +- Updated dependencies [[`f82bd7a9`](https://github.com/graphql/graphiql/commit/f82bd7a931eb5fa9a33e59d417303706844c9063)]: + - graphql-language-service-server@2.7.5 + - graphql-language-service@3.2.4 + - graphql-language-service-utils@2.6.3 + +## 3.2.4 + +### Patch Changes + +- [`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf) [#2047](https://github.com/graphql/graphiql/pull/2047) Thanks [@willstott101](https://github.com/willstott101)! - Source code included in all packages to fix source maps. codemirror-graphql includes esm build in package. + +- Updated dependencies [[`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf)]: + - graphql-language-service@3.2.3 + - graphql-language-service-server@2.7.4 + - graphql-language-service-utils@2.6.2 + +## 3.2.3 + +### Patch Changes + +- Updated dependencies [[`858907d2`](https://github.com/graphql/graphiql/commit/858907d2106742a65ec52eb017f2e91268cc37bf)]: + - graphql-language-service@3.2.2 + - graphql-language-service-server@2.7.3 + - graphql-language-service-utils@2.6.1 + +## 3.2.2 + +### Patch Changes + +- Updated dependencies [[`7e98c6ff`](https://github.com/graphql/graphiql/commit/7e98c6fff3b1c62954c9c8d902ac64ddbf23fc5d)]: + - graphql-language-service-server@2.7.2 + +## 3.2.1 + +### Patch Changes + +- [`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce) [#2013](https://github.com/graphql/graphiql/pull/2013) Thanks [@PabloSzx](https://github.com/PabloSzx)! - Update utils + +- Updated dependencies [[`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce), [`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce)]: + - graphql-language-service-utils@2.6.0 + - graphql-language-service@3.2.1 + - graphql-language-service-server@2.7.1 + +## 3.2.0 + +### Minor Changes + +- [`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a) [#2010](https://github.com/graphql/graphiql/pull/2010) Thanks [@acao](https://github.com/acao)! - upgrade to `graphql@16.0.0-experimental-stream-defer.5`. thanks @saihaj! + +### Patch Changes + +- Updated dependencies [[`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a)]: + - graphql-language-service-server@2.7.0 + - graphql-language-service@3.2.0 + +## 3.1.14 + +### Patch Changes + +- [`83c4a007`](https://github.com/graphql/graphiql/commit/83c4a0070a4df704ce874ec977d65ca6c7e43ee8) [#1964](https://github.com/graphql/graphiql/pull/1964) Thanks [@patrickszmucer](https://github.com/patrickszmucer)! - Fix unknown fragment errors on save + +* [`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8) [#1777](https://github.com/graphql/graphiql/pull/1777) Thanks [@dwwoelfel](https://github.com/dwwoelfel)! - adopt block string parsing for variables in language parser + +* Updated dependencies [[`0e2c1a02`](https://github.com/graphql/graphiql/commit/0e2c1a020cc2761155f7c9467d3ed4cb45941aeb), [`83c4a007`](https://github.com/graphql/graphiql/commit/83c4a0070a4df704ce874ec977d65ca6c7e43ee8), [`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8)]: + - graphql-language-service@3.1.6 + - graphql-language-service-server@2.6.5 + +## 3.1.13 + +### Patch Changes + +- [`6869ce77`](https://github.com/graphql/graphiql/commit/6869ce7767050787db5f1017abf82fa5a52fc97a) [#1816](https://github.com/graphql/graphiql/pull/1816) Thanks [@acao](https://github.com/acao)! - improve peer resolutions for graphql 14 & 15. `14.5.0` minimum is for built-in typescript types, and another method only available in `14.4.0` + +## [3.1.12](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.11...graphql-language-service-cli@3.1.12) (2021-01-07) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.11](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.10...graphql-language-service-cli@3.1.11) (2021-01-07) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.10](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.9...graphql-language-service-cli@3.1.10) (2021-01-07) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.9](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.8...graphql-language-service-cli@3.1.9) (2021-01-03) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.8](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.7...graphql-language-service-cli@3.1.8) (2020-12-28) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.7](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.6...graphql-language-service-cli@3.1.7) (2020-12-08) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.6](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.5...graphql-language-service-cli@3.1.6) (2020-11-28) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.5](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.4...graphql-language-service-cli@3.1.5) (2020-10-20) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.4](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.3...graphql-language-service-cli@3.1.4) (2020-09-23) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.3](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.2...graphql-language-service-cli@3.1.3) (2020-09-23) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.2](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.1...graphql-language-service-cli@3.1.2) (2020-09-20) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.1](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.0...graphql-language-service-cli@3.1.1) (2020-09-20) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.0](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.0-alpha.5...graphql-language-service-cli@3.1.0) (2020-09-18) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.0-alpha.5](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.0-alpha.4...graphql-language-service-cli@3.1.0-alpha.5) (2020-09-11) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.0-alpha.4](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.0-alpha.3...graphql-language-service-cli@3.1.0-alpha.4) (2020-08-26) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.0-alpha.3](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.0-alpha.2...graphql-language-service-cli@3.1.0-alpha.3) (2020-08-22) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.0-alpha.2](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.0-alpha.1...graphql-language-service-cli@3.1.0-alpha.2) (2020-08-12) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.0-alpha.1](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.1.0-alpha.0...graphql-language-service-cli@3.1.0-alpha.1) (2020-08-12) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.1.0-alpha.0](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.0.1...graphql-language-service-cli@3.1.0-alpha.0) (2020-08-10) + +### Bug Fixes + +- pre-caching schema bugs, new server config options ([#1636](https://github.com/graphql/graphiql/issues/1636)) ([d989456](https://github.com/graphql/graphiql/commit/d9894564c056134e15093956e0951dcefe061d76)) + +### Features + +- graphql-config@3 support in lsp server ([#1616](https://github.com/graphql/graphiql/issues/1616)) ([27cd185](https://github.com/graphql/graphiql/commit/27cd18562b64dfe18e6343b6a49f3f606af89d86)) + +## [3.0.1](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.0.0...graphql-language-service-cli@3.0.1) (2020-08-06) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.0.0](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.0.0-alpha.5...graphql-language-service-cli@3.0.0) (2020-06-11) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.0.0-alpha.5](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.0.0-alpha.4...graphql-language-service-cli@3.0.0-alpha.5) (2020-06-04) + +**Note:** Version bump only for package graphql-language-service-cli + +## [3.0.0-alpha.4](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.0.0-alpha.3...graphql-language-service-cli@3.0.0-alpha.4) (2020-06-04) + +### Bug Fixes + +- cleanup cache entry from lerna publish ([4a26218](https://github.com/graphql/graphiql/commit/4a2621808a1aea8b30d5d27b8d86a60bf2b44b01)) + +## [3.0.0-alpha.3](https://github.com/graphql/graphiql/compare/graphql-language-service-cli@3.0.0-alpha.2...graphql-language-service-cli@3.0.0-alpha.3) (2020-05-28) + +**Note:** Version bump only for package graphql-language-service-cli + +# 3.0.0-alpha.2 (2020-05-19) + +**Note:** Version bump only for package graphql-language-service-cli + +## [2.4.0-alpha.8](https://github.com/graphql/graphiql/compare/graphql-language-service@2.4.0-alpha.7...graphql-language-service@2.4.0-alpha.8) (2020-05-17) + +### Bug Fixes + +- repair CLI, handle all schema and LSP errors ([#1482](https://github.com/graphql/graphiql/issues/1482)) ([992f384](https://github.com/graphql/graphiql/commit/992f38494f20f5877bfd6ff54893854ac7a0eaa2)) + +## [2.4.0-alpha.7](https://github.com/graphql/graphiql/compare/graphql-language-service@2.4.0-alpha.6...graphql-language-service@2.4.0-alpha.7) (2020-04-10) + +**Note:** Version bump only for package graphql-language-service + +## [2.4.0-alpha.6](https://github.com/graphql/graphiql/compare/graphql-language-service@2.4.0-alpha.5...graphql-language-service@2.4.0-alpha.6) (2020-04-10) + +**Note:** Version bump only for package graphql-language-service + +## [2.4.0-alpha.5](https://github.com/graphql/graphiql/compare/graphql-language-service@2.4.0-alpha.4...graphql-language-service@2.4.0-alpha.5) (2020-04-06) + +### Features + +- upgrade to graphql@15.0.0 for [#1191](https://github.com/graphql/graphiql/issues/1191) ([#1204](https://github.com/graphql/graphiql/issues/1204)) ([f13c8e9](https://github.com/graphql/graphiql/commit/f13c8e9d0e66df4b051b332c7d02f4bb83e07ffd)) + +## [2.4.0-alpha.4](https://github.com/graphql/graphiql/compare/graphql-language-service@2.4.0-alpha.3...graphql-language-service@2.4.0-alpha.4) (2020-04-03) + +**Note:** Version bump only for package graphql-language-service + +## [2.4.0-alpha.3](https://github.com/graphql/graphiql/compare/graphql-language-service@2.4.0-alpha.2...graphql-language-service@2.4.0-alpha.3) (2020-03-20) + +**Note:** Version bump only for package graphql-language-service + +## [2.4.0-alpha.2](https://github.com/graphql/graphiql/compare/graphql-language-service@2.4.0-alpha.0...graphql-language-service@2.4.0-alpha.2) (2020-03-20) + +### Bug Fixes + +- error formatting, [#1319](https://github.com/graphql/graphiql/issues/1319) ([#1381](https://github.com/graphql/graphiql/issues/1381)) ([16509a4](https://github.com/graphql/graphiql/commit/16509a4278d523a7f0a96c846cc0f370d29a0700)) + +### Features + +- **cli:** recommend matching commands ([#1420](https://github.com/graphql/graphiql/issues/1420)) ([0fbae82](https://github.com/graphql/graphiql/commit/0fbae828ced2e8b95016268805654cde8322b076)) +- **graphql-config:** add graphql config extensions ([#1118](https://github.com/graphql/graphiql/issues/1118)) ([2a77e47](https://github.com/graphql/graphiql/commit/2a77e47719ec9181a00183a08ffa11287b8fd2f5)) +- capture unknown commands making use of the in-house s… ([#1417](https://github.com/graphql/graphiql/issues/1417)) ([dd12a6b](https://github.com/graphql/graphiql/commit/dd12a6b903976ce8d35cf91d3c9606450f1c0990)) +- use new GraphQL Config ([#1342](https://github.com/graphql/graphiql/issues/1342)) ([e45838f](https://github.com/graphql/graphiql/commit/e45838f5ba579e05b20f1a147ce431478ffad9aa)) + +## [2.4.0-alpha.1](https://github.com/graphql/graphiql/compare/graphql-language-service@2.3.4...graphql-language-service@2.4.0-alpha.1) (2020-01-18) + +### Features + +- convert LSP Server to Typescript, remove watchman ([#1138](https://github.com/graphql/graphiql/issues/1138)) ([8e33dbb](https://github.com/graphql/graphiql/commit/8e33dbb)) + +## [2.3.4](https://github.com/graphql/graphiql/compare/graphql-language-service@2.3.3...graphql-language-service@2.3.4) (2019-12-09) + +**Note:** Version bump only for package graphql-language-service + +## [2.3.3](https://github.com/graphql/graphiql/compare/graphql-language-service@2.3.2...graphql-language-service@2.3.3) (2019-12-09) + +**Note:** Version bump only for package graphql-language-service + +## [2.3.2](https://github.com/graphql/graphiql/compare/graphql-language-service@2.3.1...graphql-language-service@2.3.2) (2019-12-03) + +**Note:** Version bump only for package graphql-language-service + +## [2.3.1](https://github.com/graphql/graphiql/compare/graphql-language-service@2.3.0...graphql-language-service@2.3.1) (2019-11-26) + +**Note:** Version bump only for package graphql-language-service + +# 2.3.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 2.0.1 (2019-05-14) + +# 2.0.0 (2018-09-18) + +## 1.2.2 (2018-06-11) + +## 1.1.2 (2018-04-19) + +## 1.1.1 (2018-04-18) + +# 1.1.0 (2018-04-09) + +## 1.0.18 (2018-01-04) + +## 1.0.16 (2017-11-21) + +## 1.0.15 (2017-10-02) + +## 0.1.14 (2017-09-29) + +## 0.1.13 (2017-08-24) + +## 0.1.12 (2017-08-21) + +## 0.1.11 (2017-08-20) + +## 0.1.10 (2017-08-19) + +## 0.1.9 (2017-08-18) + +## 0.1.8 (2017-08-18) + +## 0.1.7 (2017-08-16) + +## 0.1.6 (2017-08-15) + +## 0.1.5 (2017-08-14) + +## 0.1.5-0 (2017-08-10) + +## 0.1.4-0 (2017-08-10) + +## 0.1.3-0 (2017-08-10) + +## 0.1.2-0 (2017-08-10) + +## 0.1.1-0 (2017-08-10) + +# 0.1.0-0 (2017-08-10) + +# 2.2.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 2.0.1 (2019-05-14) + +# 2.0.0 (2018-09-18) + +## 1.2.2 (2018-06-11) + +## 1.1.2 (2018-04-19) + +## 1.1.1 (2018-04-18) + +# 1.1.0 (2018-04-09) + +## 1.0.18 (2018-01-04) + +## 1.0.16 (2017-11-21) + +## 1.0.15 (2017-10-02) + +## 0.1.14 (2017-09-29) + +## 0.1.13 (2017-08-24) + +## 0.1.12 (2017-08-21) + +## 0.1.11 (2017-08-20) + +## 0.1.10 (2017-08-19) + +## 0.1.9 (2017-08-18) + +## 0.1.8 (2017-08-18) + +## 0.1.7 (2017-08-16) + +## 0.1.6 (2017-08-15) + +## 0.1.5 (2017-08-14) + +## 0.1.5-0 (2017-08-10) + +## 0.1.4-0 (2017-08-10) + +## 0.1.3-0 (2017-08-10) + +## 0.1.2-0 (2017-08-10) + +## 0.1.1-0 (2017-08-10) + +# 0.1.0-0 (2017-08-10) + +# 2.2.0-alpha.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 2.0.1 (2019-05-14) + +# 2.0.0 (2018-09-18) + +## 1.2.2 (2018-06-11) + +## 1.1.2 (2018-04-19) + +## 1.1.1 (2018-04-18) + +# 1.1.0 (2018-04-09) + +## 1.0.18 (2018-01-04) + +## 1.0.16 (2017-11-21) + +## 1.0.15 (2017-10-02) + +## 0.1.14 (2017-09-29) + +## 0.1.13 (2017-08-24) + +## 0.1.12 (2017-08-21) + +## 0.1.11 (2017-08-20) + +## 0.1.10 (2017-08-19) + +## 0.1.9 (2017-08-18) + +## 0.1.8 (2017-08-18) + +## 0.1.7 (2017-08-16) + +## 0.1.6 (2017-08-15) + +## 0.1.5 (2017-08-14) + +## 0.1.5-0 (2017-08-10) + +## 0.1.4-0 (2017-08-10) + +## 0.1.3-0 (2017-08-10) + +## 0.1.2-0 (2017-08-10) + +## 0.1.1-0 (2017-08-10) + +# 0.1.0-0 (2017-08-10) + +## 2.1.1-alpha.1 (2019-09-01) + +## 2.0.1 (2019-05-14) + +# 2.0.0 (2018-09-18) + +## 1.2.2 (2018-06-11) + +## 1.1.2 (2018-04-19) + +## 1.1.1 (2018-04-18) + +# 1.1.0 (2018-04-09) + +## 1.0.18 (2018-01-04) + +## 1.0.16 (2017-11-21) + +## 1.0.15 (2017-10-02) + +## 0.1.14 (2017-09-29) + +## 0.1.13 (2017-08-24) + +## 0.1.12 (2017-08-21) + +## 0.1.11 (2017-08-20) + +## 0.1.10 (2017-08-19) + +## 0.1.9 (2017-08-18) + +## 0.1.8 (2017-08-18) + +## 0.1.7 (2017-08-16) + +## 0.1.6 (2017-08-15) + +## 0.1.5 (2017-08-14) + +## 0.1.5-0 (2017-08-10) + +## 0.1.4-0 (2017-08-10) + +## 0.1.3-0 (2017-08-10) + +## 0.1.2-0 (2017-08-10) + +## 0.1.1-0 (2017-08-10) + +# 0.1.0-0 (2017-08-10) + +**Note:** Version bump only for package graphql-language-service + +## 2.1.1-alpha.0 (2019-09-01) + +## 2.0.1 (2019-05-14) + +# 2.0.0 (2018-09-18) + +## 1.2.2 (2018-06-11) + +## 1.1.2 (2018-04-19) + +## 1.1.1 (2018-04-18) + +# 1.1.0 (2018-04-09) + +## 1.0.18 (2018-01-04) + +## 1.0.16 (2017-11-21) + +## 1.0.15 (2017-10-02) + +## 0.1.14 (2017-09-29) + +## 0.1.13 (2017-08-24) + +## 0.1.12 (2017-08-21) + +## 0.1.11 (2017-08-20) + +## 0.1.10 (2017-08-19) + +## 0.1.9 (2017-08-18) + +## 0.1.8 (2017-08-18) + +## 0.1.7 (2017-08-16) + +## 0.1.6 (2017-08-15) + +## 0.1.5 (2017-08-14) + +## 0.1.5-0 (2017-08-10) + +## 0.1.4-0 (2017-08-10) + +## 0.1.3-0 (2017-08-10) + +## 0.1.2-0 (2017-08-10) + +## 0.1.1-0 (2017-08-10) + +# 0.1.0-0 (2017-08-10) + +**Note:** Version bump only for package graphql-language-service + +## 2.1.1 (2019-09-01) + +## 2.0.1 (2019-05-14) + +# 2.0.0 (2018-09-18) + +## 1.2.2 (2018-06-11) + +## 1.1.2 (2018-04-19) + +## 1.1.1 (2018-04-18) + +# 1.1.0 (2018-04-09) + +## 1.0.18 (2018-01-04) + +## 1.0.16 (2017-11-21) + +## 1.0.15 (2017-10-02) + +## 0.1.14 (2017-09-29) + +## 0.1.13 (2017-08-24) + +## 0.1.12 (2017-08-21) + +## 0.1.11 (2017-08-20) + +## 0.1.10 (2017-08-19) + +## 0.1.9 (2017-08-18) + +## 0.1.8 (2017-08-18) + +## 0.1.7 (2017-08-16) + +## 0.1.6 (2017-08-15) + +## 0.1.5 (2017-08-14) + +## 0.1.5-0 (2017-08-10) + +## 0.1.4-0 (2017-08-10) + +## 0.1.3-0 (2017-08-10) + +## 0.1.2-0 (2017-08-10) + +## 0.1.1-0 (2017-08-10) + +# 0.1.0-0 (2017-08-10) + +**Note:** Version bump only for package graphql-language-service diff --git a/packages/graphql-language-service-cli/LICENSE b/packages/graphql-language-service-cli/LICENSE new file mode 100644 index 00000000000..7802f239a32 --- /dev/null +++ b/packages/graphql-language-service-cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 GraphQL Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/graphql-language-service-cli/README.md b/packages/graphql-language-service-cli/README.md new file mode 100644 index 00000000000..b36a7adbe01 --- /dev/null +++ b/packages/graphql-language-service-cli/README.md @@ -0,0 +1,149 @@ +# graphql-language-service-cli + +> Note: As of 3.0.0, this package has been renamed from +> `graphql-language-service` to `graphql-language-service-cli`. please now use +> the `graphql-lsp` bin, instead of the `graphql` binary. + +[![NPM](https://img.shields.io/npm/v/graphql-language-service-cli.svg)](https://npmjs.com/graphql-language-service-cli) +![npm downloads](https://img.shields.io/npm/dm/graphql-language-service-vli?label=npm%20downloads) +![Snyk Vulnerabilities for npm package](https://img.shields.io/snyk/vulnerabilities/npm/codemirror-graphql) +[![License](https://img.shields.io/npm/l/graphql-language-service.svg?style=flat-square)](LICENSE) + +_We welcome your feedback and suggestions._ + +GraphQL Language Service provides an interface for building GraphQL language +services for IDEs. + +Almost 100% for +[Microsoft's Language Server Protocol](https://github.com/Microsoft/language-server-protocol) +is in place + +Supported features include: + +- Diagnostics (GraphQL syntax linting/validations) (**spec-compliant**) +- Autocomplete suggestions (**spec-compliant**) +- Hyperlink to fragment definitions and named types (type, input, enum) + definitions (**spec-compliant**) +- Outline view support for queries and SDL +- Symbols support across the workspace + +see more information at +[`graphql-language-service-server`](https://npmjs.com/graphql-language-service-server) + +## Installation and Usage + +### Dependencies + +An LSP-compatible client with a file watcher that sends watch notifications to +the server. + +**DROPPED**: GraphQL Language Service no longer depends on +[Watchman](https://facebook.github.io/watchman/) + +Only node 9 or greater, and npm or yarn are required dependencies. + +### Installation + +with `yarn`: + +```sh +yarn global add graphql-language-service-cli +``` + +with `npm`: + +```sh +npm i -g graphql-language-service-cli +``` + +either will install the `graphql-lsp` bin globally + +### GraphQL configuration file (`.graphqlrc.yml`) + +Check out [graphql-config](https://graphql-config.com/docs) + +The custom graphql language configurations are: + +- `customDirectives` - `['@myExampleDirective']` +- `customValidationRules` - returns rules array with parameter + `ValidationContext` from `graphql/validation` + +### LSP Workspace Configuration + +When running `server`, your LSP-compatible client can +[provide additional workspace configuration](https://npmjs.com/graphql-language-service-server#workspace-configuration). + +For example, `coc.nvim` allows for providing custom `settings` + +```json +"languageserver": { + "graphql": { + "command": "graphql-lsp", + "args": ["server", "-m", "stream"], + // customize filetypes to your needs + "filetypes": ["typescript", "typescriptreact", "graphql"], + "settings": { + "graphql-config.load.legacy": true + } + } +} +``` + +this would allow for legacy `graphql-config` file formats like `.graphqlconfig`, +useful on projects maintaining compatibility with the intellij plugin + +### Using the command-line interface + +```sh +graphql-lsp server --schema=localhost:3000 +``` + +The node executable contains several commands: `server` and the command-line +language service methods (`validate`, `autocomplete`, `outline`). + +### CLI Options + +``` +Usage: graphql-lsp + +[-h | --help][-c | --configDir] {configDir} +[-t | --text] {textBuffer} +[-f | --file] {filePath} +[-s | --schema] {schemaPath} + +Options: + +-h, --help Show help [boolean] + +-t, --text Text buffer to perform GraphQL diagnostics on. +Will defer to --file option if omitted. +Overrides the --file option, if any. +[string] + +-f, --file File path to perform GraphQL diagnostics on. +Will be ignored if --text option is supplied. +[string] + +--row A row number from the cursor location for GraphQL +autocomplete suggestions. +If omitted, the last row number will be used. +[number] + +--column A column number from the cursor location for GraphQL +autocomplete suggestions. +If omitted, the last column number will be used. +[number] + +-c, --configDir Path to the .graphqlrc.yml configuration file. +Walks up the directory tree from the provided config +directory, or the current working directory, until a +.graphqlrc is found or the root directory is found. +[string] + +-s, --schemaPath a path to schema DSL file +[string] + +At least one command is required. +Commands: "server, validate, autocomplete, outline" + +``` diff --git a/packages/graphql-language-service-cli/bin/graphql.js b/packages/graphql-language-service-cli/bin/graphql.js new file mode 100755 index 00000000000..fe1a8014b50 --- /dev/null +++ b/packages/graphql-language-service-cli/bin/graphql.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node +/* + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the LICENSE file in + * the root directory of this source tree. + */ + +if (process?.env) { + process.env.GRAPHQL_NO_NAME_WARNING = true; +} + +require('@babel/polyfill'); +require('../dist/cli'); diff --git a/packages/graphql-language-service-cli/jest.config.js b/packages/graphql-language-service-cli/jest.config.js new file mode 100644 index 00000000000..342851e977e --- /dev/null +++ b/packages/graphql-language-service-cli/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config.base')(__dirname); diff --git a/packages/graphql-language-service-cli/package.json b/packages/graphql-language-service-cli/package.json new file mode 100644 index 00000000000..77c7d67e517 --- /dev/null +++ b/packages/graphql-language-service-cli/package.json @@ -0,0 +1,47 @@ +{ + "name": "graphql-language-service-cli", + "version": "3.3.25", + "description": "An interface for building GraphQL language services for IDEs", + "contributors": [ + "Hyohyeon Jeong ", + "Lee Byron (http://leebyron.com/)" + ], + "repository": { + "type": "git", + "url": "http://github.com/graphql/graphiql", + "directory": "packages/graphql-language-service-cli" + }, + "homepage": "https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service-cli#readme", + "bugs": { + "url": "https://github.com/graphql/graphiql/issues?q=issue+label:language-service-cli" + }, + "bin": { + "graphql-lsp": "./bin/graphql.js" + }, + "license": "MIT", + "files": [ + "bin", + "dist", + "src" + ], + "keywords": [ + "graphql", + "graphql-language-service", + "graphql-language-service-cli", + "language server", + "LSP" + ], + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + }, + "dependencies": { + "@types/yargs": "^16.0.5", + "@babel/polyfill": "^7.12.1", + "graphql-language-service": "^5.1.7", + "graphql-language-service-server": "^2.11.3", + "yargs": "^16.2.0" + }, + "devDependencies": { + "graphql": "^16.4.0" + } +} diff --git a/packages/graphql-language-service-cli/src/__tests__/client-test.ts b/packages/graphql-language-service-cli/src/__tests__/client-test.ts new file mode 100644 index 00000000000..734f920b82d --- /dev/null +++ b/packages/graphql-language-service-cli/src/__tests__/client-test.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + */ + +import main from '../client'; + +describe('process.stderr.write', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('is passed information on error of string type', () => { + const argv = { + schemaPath: '...', + text: 'foo', + }; + const mockStdErrWrite = jest + .spyOn(process.stderr, 'write') + .mockImplementation(); + jest.spyOn(process, 'exit').mockImplementation(); + const undefinedWithNewLine = /^undefined\n$/; + + main('autocomplete', argv); + expect(mockStdErrWrite).toHaveBeenLastCalledWith(expect.any(String)); + expect(mockStdErrWrite).toHaveBeenLastCalledWith( + expect.not.stringMatching(undefinedWithNewLine), + ); + + main('outline', argv); + expect(mockStdErrWrite).toHaveBeenLastCalledWith(expect.any(String)); + expect(mockStdErrWrite).toHaveBeenLastCalledWith( + expect.not.stringMatching(undefinedWithNewLine), + ); + + main('validate', argv); + expect(mockStdErrWrite).toHaveBeenLastCalledWith(expect.any(String)); + expect(mockStdErrWrite).toHaveBeenLastCalledWith( + expect.not.stringMatching(undefinedWithNewLine), + ); + }); +}); diff --git a/packages/graphql-language-service-cli/src/__tests__/index-test.ts b/packages/graphql-language-service-cli/src/__tests__/index-test.ts new file mode 100644 index 00000000000..9ea346b0fb6 --- /dev/null +++ b/packages/graphql-language-service-cli/src/__tests__/index-test.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + */ + +describe('blinking light demo', () => { + it('runs', () => { + // This is just a place holder for now as all the existing tests have moved + // down into the respective package directories. In the future, this will be + // the home of the integration tests. + expect(true).toEqual(true); + }); +}); diff --git a/packages/graphql-language-service-cli/src/cli.ts b/packages/graphql-language-service-cli/src/cli.ts new file mode 100644 index 00000000000..ee3f84741bc --- /dev/null +++ b/packages/graphql-language-service-cli/src/cli.ts @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import yargs from 'yargs'; +import client from './client'; + +import { startServer } from 'graphql-language-service-server'; + +const { argv } = yargs + .usage( + 'GraphQL Language Service Command-Line Interface.\n' + + 'Usage: graphql-lsp \n' + + ' [-h | --help]\n' + + ' [-c | --configDir] {configDir}\n' + + ' [-t | --text] {textBuffer}\n' + + ' [-f | --file] {filePath}\n' + + ' [-s | --schema] {schemaPath}\n' + + ' [-m | --method] {method}\n' + + ' [-p | --port] {port}\n' + + '\n At least one command is required.\n', + ) + .help('h') + .alias('h', 'help') + .strict() + .recommendCommands() + .demandCommand( + 1, + 'At least one command is required.\n' + + 'Commands: "server, validate, autocomplete, outline"\n', + ) + .command('server', 'GraphQL language server service') + .command('validate', 'Validates the query') + .command('autocomplete', 'Get autocomplete suggestions') + .command('outline', 'Get outline') + .option('t', { + alias: 'text', + describe: + 'Text buffer to perform GraphQL diagnostics on.\n' + + 'Will defer to --file option if omitted.\n' + + 'Overrides the --file option, if any.\n', + type: 'string', + }) + .option('f', { + alias: 'file', + describe: + 'File path to perform GraphQL diagnostics on.\n' + + 'Will be ignored if --text option is supplied.\n', + type: 'string', + }) + .option('row', { + describe: + 'A row number from the cursor location for ' + + 'GraphQL autocomplete suggestions.\n' + + 'If omitted, the last row number will be used.\n', + type: 'number', + }) + .option('column', { + describe: + 'A column number from the cursor location for ' + + 'GraphQL autocomplete suggestions.\n' + + 'If omitted, the last column number will be used.\n', + type: 'number', + }) + .option('c', { + alias: 'configDir', + describe: + 'Path to the .graphqlrc configuration file.\n' + + 'Walks up the directory tree from the provided config directory, or ' + + 'the current working directory, until a .graphqlrc is found or ' + + 'the root directory is found.\n', + type: 'string', + }) + .option('m', { + alias: 'method', + describe: + 'A IPC communication method between client and server.\n' + + 'Can be one of: stream, node, socket.\n' + + 'Will default to use a node IPC channel for communication.\n', + type: 'string', + default: 'node', + }) + .option('p', { + alias: 'port', + describe: + 'Port number to communicate via socket.\n' + + 'The port number of a service running inside the IDE that the language ' + + 'service should connect to.\n' + + 'Required if the client communicates via socket connection.\n', + type: 'number', + }) + .option('s', { + alias: 'schemaPath', + describe: 'a path to schema DSL file\n', + type: 'string', + }); + +const command = argv._.pop(); + +if (!command) { + process.stdout.write('no command supplied'); + process.exit(0); +} + +if (command === 'server') { + process.on('uncaughtException', error => { + process.stderr.write( + 'An error was thrown from GraphQL language service: ' + String(error), + ); + // don't exit at all if there is an uncaughtException + // process.exit(0); + }); + + const options: { [key: string]: any } = {}; + if (argv.port) { + options.port = argv.port; + } + if (argv.method) { + options.method = argv.method; + } + if (argv.configDir) { + options.configDir = argv.configDir; + } + // eslint-disable-next-line promise/prefer-await-to-then -- don't know if I can use top level await here + startServer(options).catch(error => { + process.stderr.write( + 'An error was thrown from GraphQL language service: ' + String(error), + ); + }); +} else { + client(command as string, argv as Record); +} + +// Exit the process when stream closes from remote end. +process.stdin.on('close', () => { + process.exit(0); +}); diff --git a/packages/graphql-language-service-cli/src/client.ts b/packages/graphql-language-service-cli/src/client.ts new file mode 100644 index 00000000000..bde89aa6eec --- /dev/null +++ b/packages/graphql-language-service-cli/src/client.ts @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { GraphQLSchema, buildSchema, buildClientSchema } from 'graphql'; + +import invariant from 'node:assert'; +import fs from 'node:fs'; +import { + getAutocompleteSuggestions, + getDiagnostics, + getOutline, + Position, +} from 'graphql-language-service'; + +import path from 'node:path'; + +import type { CompletionItem, Diagnostic } from 'graphql-language-service'; + +const GRAPHQL_SUCCESS_CODE = 0; +const GRAPHQL_FAILURE_CODE = 1; + +type EXIT_CODE = 0 | 1; + +/** + * Performs GraphQL language service features with provided arguments from + * the command-line interface. + * + * `autocomplete`: returns GraphQL autocomplete suggestions at the cursor + * location provided, or at the end of the query text. + * `outline`: returns GraphQL query outline information. + * `validate`: performs GraphQL query lint/validations and returns the results. + * Query validation is only performed if a schema path is supplied. + */ + +export default function main( + command: string, + argv: { [key: string]: string }, +): void { + const filePath = argv.file?.trim(); + invariant( + argv.text || argv.file, + 'A path to the GraphQL file or its contents is required.', + ); + + const text = ensureText(argv.text, filePath); + const schemaPath = argv.schemaPath?.trim(); + + let exitCode; + switch (command) { + case 'autocomplete': + const lines = text.split('\n'); + const row = parseInt(argv.row, 10) || lines.length - 1; + const column = parseInt(argv.column, 10) || lines.at(-1)!.length; + const point = new Position(row, column); + exitCode = _getAutocompleteSuggestions(text, point, schemaPath); + break; + case 'outline': + exitCode = _getOutline(text); + break; + case 'validate': + exitCode = _getDiagnostics(filePath, text, schemaPath); + break; + default: + throw new Error(`Unknown command '${command}'`); + } + + process.exit(exitCode); +} + +interface AutocompleteResultsMap { + [i: number]: CompletionItem; +} + +function formatUnknownError(error: unknown) { + let message: string | undefined; + if (error instanceof Error) { + message = error.stack; + } + return message ?? String(error); +} + +function _getAutocompleteSuggestions( + queryText: string, + point: Position, + schemaPath: string, +): EXIT_CODE { + invariant( + schemaPath, + 'A schema path is required to provide GraphQL autocompletion', + ); + + try { + const schema = schemaPath ? generateSchema(schemaPath) : null; + const resultArray = schema + ? getAutocompleteSuggestions(schema, queryText, point) + : []; + const resultObject: AutocompleteResultsMap = resultArray.reduce( + (prev: AutocompleteResultsMap, cur, index) => { + prev[index] = cur; + return prev; + }, + {}, + ); + process.stdout.write(JSON.stringify(resultObject, null, 2)); + return GRAPHQL_SUCCESS_CODE; + } catch (error) { + process.stderr.write(formatUnknownError(error) + '\n'); + return GRAPHQL_FAILURE_CODE; + } +} + +interface DiagnosticResultsMap { + [i: number]: Diagnostic; +} + +function _getDiagnostics( + _filePath: string, + queryText: string, + schemaPath?: string, +): EXIT_CODE { + try { + // `schema` is not strictly required as GraphQL diagnostics may still notify + // whether the query text is syntactically valid. + const schema = schemaPath ? generateSchema(schemaPath) : null; + const resultArray = getDiagnostics(queryText, schema); + const resultObject: DiagnosticResultsMap = resultArray.reduce( + (prev: DiagnosticResultsMap, cur, index) => { + prev[index] = cur; + return prev; + }, + {}, + ); + process.stdout.write(JSON.stringify(resultObject, null, 2)); + return GRAPHQL_SUCCESS_CODE; + } catch (error) { + process.stderr.write(formatUnknownError(error) + '\n'); + return GRAPHQL_FAILURE_CODE; + } +} + +function _getOutline(queryText: string): EXIT_CODE { + try { + const outline = getOutline(queryText); + if (outline) { + process.stdout.write(JSON.stringify(outline, null, 2)); + } else { + throw new Error('Error parsing or no outline tree found'); + } + } catch (error) { + process.stderr.write(formatUnknownError(error) + '\n'); + return GRAPHQL_FAILURE_CODE; + } + return GRAPHQL_SUCCESS_CODE; +} + +function ensureText(queryText: string, filePath: string): string { + let text = queryText; + // Always honor text argument over filePath. + // If text isn't available, try reading from the filePath. + if (!text) { + try { + text = fs.readFileSync(filePath, 'utf8'); + } catch (error) { + throw new Error(String(error)); + } + } + return text; +} + +function generateSchema(schemaPath: string): GraphQLSchema { + const schemaDSL = fs.readFileSync(schemaPath, 'utf8'); + const schemaFileExt = path.extname(schemaPath); + switch (schemaFileExt) { + case '.graphql': + return buildSchema(schemaDSL); + case '.json': + return buildClientSchema(JSON.parse(schemaDSL)); + default: + throw new Error('Unsupported schema file extension'); + } +} diff --git a/packages/graphql-language-service-cli/tsconfig.esm.json b/packages/graphql-language-service-cli/tsconfig.esm.json new file mode 100644 index 00000000000..7f3a870ebd8 --- /dev/null +++ b/packages/graphql-language-service-cli/tsconfig.esm.json @@ -0,0 +1,19 @@ +{ + "extends": "../../resources/tsconfig.base.esm.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./esm", + "composite": true, + "target": "ES2018" + }, + "references": [ + { + "path": "../graphql-language-service" + }, + { + "path": "../graphql-language-service-server" + } + ], + "include": ["src"], + "exclude": ["**/__tests__/**", "**/*.spec.*"] +} diff --git a/packages/graphql-language-service-cli/tsconfig.json b/packages/graphql-language-service-cli/tsconfig.json new file mode 100644 index 00000000000..ec7501b674c --- /dev/null +++ b/packages/graphql-language-service-cli/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../resources/tsconfig.base.cjs.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./dist", + "target": "ES2018" + }, + "references": [ + { + "path": "../graphql-language-service" + }, + { + "path": "../graphql-language-service-server" + } + ], + "include": ["src"], + "exclude": ["**/__tests__/**", "**/*.spec.*"] +} diff --git a/packages/graphql-language-service-server/.npmignore b/packages/graphql-language-service-server/.npmignore new file mode 100644 index 00000000000..1e433b22baa --- /dev/null +++ b/packages/graphql-language-service-server/.npmignore @@ -0,0 +1,3 @@ +node_modules +src +yarn.lock diff --git a/packages/graphql-language-service-server/CHANGELOG.md b/packages/graphql-language-service-server/CHANGELOG.md new file mode 100644 index 00000000000..d5195968f64 --- /dev/null +++ b/packages/graphql-language-service-server/CHANGELOG.md @@ -0,0 +1,809 @@ +# graphql-language-service-server + +## 2.11.3 + +### Patch Changes + +- [#3322](https://github.com/graphql/graphiql/pull/3322) [`6939bac4`](https://github.com/graphql/graphiql/commit/6939bac4a9a849fe497260fd0702bdd95eefd943) Thanks [@acao](https://github.com/acao)! - Bypass babel typescript parsing errors to continue extracting graphql strings + +## 2.11.2 + +### Patch Changes + +- [#3224](https://github.com/graphql/graphiql/pull/3224) [`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9) Thanks [@acao](https://github.com/acao)! - try removing some packages from pre.json + +- [#3216](https://github.com/graphql/graphiql/pull/3216) [`55135804`](https://github.com/graphql/graphiql/commit/551358045611a27551e5654c2b115295c35639d8) Thanks [@simowe](https://github.com/simowe)! - fix: reload schema when a change to the schema file is detected + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5)]: + - graphql-language-service@5.1.7 + +## 2.11.2-alpha.0 + +### Patch Changes + +- [#3224](https://github.com/graphql/graphiql/pull/3224) [`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9) Thanks [@acao](https://github.com/acao)! - try removing some packages from pre.json + +- [#3216](https://github.com/graphql/graphiql/pull/3216) [`55135804`](https://github.com/graphql/graphiql/commit/551358045611a27551e5654c2b115295c35639d8) Thanks [@simowe](https://github.com/simowe)! - fix: reload schema when a change to the schema file is detected + +- Updated dependencies [[`5971d528`](https://github.com/graphql/graphiql/commit/5971d528b0608e76d9d109103f64857a790a99b9), [`d9e5089f`](https://github.com/graphql/graphiql/commit/d9e5089f78f85cd50c3e3e3ba8510f7dda3d06f5)]: + - graphql-language-service@5.1.7-alpha.0 + +## 2.11.1 + +### Patch Changes + +- [#3143](https://github.com/graphql/graphiql/pull/3143) [`4c3a08b1`](https://github.com/graphql/graphiql/commit/4c3a08b1a99e0933362a1c93340b613730c90aa4) Thanks [@B2o5T](https://github.com/B2o5T)! - [ESLint] enable `sonar/prefer-promise-shorthand` and `sonar/no-dead-store` rules + +## 2.11.0 + +### Minor Changes + +- [#3148](https://github.com/graphql/graphiql/pull/3148) [`06007498`](https://github.com/graphql/graphiql/commit/06007498880528ed75dd4d705dcbcd7c9e775939) Thanks [@mskelton](https://github.com/mskelton)! - Use native LSP logger instead of manual file based logging. This fixes errors in Neovim when using the GraphQL LSP. + +### Patch Changes + +- [#3135](https://github.com/graphql/graphiql/pull/3135) [`28b1b5a0`](https://github.com/graphql/graphiql/commit/28b1b5a016787ec4119d28f057a9d93814d4e310) Thanks [@KammererTob](https://github.com/KammererTob)! - fixed wrong script tag offset for vue-sfc + +- Updated dependencies [[`06007498`](https://github.com/graphql/graphiql/commit/06007498880528ed75dd4d705dcbcd7c9e775939)]: + - graphql-language-service@5.1.6 + +## 2.10.0 + +### Minor Changes + +- [#3163](https://github.com/graphql/graphiql/pull/3163) [`f2040452`](https://github.com/graphql/graphiql/commit/f20404529677635f5d4792b328aa648641bf8d9c) Thanks [@AaronMoat](https://github.com/AaronMoat)! - Fix GraphQLCache to read both documents and schema + +## 2.9.10 + +### Patch Changes + +- [#3150](https://github.com/graphql/graphiql/pull/3150) [`4d33b221`](https://github.com/graphql/graphiql/commit/4d33b2214e941f171385a1b72a1fa995714bb284) Thanks [@AaronMoat](https://github.com/AaronMoat)! - fix(graphql-language-service-server): allow getDefinition to work for unions + + Fixes the issue where a schema like the one below won't allow you to click through to X. + + ```graphql + union X = A | B + type A { + x: String + } + type B { + x: String + } + type Query { + a: X + } + ``` + +- Updated dependencies [[`4d33b221`](https://github.com/graphql/graphiql/commit/4d33b2214e941f171385a1b72a1fa995714bb284)]: + - graphql-language-service@5.1.5 + +## 2.9.9 + +### Patch Changes + +- [#3154](https://github.com/graphql/graphiql/pull/3154) [`632a7c6b`](https://github.com/graphql/graphiql/commit/632a7c6bb2959ef5d59236aeab218587578466e7) Thanks [@scamden](https://github.com/scamden)! - allow caching for multiple projects in graphql config + +## 2.9.8 + +### Patch Changes + +- [#3113](https://github.com/graphql/graphiql/pull/3113) [`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9) Thanks [@B2o5T](https://github.com/B2o5T)! - replace `.forEach` with `for..of` + +- [#3157](https://github.com/graphql/graphiql/pull/3157) [`06d39823`](https://github.com/graphql/graphiql/commit/06d39823e093c8441fea469446c25f18a664e778) Thanks [@jycouet](https://github.com/jycouet)! - fix: `.vue` and `.svelte` files doesn't log errors anymore when parsing with no script tag (#2836) + +- [#3109](https://github.com/graphql/graphiql/pull/3109) [`51007002`](https://github.com/graphql/graphiql/commit/510070028b7d8e98f2ba25f396519976aea5fa4b) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-floating-promises` eslint rule + +- [#3120](https://github.com/graphql/graphiql/pull/3120) [`15c26eb6`](https://github.com/graphql/graphiql/commit/15c26eb6d621a85df9eecb2b8a5fa009fa2fe040) Thanks [@B2o5T](https://github.com/B2o5T)! - prefer await to then + +- Updated dependencies [[`2e477eb2`](https://github.com/graphql/graphiql/commit/2e477eb24672a242ae4a4f2dfaeaf41152ed7ee9)]: + - graphql-language-service@5.1.4 + +## 2.9.7 + +### Patch Changes + +- [#3088](https://github.com/graphql/graphiql/pull/3088) [`9d9478ae`](https://github.com/graphql/graphiql/commit/9d9478aea7536d2957e4371cef4f30577db2113d) Thanks [@B2o5T](https://github.com/B2o5T)! - remove nowhere used `node-fetch` dependency + +- [#3046](https://github.com/graphql/graphiql/pull/3046) [`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d) Thanks [@B2o5T](https://github.com/B2o5T)! - Prefer .at() method for index access + +- Updated dependencies [[`b9c13328`](https://github.com/graphql/graphiql/commit/b9c13328f3d28c0026ee0f0ecc7213065c9b016d), [`881a2024`](https://github.com/graphql/graphiql/commit/881a202497d5a58eb5260a5aa54c0c88930d69a0)]: + - graphql-language-service@5.1.3 + +## 2.9.6 + +### Patch Changes + +- [#2993](https://github.com/graphql/graphiql/pull/2993) [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4) Thanks [@B2o5T](https://github.com/B2o5T)! - add `unicorn/consistent-destructuring` rule + +- [#2962](https://github.com/graphql/graphiql/pull/2962) [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d) Thanks [@B2o5T](https://github.com/B2o5T)! - clean all ESLint warnings, add `--max-warnings=0` and `--cache` flags + +- [#3051](https://github.com/graphql/graphiql/pull/3051) [`90350022`](https://github.com/graphql/graphiql/commit/90350022334d9fcce0f4b72b3b0f7a12d21f78f9) Thanks [@B2o5T](https://github.com/B2o5T)! - update babel, support `satisfies` operator + +- [#2940](https://github.com/graphql/graphiql/pull/2940) [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-node-protocol` rule + +- Updated dependencies [[`e68cb8bc`](https://github.com/graphql/graphiql/commit/e68cb8bcaf9baddf6fca747abab871ecd1bc7a4c), [`f788e65a`](https://github.com/graphql/graphiql/commit/f788e65aff267ec873237034831d1fd936222a9b), [`bdc966cb`](https://github.com/graphql/graphiql/commit/bdc966cba6134a72ff7fe40f76543c77ba15d4a4), [`db2a0982`](https://github.com/graphql/graphiql/commit/db2a0982a17134f0069483ab283594eb64735b7d), [`8725d1b6`](https://github.com/graphql/graphiql/commit/8725d1b6b686139286cf05dec6a84d89942128ba)]: + - graphql-language-service@5.1.2 + +## 2.9.5 + +### Patch Changes + +- [#2931](https://github.com/graphql/graphiql/pull/2931) [`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `no-negated-condition` and `no-else-return` rules + +- [#2922](https://github.com/graphql/graphiql/pull/2922) [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b) Thanks [@B2o5T](https://github.com/B2o5T)! - extends `plugin:import/recommended` and fix warnings + +- [#2966](https://github.com/graphql/graphiql/pull/2966) [`f9aa87dc`](https://github.com/graphql/graphiql/commit/f9aa87dc6a88ed8a8a0a94de520c7a41fff8ffde) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `sonarjs/no-small-switch` and `sonarjs/no-duplicated-branches` rules + +- [#2926](https://github.com/graphql/graphiql/pull/2926) [`10e97bbe`](https://github.com/graphql/graphiql/commit/10e97bbe6c9ff81bae73b11ba81ac2b69eca2772) Thanks [@elijaholmos](https://github.com/elijaholmos)! - support cts and mts file extensions + +- [#2937](https://github.com/graphql/graphiql/pull/2937) [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-includes` + +- [#2933](https://github.com/graphql/graphiql/pull/2933) [`d502a33b`](https://github.com/graphql/graphiql/commit/d502a33b4332f1025e947c02d7cfdc5799365c8d) Thanks [@B2o5T](https://github.com/B2o5T)! - enable @typescript-eslint/no-unused-expressions + +- [#2965](https://github.com/graphql/graphiql/pull/2965) [`0669767e`](https://github.com/graphql/graphiql/commit/0669767e1e2196a78cbefe3679a52bcbb341e913) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `unicorn/prefer-optional-catch-binding` rule + +- [#2963](https://github.com/graphql/graphiql/pull/2963) [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `prefer-destructuring` rule + +- [#2942](https://github.com/graphql/graphiql/pull/2942) [`4ff2794c`](https://github.com/graphql/graphiql/commit/4ff2794c8b6032168e27252096cb276ce712878e) Thanks [@B2o5T](https://github.com/B2o5T)! - enable `sonarjs/no-redundant-jump` rule + +- Updated dependencies [[`f7addb20`](https://github.com/graphql/graphiql/commit/f7addb20c4a558fbfb4112c8ff095bbc8f9d9147), [`d1fcad72`](https://github.com/graphql/graphiql/commit/d1fcad72607e2789517dfe4936b5ec604e46762b), [`4a8b2e17`](https://github.com/graphql/graphiql/commit/4a8b2e1766a38eb4828cf9a81bf9d767070041de), [`c70d9165`](https://github.com/graphql/graphiql/commit/c70d9165cc1ef8eb1cd0d6b506ced98c626597f9), [`c44ea4f1`](https://github.com/graphql/graphiql/commit/c44ea4f1917b97daac815c08299b934c8ca57ed9), [`0669767e`](https://github.com/graphql/graphiql/commit/0669767e1e2196a78cbefe3679a52bcbb341e913), [`18f8e80a`](https://github.com/graphql/graphiql/commit/18f8e80ae12edfd0c36adcb300cf9e06ac27ea49), [`f263f778`](https://github.com/graphql/graphiql/commit/f263f778cb95b9f413bd09ca56a43f5b9c2f6215), [`6a9d913f`](https://github.com/graphql/graphiql/commit/6a9d913f0d1b847124286b3fa1f3a2649d315171)]: + - graphql-language-service@5.1.1 + +## 2.9.4 + +### Patch Changes + +- [#2901](https://github.com/graphql/graphiql/pull/2901) [`eff4fd6b`](https://github.com/graphql/graphiql/commit/eff4fd6b9087c2d9cdb260ee2502a31d23769c3f) Thanks [@acao](https://github.com/acao)! - Reload the language service when a legacy format .graphqlconfig file has changed + +## 2.9.3 + +### Patch Changes + +- [#2900](https://github.com/graphql/graphiql/pull/2900) [`8989ffce`](https://github.com/graphql/graphiql/commit/8989ffce7d6beca874e70f5a1ff066102580173a) Thanks [@acao](https://github.com/acao)! - use decorators-legacy @babel/parser plugin so that all styles of decorator usage are supported + +## 2.9.2 + +### Patch Changes + +- [#2861](https://github.com/graphql/graphiql/pull/2861) [`bdd1bd04`](https://github.com/graphql/graphiql/commit/bdd1bd045fc6610ccaae4745b8ecc10004594274) Thanks [@aloker](https://github.com/aloker)! - add missing pieces for svelte language support + +* [#2488](https://github.com/graphql/graphiql/pull/2488) [`967006a6`](https://github.com/graphql/graphiql/commit/967006a68e56f8f3a605c69fee5f920afdb6d8cf) Thanks [@acao](https://github.com/acao)! - Disable`fillLeafsOnComplete` by default + + Users found this generally annoying by default, especially when there are required arguments + + Without automatically prompting autocompletion of required arguments as well as lead expansion, it makes the extension harder to use + + You can now supply this in your graphql config: + + `config.extensions.languageService.fillLeafsOnComplete` + + Setting it to to `true` will enable this feature. Will soon add the ability to manually enable this in `monaco-graphql` as well. + + For both, this kind of behavior would be better as a keyboard command, context menu item &/or codelens prompt + +## 2.9.1 + +### Patch Changes + +- [#2829](https://github.com/graphql/graphiql/pull/2829) [`c835ca87`](https://github.com/graphql/graphiql/commit/c835ca87e93e00713fbbbb2f4448db03f6b97b10) Thanks [@acao](https://github.com/acao)! - major bugfixes with `onDidChange` and `onDidChangeWatchedFiles` events + +* [#2829](https://github.com/graphql/graphiql/pull/2829) [`c835ca87`](https://github.com/graphql/graphiql/commit/c835ca87e93e00713fbbbb2f4448db03f6b97b10) Thanks [@acao](https://github.com/acao)! - svelte language support, using the vue sfc parser introduced for vue support + +## 2.9.0 + +### Minor Changes + +- [#2827](https://github.com/graphql/graphiql/pull/2827) [`b422003c`](https://github.com/graphql/graphiql/commit/b422003c2403072e96d14f920a3f0f1dc1f4f708) Thanks [@acao](https://github.com/acao)! - Introducing vue.js support for intellisense! Thanks @AumyF + +## 2.8.9 + +### Patch Changes + +- [#2818](https://github.com/graphql/graphiql/pull/2818) [`929152f8`](https://github.com/graphql/graphiql/commit/929152f8ea076ffa3bf34b83445473331c3bdb67) Thanks [@acao](https://github.com/acao)! - Workspaces support introduced a regression for no-config scenario. Reverting to fix bugs with no graphql config crashing the server. + +## 2.8.8 + +### Patch Changes + +- [#2812](https://github.com/graphql/graphiql/pull/2812) [`cf2e3061`](https://github.com/graphql/graphiql/commit/cf2e3061f67ef5cf6b890e217d20915d0eaec1bd) Thanks [@acao](https://github.com/acao)! - fix a bundling bug for vscode, rolling back graphql-config upgrade + +## 2.8.7 + +### Patch Changes + +- [#2810](https://github.com/graphql/graphiql/pull/2810) [`f688422e`](https://github.com/graphql/graphiql/commit/f688422ed87ddd411cf3552fa6d9a5a367cd8662) Thanks [@acao](https://github.com/acao)! - fix graphql exec extension, upgrade `graphql-config`, fix issue with graphql-config cosmiconfig typescript config loader. + +## 2.8.6 + +### Patch Changes + +- [#2808](https://github.com/graphql/graphiql/pull/2808) [`a2071504`](https://github.com/graphql/graphiql/commit/a20715046fe7684bb9b17fbc9f5637b44e5210d6) Thanks [@acao](https://github.com/acao)! - fix graphql config init bug + +## 2.8.5 + +### Patch Changes + +- [#2616](https://github.com/graphql/graphiql/pull/2616) [`b0d7f06c`](https://github.com/graphql/graphiql/commit/b0d7f06cf9ec6fd6b1dcb61dd0273e37dd546ed5) Thanks [@acao](https://github.com/acao)! - support vscode multi-root workspaces! creates an LSP server instance for each workspace. + + WARNING: large-scale vscode workspaces usage, and this in tandem with `graphql.config.*` multi-project configs could lead to excessive system resource usage. Optimizations coming soon. + +## 2.8.4 + +### Patch Changes + +- Updated dependencies [[`d6ff4d7a`](https://github.com/graphql/graphiql/commit/d6ff4d7a5d535a0c43fe5914016bac9ef0c2b782)]: + - graphql-language-service@5.1.0 + +## 2.8.3 + +### Patch Changes + +- [#2664](https://github.com/graphql/graphiql/pull/2664) [`721425b3`](https://github.com/graphql/graphiql/commit/721425b3382e68dd4c7b883473e3eda38a9816ee) Thanks [@acao](https://github.com/acao)! - This reverts the bugfix for .graphqlrc.ts users, which broke the extension for schema url users + +## 2.8.2 + +### Patch Changes + +- [#2660](https://github.com/graphql/graphiql/pull/2660) [`34d31fbc`](https://github.com/graphql/graphiql/commit/34d31fbce6c49c929b48bdf1a6b0cebc33d8bbbf) Thanks [@acao](https://github.com/acao)! - bump `ts-node` to 10.x, so that TypeScript based configs (i.e. `.graphqlrc.ts`) will continue to work. It also bumps to the latest patch releases of `graphql-config` fixed several issues with TypeScript loading ([v4.3.2](https://github.com/kamilkisiela/graphql-config/releases/tag/v4.3.2), [v4.3.3](https://github.com/kamilkisiela/graphql-config/releases/tag/v4.3.3)). We tested manually, but please open a bug if you encounter any with schema-as-url configs & schema introspection. + +## 2.8.1 + +### Patch Changes + +- [#2623](https://github.com/graphql/graphiql/pull/2623) [`12cf4db0`](https://github.com/graphql/graphiql/commit/12cf4db006d1c058460bc04f51d8743fe1ac63bb) Thanks [@acao](https://github.com/acao)! - In #2624, fix introspection schema fetching regression in lsp server, and fix for users writing new .gql/.graphql files + +## 2.8.0 + +### Minor Changes + +- [#2557](https://github.com/graphql/graphiql/pull/2557) [`3304606d`](https://github.com/graphql/graphiql/commit/3304606d5130a745cbdab0e6c9182e75101ddde9) Thanks [@acao](https://github.com/acao)! - upgrades the `vscode-languageserver` and `vscode-jsonrpc` reference implementations for the lsp server to the latest. also upgrades `vscode-languageclient` in `vscode-graphql` to the latest 8.0.1. seems to work fine for IPC in `vscode-graphql` at least! + + hopefully this solves #2230 once and for all! + +## 2.7.29 + +### Patch Changes + +- [#2553](https://github.com/graphql/graphiql/pull/2553) [`edc1c964`](https://github.com/graphql/graphiql/commit/edc1c96477cc2fbc2b6ac5d6195b8f9766a8c5d4) Thanks [@acao](https://github.com/acao)! - Fix error with LSP crash for CLI users #2230. `vscode-graphql` not impacted - rather, `nvim.coc`, maybe other clients who use CLI directly). recreation of #2546 by [@xuanduc987](https://github.com/xuanduc987, thank you!) + +## 2.7.28 + +### Patch Changes + +- [#2519](https://github.com/graphql/graphiql/pull/2519) [`de5d5a07`](https://github.com/graphql/graphiql/commit/de5d5a07891fd49241a5abbb17eaf377a015a0a8) Thanks [@acao](https://github.com/acao)! - enable graphql-config legacy mode by default in the LSP server + +* [#2509](https://github.com/graphql/graphiql/pull/2509) [`737d4184`](https://github.com/graphql/graphiql/commit/737d4184f3af1d8fe9d64eb1b7e23dfcfbe640ea) Thanks [@Chnapy](https://github.com/Chnapy)! - Add `gql(``)`, `graphql(``)` call expressions support for highlighting & language + +## 2.7.27 + +### Patch Changes + +- [#2506](https://github.com/graphql/graphiql/pull/2506) [`cccefa70`](https://github.com/graphql/graphiql/commit/cccefa70c0466d60e8496e1df61aeb1490af723c) Thanks [@acao](https://github.com/acao)! - Remove redundant check, trigger LSP release + +- Updated dependencies [[`cccefa70`](https://github.com/graphql/graphiql/commit/cccefa70c0466d60e8496e1df61aeb1490af723c)]: + - graphql-language-service@5.0.6 + +## 2.7.26 + +### Patch Changes + +- [#2486](https://github.com/graphql/graphiql/pull/2486) [`c9c51b8a`](https://github.com/graphql/graphiql/commit/c9c51b8a98e1f0427272d3e9ad60989b32f1a1aa) Thanks [@stonexer](https://github.com/stonexer)! - definition support for operation fields ✨ + + you can now jump to the applicable object type definition for query/mutation/subscription fields! + +- Updated dependencies [[`c9c51b8a`](https://github.com/graphql/graphiql/commit/c9c51b8a98e1f0427272d3e9ad60989b32f1a1aa)]: + - graphql-language-service@5.0.5 + +## 2.7.25 + +### Patch Changes + +- [#2481](https://github.com/graphql/graphiql/pull/2481) [`cf092f59`](https://github.com/graphql/graphiql/commit/cf092f5960eae250bb193b9011b2fb883f797a99) Thanks [@acao](https://github.com/acao)! - No longer load dotenv in the LSP server + +## 2.7.24 + +### Patch Changes + +- [#2470](https://github.com/graphql/graphiql/pull/2470) [`d0017a93`](https://github.com/graphql/graphiql/commit/d0017a93b818cf3119e51c2b6c4a19004f98e29b) Thanks [@acao](https://github.com/acao)! - Aims to resolve #2421 + + - graphql config errors only log to output channel, no longer crash the LSP + - more performant LSP request no-ops for failing/missing config + + this used to fail silently in the output channel, but vscode introduced a new retry and notification for this + + would like to provide more helpful graphql config DX in the future but this should be better for now + +## 2.7.23 + +### Patch Changes + +- [#2417](https://github.com/graphql/graphiql/pull/2417) [`6ca6a92d`](https://github.com/graphql/graphiql/commit/6ca6a92d0fd12af974683de9706c8e8e06c751c2) Thanks [@acao](https://github.com/acao)! - fix annoying trigger character on newline issue #2182 + +## 2.7.22 + +### Patch Changes + +- [#2385](https://github.com/graphql/graphiql/pull/2385) [`6db28447`](https://github.com/graphql/graphiql/commit/6db284479a14873fea3e359efd71be0b15ab3ee8) Thanks [@acao](https://github.com/acao)! - Stop reporting unnecessary EOF errors when authoring new queries + +* [#2382](https://github.com/graphql/graphiql/pull/2382) [`1bea864d`](https://github.com/graphql/graphiql/commit/1bea864d05dee04bb20c06dc3c3d68675b87a50a) Thanks [@acao](https://github.com/acao)! - allow disabling query/SDL validation with `graphql-config` setting `{ extensions: { languageService: { enableValidation: false } } }`. + + Currently, users receive duplicate validation messages when using our LSP alongside existing validation tools like `graphql-eslint`, and this allows them to disable the LSP feature in that case. + +## 2.7.21 + +### Patch Changes + +- [#2378](https://github.com/graphql/graphiql/pull/2378) [`d22f6111`](https://github.com/graphql/graphiql/commit/d22f6111a60af25727d8dbc1058c79607df76af2) Thanks [@acao](https://github.com/acao)! - Trap all graphql parsing exceptions from (relatively) newly added logic. This should clear up bugs that have been plaguing users for two years now, sorry! + +- Updated dependencies [[`d22f6111`](https://github.com/graphql/graphiql/commit/d22f6111a60af25727d8dbc1058c79607df76af2)]: + - graphql-language-service@5.0.4 + +## 2.7.20 + +### Patch Changes + +- Updated dependencies [[`45cbc759`](https://github.com/graphql/graphiql/commit/45cbc759c732999e8b1eb4714d6047ab77c17902)]: + - graphql-language-service@5.0.3 + +## 2.7.19 + +### Patch Changes + +- [`c36504a8`](https://github.com/graphql/graphiql/commit/c36504a804d8cc54a5136340152999b4a1a2c69f) Thanks [@acao](https://github.com/acao)! - - upgrade `graphql-config` to latest in server + - remove `graphql-config` dependency from `vscode-graphql` and `graphql-language-service` + - fix `vscode-graphql` esbuild bundling bug in `vscode-graphql` [#2269](https://github.com/graphql/graphiql/issues/2269) by fixing `esbuild` version +- Updated dependencies [[`c36504a8`](https://github.com/graphql/graphiql/commit/c36504a804d8cc54a5136340152999b4a1a2c69f)]: + - graphql-language-service@5.0.2 + +## 2.7.18 + +### Patch Changes + +- [#2271](https://github.com/graphql/graphiql/pull/2271) [`e15d1dae`](https://github.com/graphql/graphiql/commit/e15d1dae399a7d43d8d98f2ce431a9a1f0ba84ae) Thanks [@acao](https://github.com/acao)! - a few bugfixes related to config handling impacting vim and potentially other LSP server users + +## 2.7.17 + +### Patch Changes + +- Updated dependencies [[`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa), [`3626f8d5`](https://github.com/graphql/graphiql/commit/3626f8d5012ee77a39e984ae347396cb00fcc6fa)]: + - graphql-language-service@5.0.1 + +## 2.7.16 + +### Patch Changes + +- Updated dependencies [[`2502a364`](https://github.com/graphql/graphiql/commit/2502a364b74dc754d92baa1579b536cf42139958)]: + - graphql-language-service@5.0.0 + +## 2.7.15 + +### Patch Changes + +- [#2214](https://github.com/graphql/graphiql/pull/2214) [`ab83198f`](https://github.com/graphql/graphiql/commit/ab83198fa8b3c5453d3733982ee9ca8a2d6bca7a) Thanks [@Cellule](https://github.com/Cellule)! - Fixed Windows fileUri when resolving type definition location + +## 2.7.14 + +### Patch Changes + +- [#2161](https://github.com/graphql/graphiql/pull/2161) [`484c0523`](https://github.com/graphql/graphiql/commit/484c0523cdd529f9e261d61a38616b6745075c7f) Thanks [@orta](https://github.com/orta)! - Do not log errors when a JS/TS file has no embedded graphql tags + +* [#2176](https://github.com/graphql/graphiql/pull/2176) [`5852ba47`](https://github.com/graphql/graphiql/commit/5852ba47c720a2577817aed512bef9a262254f2c) Thanks [@orta](https://github.com/orta)! - Update babel parser in the graphql language server + +- [#2175](https://github.com/graphql/graphiql/pull/2175) [`48c5df65`](https://github.com/graphql/graphiql/commit/48c5df654e323cee3b8c57d7414247465235d1b5) Thanks [@orta](https://github.com/orta)! - Better handling of unparsable babel JS/TS files + +- Updated dependencies [[`484c0523`](https://github.com/graphql/graphiql/commit/484c0523cdd529f9e261d61a38616b6745075c7f), [`5852ba47`](https://github.com/graphql/graphiql/commit/5852ba47c720a2577817aed512bef9a262254f2c), [`48c5df65`](https://github.com/graphql/graphiql/commit/48c5df654e323cee3b8c57d7414247465235d1b5)]: + - graphql-language-service@4.1.5 + +## 2.7.13 + +### Patch Changes + +- [#2111](https://github.com/graphql/graphiql/pull/2111) [`08ff6dce`](https://github.com/graphql/graphiql/commit/08ff6dce0625f7ab58a45364aed9ca04c7862fa7) Thanks [@acao](https://github.com/acao)! - Support template literals and tagged template literals with replacement expressions + +- Updated dependencies []: + - graphql-language-service@4.1.4 + +## 2.7.12 + +### Patch Changes + +- Updated dependencies [[`a44772d6`](https://github.com/graphql/graphiql/commit/a44772d6af97254c4f159ea7237e842a3e3719e8)]: + - graphql-language-service@4.1.3 + +## 2.7.11 + +### Patch Changes + +- Updated dependencies [[`e20760fb`](https://github.com/graphql/graphiql/commit/e20760fbd95c13d6d549cba3faa15a59aee9a2c0)]: + - graphql-language-service@4.1.2 + +## 2.7.10 + +### Patch Changes + +- [#2091](https://github.com/graphql/graphiql/pull/2091) [`ff9cebe5`](https://github.com/graphql/graphiql/commit/ff9cebe515a3539f85b9479954ae644dfeb68b63) Thanks [@acao](https://github.com/acao)! - Fix graphql 15 related issues. Should now build & test interchangeably. + +- Updated dependencies [[`ff9cebe5`](https://github.com/graphql/graphiql/commit/ff9cebe515a3539f85b9479954ae644dfeb68b63)]: + - graphql-language-service-utils@2.7.1 + - graphql-language-service@4.1.1 + +## 2.7.9 + +### Patch Changes + +- Updated dependencies [[`0f1f90ce`](https://github.com/graphql/graphiql/commit/0f1f90ce8f4a25ddebdaf7a9ddbe136214aa64a3)]: + - graphql-language-service@4.1.0 + +## 2.7.8 + +### Patch Changes + +- Updated dependencies [[`9df315b4`](https://github.com/graphql/graphiql/commit/9df315b44896efa313ed6744445fc8f9e702ebc3)]: + - graphql-language-service-utils@2.7.0 + - graphql-language-service@4.0.0 + +## 2.7.7 + +### Patch Changes + +- [`c4236190`](https://github.com/graphql/graphiql/commit/c4236190f91adedaf4f4a54cd0400a6b42c3c407) [#2072](https://github.com/graphql/graphiql/pull/2072) Thanks [@acao](https://github.com/acao)! - this fixes the parsing of file URIs by `graphql-language-service-server` in cases such as: + + - windows without WSL + - special characters in filenames + - likely other cases + + previously we were using the old approach of `URL(uri).pathname` which was not working! now using the standard `vscode-uri` approach of `URI.parse(uri).fsName`. + + this should fix issues with object and fragment type completion as well I think + + also for #2066 made it so that graphql config is not loaded into the file cache unnecessarily, and that it's only loaded on editor save events rather than on file changed events + + fixes #1644 and #2066 + +* [`df57cd25`](https://github.com/graphql/graphiql/commit/df57cd2556302d6aa5dd140e7bee3f7bdab4deb1) [#2065](https://github.com/graphql/graphiql/pull/2065) Thanks [@acao](https://github.com/acao)! - Add an opt-in feature to generate markdown in hover elements, starting with highlighting type information. Enabled for the language server and also the language service and thus `monaco-graphql` as well. + +* Updated dependencies [[`df57cd25`](https://github.com/graphql/graphiql/commit/df57cd2556302d6aa5dd140e7bee3f7bdab4deb1)]: + - graphql-language-service@3.2.5 + +## 2.7.6 + +### Patch Changes + +- [`4286185c`](https://github.com/graphql/graphiql/commit/4286185cdc6119175e23d66b8e177ba32693a63a) [#2060](https://github.com/graphql/graphiql/pull/2060) Thanks [@acao](https://github.com/acao)! - Parse more JS extensions in the language server + +## 2.7.5 + +### Patch Changes + +- [`f82bd7a9`](https://github.com/graphql/graphiql/commit/f82bd7a931eb5fa9a33e59d417303706844c9063) [#2055](https://github.com/graphql/graphiql/pull/2055) Thanks [@acao](https://github.com/acao)! - this fixes the URI scheme related bugs and make sure schema as sdl config works again. + + `fileURLToPath` had been introduced by a contributor and I didn't test properly, it broke sdl file loading! + + definitions, autocomplete, diagnostics, etc should work again also hides the more verbose logging output for now + +- Updated dependencies []: + - graphql-language-service@3.2.4 + - graphql-language-service-utils@2.6.3 + +## 2.7.4 + +### Patch Changes + +- [`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf) [#2047](https://github.com/graphql/graphiql/pull/2047) Thanks [@willstott101](https://github.com/willstott101)! - Source code included in all packages to fix source maps. codemirror-graphql includes esm build in package. + +- Updated dependencies [[`bdd57312`](https://github.com/graphql/graphiql/commit/bdd573129844168749aba0aaa20e31b9da81aacf)]: + - graphql-language-service@3.2.3 + - graphql-language-service-utils@2.6.2 + +## 2.7.3 + +### Patch Changes + +- [`858907d2`](https://github.com/graphql/graphiql/commit/858907d2106742a65ec52eb017f2e91268cc37bf) [#2045](https://github.com/graphql/graphiql/pull/2045) Thanks [@acao](https://github.com/acao)! - fix graphql-js peer dependencies - [#2044](https://github.com/graphql/graphiql/pull/2044) + +- Updated dependencies [[`858907d2`](https://github.com/graphql/graphiql/commit/858907d2106742a65ec52eb017f2e91268cc37bf)]: + - graphql-language-service@3.2.2 + - graphql-language-service-utils@2.6.1 + +## 2.7.2 + +### Patch Changes + +- [`7e98c6ff`](https://github.com/graphql/graphiql/commit/7e98c6fff3b1c62954c9c8d902ac64ddbf23fc5d) Thanks [@acao](https://github.com/acao)! - upgrade graphql-language-service-server to use graphql-config 4.1.0! adds support for .ts and .toml config files in the language server, amongst many other improvements! + +## 2.7.1 + +### Patch Changes + +- [`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce) [#2013](https://github.com/graphql/graphiql/pull/2013) Thanks [@PabloSzx](https://github.com/PabloSzx)! - Update utils + +- Updated dependencies [[`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce), [`9a6ed03f`](https://github.com/graphql/graphiql/commit/9a6ed03fbe4de9652ff5d81a8f584234995dd2ce)]: + - graphql-language-service-utils@2.6.0 + - graphql-language-service@3.2.1 + +## 2.7.0 + +### Minor Changes + +- [`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a) [#2010](https://github.com/graphql/graphiql/pull/2010) Thanks [@acao](https://github.com/acao)! - upgrade to `graphql@16.0.0-experimental-stream-defer.5`. thanks @saihaj! + +### Patch Changes + +- Updated dependencies [[`716cf786`](https://github.com/graphql/graphiql/commit/716cf786aea6af42ea637ca3c56ae6c6ebc17c7a)]: + - graphql-language-service@3.2.0 + +## 2.6.5 + +### Patch Changes + +- [`83c4a007`](https://github.com/graphql/graphiql/commit/83c4a0070a4df704ce874ec977d65ca6c7e43ee8) [#1964](https://github.com/graphql/graphiql/pull/1964) Thanks [@patrickszmucer](https://github.com/patrickszmucer)! - Fix unknown fragment errors on save + +* [`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8) [#1777](https://github.com/graphql/graphiql/pull/1777) Thanks [@dwwoelfel](https://github.com/dwwoelfel)! - adopt block string parsing for variables in language parser + +* Updated dependencies [[`0e2c1a02`](https://github.com/graphql/graphiql/commit/0e2c1a020cc2761155f7c9467d3ed4cb45941aeb), [`75dbb0b1`](https://github.com/graphql/graphiql/commit/75dbb0b18e2102d271a5cfe78faf54fe22e83ac8)]: + - graphql-language-service@3.1.6 + +## 2.6.4 + +### Patch Changes + +- [`72bff0e7`](https://github.com/graphql/graphiql/commit/72bff0e7db46fb53293efc990dc64d2c06401459) [#1951](https://github.com/graphql/graphiql/pull/1951) Thanks [@GoodForOneFare](https://github.com/GoodForOneFare)! - fix: skip config updates when no custom filename is defined + +* [`2fd5bf72`](https://github.com/graphql/graphiql/commit/2fd5bf7239edb78339e5ac7211f09c245e47c3bb) [#1941](https://github.com/graphql/graphiql/pull/1941) Thanks [@arcanis](https://github.com/arcanis)! - Adds support for `#graphql` and `/* GraphQL */` in the language server + +* Updated dependencies [[`2fd5bf72`](https://github.com/graphql/graphiql/commit/2fd5bf7239edb78339e5ac7211f09c245e47c3bb)]: + - graphql-language-service@3.1.5 + +## 2.6.3 + +### Patch Changes + +- [`6869ce77`](https://github.com/graphql/graphiql/commit/6869ce7767050787db5f1017abf82fa5a52fc97a) [#1816](https://github.com/graphql/graphiql/pull/1816) Thanks [@acao](https://github.com/acao)! - improve peer resolutions for graphql 14 & 15. `14.5.0` minimum is for built-in typescript types, and another method only available in `14.4.0` + +## [2.6.2](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.6.1...graphql-language-service-server@2.6.2) (2021-01-07) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.6.1](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.6.0...graphql-language-service-server@2.6.1) (2021-01-07) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.6.0](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.9...graphql-language-service-server@2.6.0) (2021-01-07) + +### Features + +- implied or external fragments, for [#612](https://github.com/graphql/graphiql/issues/612) ([#1750](https://github.com/graphql/graphiql/issues/1750)) ([cfed265](https://github.com/graphql/graphiql/commit/cfed265e3cf31875b39ea517781a217fcdfcadc2)) + +## [2.5.9](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.8...graphql-language-service-server@2.5.9) (2021-01-03) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.8](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.7...graphql-language-service-server@2.5.8) (2020-12-28) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.7](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.6...graphql-language-service-server@2.5.7) (2020-12-08) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.6](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.5...graphql-language-service-server@2.5.6) (2020-11-28) + +### Bug Fixes + +- crash on receiving an LSP message in "stream" mode ([1238075](https://github.com/graphql/graphiql/commit/1238075c5bbd18b09f493c0018da5e4b24e8e615)), closes [#1708](https://github.com/graphql/graphiql/issues/1708) +- languageserver filepath on Windows ([#1715](https://github.com/graphql/graphiql/issues/1715)) ([d2feff9](https://github.com/graphql/graphiql/commit/d2feff92aba979fb52fd0e5846776be223fbf11e)) + +## [2.5.5](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.4...graphql-language-service-server@2.5.5) (2020-10-20) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.4](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.3...graphql-language-service-server@2.5.4) (2020-09-23) + +### Bug Fixes + +- useSchemaFileDefinitions, cleanup ([#1674](https://github.com/graphql/graphiql/issues/1674)) ([3673455](https://github.com/graphql/graphiql/commit/36734557e2874384adbfe86b64aeaa93e06df53f)) + +## [2.5.3](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.2...graphql-language-service-server@2.5.3) (2020-09-23) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.2](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.1...graphql-language-service-server@2.5.2) (2020-09-20) + +### Bug Fixes + +- re-introduce allowed extensions ([#1668](https://github.com/graphql/graphiql/issues/1668)) ([eedd575](https://github.com/graphql/graphiql/commit/eedd5753751857bd5837dd8be8602bf7fadb5517)) + +## [2.5.1](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.0...graphql-language-service-server@2.5.1) (2020-09-20) + +### Bug Fixes + +- better error handling when the config isn't present ([#1667](https://github.com/graphql/graphiql/issues/1667)) ([f414300](https://github.com/graphql/graphiql/commit/f4143008f93a8849dfa4caae948d2eceb299a141)) + +## [2.5.0](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.0-alpha.5...graphql-language-service-server@2.5.0) (2020-09-18) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.0-alpha.5](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.0-alpha.4...graphql-language-service-server@2.5.0-alpha.5) (2020-09-11) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.0-alpha.4](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.0-alpha.3...graphql-language-service-server@2.5.0-alpha.4) (2020-08-26) + +### Features + +- custom config baseDir, embedded fragment def offsets ([#1651](https://github.com/graphql/graphiql/issues/1651)) ([e8dc958](https://github.com/graphql/graphiql/commit/e8dc958b46544022fe58b498ca5eef572f54afe0)) + +## [2.5.0-alpha.3](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.0-alpha.2...graphql-language-service-server@2.5.0-alpha.3) (2020-08-22) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.0-alpha.2](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.0-alpha.1...graphql-language-service-server@2.5.0-alpha.2) (2020-08-12) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.5.0-alpha.1](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.5.0-alpha.0...graphql-language-service-server@2.5.0-alpha.1) (2020-08-12) + +### Bug Fixes + +- recursively write tmp directories, write schema async ([#1641](https://github.com/graphql/graphiql/issues/1641)) ([cd0061e](https://github.com/graphql/graphiql/commit/cd0061e1abe47f5f4075d52a6c1e4157cbd0a95a)) + +## [2.5.0-alpha.0](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.1...graphql-language-service-server@2.5.0-alpha.0) (2020-08-10) + +### Bug Fixes + +- pre-caching schema bugs, new server config options ([#1636](https://github.com/graphql/graphiql/issues/1636)) ([d989456](https://github.com/graphql/graphiql/commit/d9894564c056134e15093956e0951dcefe061d76)) + +### Features + +- graphql-config@3 support in lsp server ([#1616](https://github.com/graphql/graphiql/issues/1616)) ([27cd185](https://github.com/graphql/graphiql/commit/27cd18562b64dfe18e6343b6a49f3f606af89d86)) + +## [2.4.1](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0...graphql-language-service-server@2.4.1) (2020-08-06) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.12...graphql-language-service-server@2.4.0) (2020-06-11) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0-alpha.12](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.11...graphql-language-service-server@2.4.0-alpha.12) (2020-06-04) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0-alpha.11](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.10...graphql-language-service-server@2.4.0-alpha.11) (2020-06-04) + +### Bug Fixes + +- cleanup cache entry from lerna publish ([4a26218](https://github.com/graphql/graphiql/commit/4a2621808a1aea8b30d5d27b8d86a60bf2b44b01)) + +## [2.4.0-alpha.10](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.9...graphql-language-service-server@2.4.0-alpha.10) (2020-05-28) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0-alpha.9](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.8...graphql-language-service-server@2.4.0-alpha.9) (2020-05-19) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0-alpha.8](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.7...graphql-language-service-server@2.4.0-alpha.8) (2020-05-17) + +### Bug Fixes + +- remove problematic file resolution module from webpack sco… ([#1489](https://github.com/graphql/graphiql/issues/1489)) ([8dab038](https://github.com/graphql/graphiql/commit/8dab0385772f443f73b559e2c668080733168236)) +- repair CLI, handle all schema and LSP errors ([#1482](https://github.com/graphql/graphiql/issues/1482)) ([992f384](https://github.com/graphql/graphiql/commit/992f38494f20f5877bfd6ff54893854ac7a0eaa2)) + +### Features + +- Monaco Mode - Phase 2 - Mode & Worker ([#1459](https://github.com/graphql/graphiql/issues/1459)) ([bc95fb4](https://github.com/graphql/graphiql/commit/bc95fb46459a4437ff9471ff43c98e1c5c50f51e)) + +## [2.4.0-alpha.7](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.6...graphql-language-service-server@2.4.0-alpha.7) (2020-04-10) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0-alpha.6](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.5...graphql-language-service-server@2.4.0-alpha.6) (2020-04-10) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0-alpha.5](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.4...graphql-language-service-server@2.4.0-alpha.5) (2020-04-06) + +### Features + +- upgrade to graphql@15.0.0 for [#1191](https://github.com/graphql/graphiql/issues/1191) ([#1204](https://github.com/graphql/graphiql/issues/1204)) ([f13c8e9](https://github.com/graphql/graphiql/commit/f13c8e9d0e66df4b051b332c7d02f4bb83e07ffd)) + +## [2.4.0-alpha.4](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.3...graphql-language-service-server@2.4.0-alpha.4) (2020-04-03) + +### Bug Fixes + +- make sure that custom parser is used if passed to process ([#1438](https://github.com/graphql/graphiql/issues/1438)) ([5e098a4](https://github.com/graphql/graphiql/commit/5e098a4a80a8e1cff4541ad34363ab2001fcda4a)) + +### Features + +- make sure @ triggers directive completion automatically ([#1441](https://github.com/graphql/graphiql/issues/1441)) ([935220a](https://github.com/graphql/graphiql/commit/935220a68641b94af2598840b0ced3fd945f86dd)) + +## [2.4.0-alpha.3](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.2...graphql-language-service-server@2.4.0-alpha.3) (2020-03-20) + +**Note:** Version bump only for package graphql-language-service-server + +## [2.4.0-alpha.2](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.4.0-alpha.0...graphql-language-service-server@2.4.0-alpha.2) (2020-03-20) + +### Bug Fixes + +- eslint warnings ([#1360](https://github.com/graphql/graphiql/issues/1360)) ([84d4821](https://github.com/graphql/graphiql/commit/84d4821ee19030314666a46f11a4b69ffaddca45)) +- initial request cache set, import tsc bugs ([#1266](https://github.com/graphql/graphiql/issues/1266)) ([6b98f8a](https://github.com/graphql/graphiql/commit/6b98f8a442d4a8ea160fb90a29acf33f5382db2e)) +- restore error handling for server [#1306](https://github.com/graphql/graphiql/issues/1306) ([#1425](https://github.com/graphql/graphiql/issues/1425)) ([c12d975](https://github.com/graphql/graphiql/commit/c12d975027e4021bbea7ad54e7e0c19ac7943e6c)) +- type check ([#1374](https://github.com/graphql/graphiql/issues/1374)) ([84cc41e](https://github.com/graphql/graphiql/commit/84cc41ef1c5b56d26929edd9669c766cdf3628e8)) +- typo to fix hover ([#1426](https://github.com/graphql/graphiql/issues/1426)) ([1fdcb28](https://github.com/graphql/graphiql/commit/1fdcb28689bf85a31af10cbdc4648c5ed3013672)) + +### Features + +- optionally provide LSP an instantiated GraphQLConfig ([#1432](https://github.com/graphql/graphiql/issues/1432)) ([012db2a](https://github.com/graphql/graphiql/commit/012db2a39bfcddde63ffd2e93dae0c158f8e73ed)) +- typescript, tsx, jsx support for LSP server using babel ([#1427](https://github.com/graphql/graphiql/issues/1427)) ([ee06123](https://github.com/graphql/graphiql/commit/ee061235489c8f5ed27c116c09b606e371ee40c5)) +- **graphql-config:** add graphql config extensions ([#1118](https://github.com/graphql/graphiql/issues/1118)) ([2a77e47](https://github.com/graphql/graphiql/commit/2a77e47719ec9181a00183a08ffa11287b8fd2f5)) +- Symbol support for single document ([#1244](https://github.com/graphql/graphiql/issues/1244)) ([f729f9a](https://github.com/graphql/graphiql/commit/f729f9a3c20362f4515bf3037347a07cc3690b38)) +- use new GraphQL Config ([#1342](https://github.com/graphql/graphiql/issues/1342)) ([e45838f](https://github.com/graphql/graphiql/commit/e45838f5ba579e05b20f1a147ce431478ffad9aa)) + +## [2.4.0-alpha.1](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.3.3...graphql-language-service-server@2.4.0-alpha.1) (2020-01-18) + +### Bug Fixes + +- linting issues, trailingCommas: all ([#1099](https://github.com/graphql/graphiql/issues/1099)) ([de4005b](https://github.com/graphql/graphiql/commit/de4005b)) + +### Features + +- convert LSP Server to Typescript, remove watchman ([#1138](https://github.com/graphql/graphiql/issues/1138)) ([8e33dbb](https://github.com/graphql/graphiql/commit/8e33dbb)) + +## [2.3.3](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.3.2...graphql-language-service-server@2.3.3) (2019-12-09) + +### Bug Fixes + +- a few more tweaks to babel ignore ([e0ad2c6](https://github.com/graphql/graphiql/commit/e0ad2c6)) + +## [2.3.2](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.3.1...graphql-language-service-server@2.3.2) (2019-12-03) + +### Bug Fixes + +- convert browserify build to webpack, fixes [#976](https://github.com/graphql/graphiql/issues/976) ([#1001](https://github.com/graphql/graphiql/issues/1001)) ([3caf041](https://github.com/graphql/graphiql/commit/3caf041)) + +## [2.3.1](https://github.com/graphql/graphiql/compare/graphql-language-service-server@2.3.0...graphql-language-service-server@2.3.1) (2019-11-26) + +**Note:** Version bump only for package graphql-language-service-server + +# 2.3.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 0.0.1 (2017-03-29) + +# 2.2.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 0.0.1 (2017-03-29) + +# 2.2.0-alpha.0 (2019-10-04) + +### Features + +- convert LSP from flow to typescript ([#957](https://github.com/graphql/graphiql/issues/957)) [@acao](https://github.com/acao) @Neitsch [@benjie](https://github.com/benjie) ([36ed669](https://github.com/graphql/graphiql/commit/36ed669)) + +## 0.0.1 (2017-03-29) + +## 2.1.1-alpha.1 (2019-09-01) + +## 0.0.1 (2017-03-29) + +**Note:** Version bump only for package graphql-language-service-server + +## 2.1.1-alpha.0 (2019-09-01) + +## 0.0.1 (2017-03-29) + +**Note:** Version bump only for package graphql-language-service-server + +## 2.1.1 (2019-09-01) + +## 0.0.1 (2017-03-29) + +**Note:** Version bump only for package graphql-language-service-server diff --git a/packages/graphql-language-service-server/LICENSE b/packages/graphql-language-service-server/LICENSE new file mode 100644 index 00000000000..7802f239a32 --- /dev/null +++ b/packages/graphql-language-service-server/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 GraphQL Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/graphql-language-service-server/README.md b/packages/graphql-language-service-server/README.md new file mode 100644 index 00000000000..d0f5388cd9a --- /dev/null +++ b/packages/graphql-language-service-server/README.md @@ -0,0 +1,313 @@ +# graphql-language-service-server + +[![NPM](https://img.shields.io/npm/v/graphql-language-service-server.svg?style=flat-square)](https://npmjs.com/graphql-language-service-server) +![npm downloads](https://img.shields.io/npm/dm/graphql-language-service-server?label=npm%20downloads) +[![License](https://img.shields.io/npm/l/graphql-language-service-server.svg?style=flat-square)](LICENSE) + +[Changelog](https://github.com/graphql/graphiql/blob/main/packages/graphql-language-service-server/CHANGELOG.md) +| +[API Docs](https://graphiql-test.netlify.app/typedoc/modules/graphql_language_service_server.html) +| [Discord Channel](https://discord.gg/PXaRYrpgK4) + +Server process backing the +[GraphQL Language Service](https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service). + +GraphQL Language Service Server provides an interface for building GraphQL +language services for IDEs. + +Partial support for +[Microsoft's Language Server Protocol](https://github.com/Microsoft/language-server-protocol) +is in place, with more to come in the future. + +Supported features include: + +- Diagnostics (GraphQL syntax linting/validations) (**spec-compliant**) +- Autocomplete suggestions (**spec-compliant**) +- Hyperlink to fragment definitions and named types (type, input, enum) + definitions (**spec-compliant**) +- Outline view support for queries +- Support for `gql` `graphql` and other template tags inside javascript, + typescript, jsx, ts, vue and svelte files, and an interface to allow custom + parsing of all files. + +## Installation and Usage + +### Dependencies + +An LSP compatible client with its own file watcher, that sends watch +notifications to the server. + +**DROPPED**: GraphQL Language Service no longer depends on +[Watchman](https://facebook.github.io/watchman/) + +### Installation + +```bash +npm install --save graphql-language-service-server +# or +yarn add graphql-language-service-server +``` + +We also provide a CLI interface to this server, see +[`graphql-language-service-cli`](../graphql-language-service-cli/) + +### Usage + +Initialize the GraphQL Language Server with the `startServer` function: + +```ts +import { startServer } from 'graphql-language-service-server'; + +await startServer({ + method: 'node', +}); +``` + +If you are developing a service or extension, this is the LSP language server +you want to run. + +When developing vscode extensions, just the above is enough to get started for +your extension's `ServerOptions.run.module`, for example. + +`startServer` function takes the following parameters: + +| Parameter | Required | Description | +| -------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------- | +| port | `true` when method is `socket`, `false` otherwise | port for the LSP server to run on | +| method | `false` | `socket`, `streams`, or `node` (ipc) | +| config | `false` | custom `graphql-config` instance from `loadConfig` (see example above) | +| configDir | `false` | the directory where graphql-config is found | +| extensions | `false` | array of functions to transform the graphql-config and add extensions dynamically | +| parser | `false` | Customize _all_ file parsing by overriding the default `parseDocument` function | +| fileExtensions | `false`. defaults to `['.js', '.ts', '.tsx, '.jsx']` | Customize file extensions used by the default LSP parser | + +### GraphQL configuration file + +You _must_ provide a graphql config file + +Check out [graphql-config](https://graphql-config.com/introduction) to learn the +many ways you can define your graphql config + +#### `.graphqlrc` or `.graphqlrc.yml/yaml` or `graphql.config.yml` + +```yaml +schema: 'packages/api/src/schema.graphql' +documents: 'packages/app/src/components/**/*.{tsx,ts}' +extensions: + endpoints: + example: + url: 'http://localhost:8000' + customExtension: + foo: true +``` + +#### `.graphqlrc` or `.graphqlrc.json` or `graphql.config.json` + +```json +{ + "schema": "https://localhost:8000" +} +``` + +#### `graphql.config.js` or `.graphqlrc.js` + +```js +module.exports = { schema: 'https://localhost:8000' }; +``` + +#### custom `startServer` + +use graphql config [`loadConfig`](https://graphql-config.com/load-config) for +further customization: + +```ts +import { loadConfig } from 'graphql-config'; // 3.0.0 or later! + +await startServer({ + method: 'node', + // or instead of configName, an exact path (relative from rootDir or absolute) + + // deprecated for: loadConfigOptions.rootDir. root directory for graphql config file(s), or for relative resolution for exact `filePath`. default process.cwd() + // configDir: '', + loadConfigOptions: { + // any of the options for graphql-config@3 `loadConfig()` + + // rootDir is same as `configDir` before, the path where the graphql config file would be found by cosmic-config + rootDir: 'config/', + // or - the relative or absolute path to your file + filePath: 'exact/path/to/config.js', // (also supports yml, json, ts, toml) + // myPlatform.config.js/json/yaml works now! + configName: 'myPlatform', + }, +}); +``` + + + +#### Custom `graphql-config` features + +The graphql-config features we support are: + +```js +module.exports = { + extensions: { + // add customDirectives (legacy). you can now provide multiple schema pointers to config.schema/project.schema, including inline strings. same with scalars or any SDL type that you'd like to append to the schema + customDirectives: ['@myExampleDirective'], + // a function that returns an array of validation rules, ala https://github.com/graphql/graphql-js/tree/main/src/validation/rules + // note that this file will be loaded by the vscode runtime, so the node version and other factors will come into play + customValidationRules: require('./config/customValidationRules'), + languageService: { + // should the language service read schema for definition lookups from a cached file based on graphql config output? + // NOTE: this will disable all definition lookup for local SDL files + cacheSchemaFileForLookup: true, + // undefined by default which has the same effect as `true`, set to `false` if you are already using // `graphql-eslint` or some other tool for validating graphql in your IDE. Must be explicitly `false` to disable this feature, not just "falsy" + enableValidation: true, + }, + }, +}; +``` + +or for multi-project workspaces: + +```ts +// graphql.config.ts +export default { + projects: { + myProject: { + schema: [ + // internally in `graphql-config`, an attempt will be made to combine these schemas into one in-memory schema to use for validation, lookup, etc + 'http://localhost:8080', + './my-project/schema.graphql', + './my-project/schema.ts', + '@customDirective(arg: String!)', + 'scalar CustomScalar', + ], + // project specific defaults + extensions: { + languageService: { + cacheSchemaFileForLookup: true, + enableValidation: false, + }, + }, + }, + anotherProject: { + schema: { + 'http://localhost:8081': { + customHeaders: { Authorization: 'Bearer example' }, + }, + }, + }, + }, + // global defaults for all projects + extensions: { + languageService: { + cacheSchemaFileForLookup: false, + enableValidation: true, + }, + }, +}; +``` + +You can specify any of these settings globally as above, or per project. Read +the graphql-config docs to learn more about this! + +For secrets (headers, urls, etc), you can import `dotenv()` and set a base path +as you wish in your `graphql-config` file to pre-load `process.env` variables. + +### Troubleshooting notes + +- you may need to manually restart the language server for some of these + configurations to take effect +- graphql-config's multi-project support is not related to multi-root workspaces + in vscode - in fact, each workspace can have multiple graphql config projects, + which is what makes multi-root workspaces tricky to support. coming soon! + + + +### Workspace Configuration + +The LSP Server reads config by sending `workspace/configuration` method when it +initializes. + +Note: We still do not support LSP multi-root workspaces but will tackle this +very soon! + +Many LSP clients beyond vscode offer ways to set these configurations, such as +via `initializationOptions` in nvim.coc. The options are mostly designed to +configure graphql-config's load parameters, the only thing we can't configure +with graphql config. The final option can be set in `graphql-config` as well + +| Parameter | Default | Description | +| ----------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `graphql-config.load.baseDir` | workspace root or process.cwd() | the path where graphql config looks for config files | +| `graphql-config.load.filePath` | `null` | exact filepath of the config file. | +| `graphql-config.load.configName` | `graphql` | config name prefix instead of `graphql` | +| `graphql-config.load.legacy` | `true` | backwards compatibility with `graphql-config@2` | +| `graphql-config.dotEnvPath` | `null` | backwards compatibility with `graphql-config@2` | +| `vscode-graphql.cacheSchemaFileForLookup` | `false` | generate an SDL file based on your graphql-config schema configuration for schema definition lookup and other features. useful when your `schema` config are urls | + +all the `graphql-config.load.*` configuration values come from static +`loadConfig()` options in graphql config. + +(more coming soon!) + +### Architectural Overview + +GraphQL Language Service currently communicates via Stream transport with the +IDE server. GraphQL server will receive/send RPC messages to perform language +service features, while caching the necessary GraphQL artifacts such as fragment +definitions, GraphQL schemas etc. More about the server interface and RPC +message format below. + +The IDE server should launch a separate GraphQL server with its own child +process for each `.graphqlrc.yml` file the IDE finds (using the nearest ancestor +directory relative to the file currently being edited): + +``` +./application + + ./productA + .graphqlrc.yml + ProductAQuery.graphql + ProductASchema.graphql + + ./productB + .graphqlrc.yml + ProductBQuery.graphql + ProductBSchema.graphql +``` + +A separate GraphQL server should be instantiated for `ProductA` and `ProductB`, +each with its own `.graphqlrc.yml` file, as illustrated in the directory +structure above. + +The IDE server should manage the lifecycle of the GraphQL server. Ideally, the +IDE server should spawn a child process for each of the GraphQL Language Service +processes necessary, and gracefully exit the processes as the IDE closes. In +case of errors or a sudden halt the GraphQL Language Service will close as the +stream from the IDE closes. + +### Server Interface + +GraphQL Language Server uses [JSON-RPC](http://www.jsonrpc.org/specification) to +communicate with the IDE servers. Microsoft's language server currently supports +two communication transports: Stream (stdio) and IPC. For IPC transport, the +reference guide to be used for development is +[the language server protocol](https://microsoft.github.io/language-server-protocol/specification) +documentation. + +For each transport, there is a slight difference in JSON message format, +especially in how the methods to be invoked are defined - below are the +currently supported methods for each transport (will be updated as progress is +made): + +| | Stream | IPC | +| -------------------: | ---------------------------- | ------------------------------------------- | +| Diagnostics | `getDiagnostics` | `textDocument/publishDiagnostics` | +| Autocompletion | `getAutocompleteSuggestions` | `textDocument/completion` | +| Outline | `getOutline` | `textDocument/outline` | +| Document Symbols | `getDocumentSymbols` | `textDocument/symbols` | +| Workspace Symbols | `getWorkspaceSymbols` | `workspace/symbols` | +| Go-to definition | `getDefinition` | `textDocument/definition` | +| Workspace Definition | `getWorkspaceDefinition` | `workspace/definition` | +| File Events | Not supported yet | `didOpen/didClose/didSave/didChange` events | diff --git a/packages/graphql-language-service-server/jest.config.js b/packages/graphql-language-service-server/jest.config.js new file mode 100644 index 00000000000..b1f1c02ab86 --- /dev/null +++ b/packages/graphql-language-service-server/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../jest.config.base')(__dirname, 'node'); diff --git a/packages/graphql-language-service-server/package.json b/packages/graphql-language-service-server/package.json new file mode 100644 index 00000000000..02bc04a48cc --- /dev/null +++ b/packages/graphql-language-service-server/package.json @@ -0,0 +1,66 @@ +{ + "name": "graphql-language-service-server", + "version": "2.11.3", + "description": "Server process backing the GraphQL Language Service", + "contributors": [ + "Greg Hurrell (https://greg.hurrell.net/)", + "Hyohyeon Jeong ", + "Lee Byron (http://leebyron.com/)" + ], + "repository": { + "type": "git", + "url": "http://github.com/graphql/graphiql", + "directory": "packages/graphql-language-service-server" + }, + "homepage": "https://github.com/graphql/graphiql/tree/main/packages/graphql-language-service-server#readme", + "bugs": { + "url": "https://github.com/graphql/graphiql/issues?q=issue+label:lsp-server" + }, + "license": "MIT", + "files": [ + "dist", + "esm", + "src" + ], + "keywords": [ + "graphql", + "language server", + "LSP", + "vue", + "svelte", + "typescript" + ], + "main": "dist/index.js", + "module": "esm/index.js", + "typings": "esm/index.d.ts", + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0" + }, + "dependencies": { + "@babel/parser": "^7.22.6", + "@babel/types": "^7.22.5", + "@graphql-tools/code-file-loader": "8.0.1", + "@vue/compiler-sfc": "^3.2.41", + "dotenv": "8.2.0", + "fast-glob": "^3.2.7", + "glob": "^7.2.0", + "graphql-config": "5.0.2", + "graphql-language-service": "^5.1.7", + "mkdirp": "^1.0.4", + "node-abort-controller": "^3.0.1", + "nullthrows": "^1.0.0", + "vscode-jsonrpc": "^8.0.1", + "vscode-languageserver": "^8.0.1", + "vscode-languageserver-types": "^3.17.2", + "vscode-uri": "^3.0.2", + "svelte2tsx": "^0.6.16", + "svelte": "^4.0.0", + "cosmiconfig-toml-loader": "^1.0.0" + }, + "devDependencies": { + "@types/mkdirp": "^1.0.1", + "@types/glob": "^8.1.0", + "cross-env": "^7.0.2", + "graphql": "^16.4.0" + } +} diff --git a/packages/graphql-language-service-server/src/GraphQLCache.ts b/packages/graphql-language-service-server/src/GraphQLCache.ts new file mode 100644 index 00000000000..fcf578d42fd --- /dev/null +++ b/packages/graphql-language-service-server/src/GraphQLCache.ts @@ -0,0 +1,870 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { + ASTNode, + DocumentNode, + DefinitionNode, + isTypeDefinitionNode, + GraphQLSchema, + Kind, + extendSchema, + parse, + visit, +} from 'graphql'; +import type { + CachedContent, + GraphQLCache as GraphQLCacheInterface, + GraphQLFileMetadata, + GraphQLFileInfo, + FragmentInfo, + ObjectTypeInfo, + Uri, +} from 'graphql-language-service'; +import type { Logger } from 'vscode-languageserver'; + +import * as fs from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import nullthrows from 'nullthrows'; + +import { + loadConfig, + GraphQLConfig, + GraphQLProjectConfig, + GraphQLExtensionDeclaration, +} from 'graphql-config'; + +import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load'; + +import { parseDocument } from './parseDocument'; +import stringToHash from './stringToHash'; +import glob from 'glob'; +import { LoadConfigOptions } from './types'; +import { URI } from 'vscode-uri'; +import { CodeFileLoader } from '@graphql-tools/code-file-loader'; + +const LanguageServiceExtension: GraphQLExtensionDeclaration = api => { + // For schema + api.loaders.schema.register(new CodeFileLoader()); + // For documents + api.loaders.documents.register(new CodeFileLoader()); + + return { name: 'languageService' }; +}; + +// Maximum files to read when processing GraphQL files. +const MAX_READS = 200; + +export async function getGraphQLCache({ + parser, + logger, + loadConfigOptions, + config, +}: { + parser: typeof parseDocument; + logger: Logger; + loadConfigOptions: LoadConfigOptions; + config?: GraphQLConfig; +}): Promise { + const graphQLConfig = + config || + (await loadConfig({ + ...loadConfigOptions, + extensions: [ + ...(loadConfigOptions?.extensions ?? []), + LanguageServiceExtension, + ], + })); + return new GraphQLCache({ + configDir: loadConfigOptions.rootDir!, + config: graphQLConfig!, + parser, + logger, + }); +} + +export class GraphQLCache implements GraphQLCacheInterface { + _configDir: Uri; + _graphQLFileListCache: Map>; + _graphQLConfig: GraphQLConfig; + _schemaMap: Map; + _typeExtensionMap: Map; + _fragmentDefinitionsCache: Map>; + _typeDefinitionsCache: Map>; + _parser: typeof parseDocument; + _logger: Logger; + + constructor({ + configDir, + config, + parser, + logger, + }: { + configDir: Uri; + config: GraphQLConfig; + parser: typeof parseDocument; + logger: Logger; + }) { + this._configDir = configDir; + this._graphQLConfig = config; + this._graphQLFileListCache = new Map(); + this._schemaMap = new Map(); + this._fragmentDefinitionsCache = new Map(); + this._typeDefinitionsCache = new Map(); + this._typeExtensionMap = new Map(); + this._parser = parser; + this._logger = logger; + } + + getGraphQLConfig = (): GraphQLConfig => this._graphQLConfig; + + getProjectForFile = (uri: string): GraphQLProjectConfig => { + try { + return this._graphQLConfig.getProjectForFile(URI.parse(uri).fsPath); + } catch (err) { + this._logger.error( + `there was an error loading the project config for this file ${err}`, + ); + // @ts-expect-error + return null; + } + }; + + getFragmentDependencies = async ( + query: string, + fragmentDefinitions?: Map | null, + ): Promise => { + // If there isn't context for fragment references, + // return an empty array. + if (!fragmentDefinitions) { + return []; + } + // If the query cannot be parsed, validations cannot happen yet. + // Return an empty array. + let parsedQuery; + try { + parsedQuery = parse(query); + } catch { + return []; + } + return this.getFragmentDependenciesForAST(parsedQuery, fragmentDefinitions); + }; + + getFragmentDependenciesForAST = async ( + parsedQuery: ASTNode, + fragmentDefinitions: Map, + ): Promise => { + if (!fragmentDefinitions) { + return []; + } + + const existingFrags = new Map(); + const referencedFragNames = new Set(); + + visit(parsedQuery, { + FragmentDefinition(node) { + existingFrags.set(node.name.value, true); + }, + FragmentSpread(node) { + if (!referencedFragNames.has(node.name.value)) { + referencedFragNames.add(node.name.value); + } + }, + }); + + const asts = new Set(); + for (const name of referencedFragNames) { + if (!existingFrags.has(name) && fragmentDefinitions.has(name)) { + asts.add(nullthrows(fragmentDefinitions.get(name))); + } + } + + const referencedFragments: FragmentInfo[] = []; + + for (const ast of asts) { + visit(ast.definition, { + FragmentSpread(node) { + if ( + !referencedFragNames.has(node.name.value) && + fragmentDefinitions.get(node.name.value) + ) { + asts.add(nullthrows(fragmentDefinitions.get(node.name.value))); + referencedFragNames.add(node.name.value); + } + }, + }); + if (!existingFrags.has(ast.definition.name.value)) { + referencedFragments.push(ast); + } + } + + return referencedFragments; + }; + + _cacheKeyForProject = ({ dirpath, name }: GraphQLProjectConfig): string => { + return `${dirpath}-${name}`; + }; + + getFragmentDefinitions = async ( + projectConfig: GraphQLProjectConfig, + ): Promise> => { + // This function may be called from other classes. + // If then, check the cache first. + const rootDir = projectConfig.dirpath; + const cacheKey = this._cacheKeyForProject(projectConfig); + if (this._fragmentDefinitionsCache.has(cacheKey)) { + return this._fragmentDefinitionsCache.get(cacheKey) || new Map(); + } + + const list = await this._readFilesFromInputDirs(rootDir, projectConfig); + + const { fragmentDefinitions, graphQLFileMap } = + await this.readAllGraphQLFiles(list); + + this._fragmentDefinitionsCache.set(cacheKey, fragmentDefinitions); + this._graphQLFileListCache.set(cacheKey, graphQLFileMap); + + return fragmentDefinitions; + }; + + getObjectTypeDependencies = async ( + query: string, + objectTypeDefinitions?: Map, + ): Promise> => { + // If there isn't context for object type references, + // return an empty array. + if (!objectTypeDefinitions) { + return []; + } + // If the query cannot be parsed, validations cannot happen yet. + // Return an empty array. + let parsedQuery; + try { + parsedQuery = parse(query); + } catch { + return []; + } + return this.getObjectTypeDependenciesForAST( + parsedQuery, + objectTypeDefinitions, + ); + }; + + getObjectTypeDependenciesForAST = async ( + parsedQuery: ASTNode, + objectTypeDefinitions: Map, + ): Promise> => { + if (!objectTypeDefinitions) { + return []; + } + + const existingObjectTypes = new Map(); + const referencedObjectTypes = new Set(); + + visit(parsedQuery, { + ObjectTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + InputObjectTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + EnumTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + NamedType(node) { + if (!referencedObjectTypes.has(node.name.value)) { + referencedObjectTypes.add(node.name.value); + } + }, + UnionTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + ScalarTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + InterfaceTypeDefinition(node) { + existingObjectTypes.set(node.name.value, true); + }, + }); + + const asts = new Set(); + for (const name of referencedObjectTypes) { + if (!existingObjectTypes.has(name) && objectTypeDefinitions.has(name)) { + asts.add(nullthrows(objectTypeDefinitions.get(name))); + } + } + + const referencedObjects: ObjectTypeInfo[] = []; + + for (const ast of asts) { + visit(ast.definition, { + NamedType(node) { + if ( + !referencedObjectTypes.has(node.name.value) && + objectTypeDefinitions.get(node.name.value) + ) { + asts.add(nullthrows(objectTypeDefinitions.get(node.name.value))); + referencedObjectTypes.add(node.name.value); + } + }, + }); + if (!existingObjectTypes.has(ast.definition.name.value)) { + referencedObjects.push(ast); + } + } + + return referencedObjects; + }; + + getObjectTypeDefinitions = async ( + projectConfig: GraphQLProjectConfig, + ): Promise> => { + // This function may be called from other classes. + // If then, check the cache first. + const rootDir = projectConfig.dirpath; + const cacheKey = this._cacheKeyForProject(projectConfig); + if (this._typeDefinitionsCache.has(cacheKey)) { + return this._typeDefinitionsCache.get(cacheKey) || new Map(); + } + const list = await this._readFilesFromInputDirs(rootDir, projectConfig); + const { objectTypeDefinitions, graphQLFileMap } = + await this.readAllGraphQLFiles(list); + this._typeDefinitionsCache.set(cacheKey, objectTypeDefinitions); + this._graphQLFileListCache.set(cacheKey, graphQLFileMap); + + return objectTypeDefinitions; + }; + + _readFilesFromInputDirs = ( + rootDir: string, + projectConfig: GraphQLProjectConfig, + ): Promise> => { + let pattern: string; + const patterns = this._getSchemaAndDocumentFilePatterns(projectConfig); + + // See https://github.com/graphql/graphql-language-service/issues/221 + // for details on why special handling is required here for the + // documents.length === 1 case. + if (patterns.length === 1) { + // @ts-ignore + pattern = patterns[0]; + } else { + pattern = `{${patterns.join(',')}}`; + } + + return new Promise((resolve, reject) => { + const globResult = new glob.Glob( + pattern, + { + cwd: rootDir, + stat: true, + absolute: false, + ignore: [ + 'generated/relay', + '**/__flow__/**', + '**/__generated__/**', + '**/__github__/**', + '**/__mocks__/**', + '**/node_modules/**', + '**/__flowtests__/**', + ], + }, + error => { + if (error) { + reject(error); + } + }, + ); + globResult.on('end', () => { + resolve( + Object.keys(globResult.statCache) + .filter( + filePath => typeof globResult.statCache[filePath] === 'object', + ) + .filter(filePath => projectConfig.match(filePath)) + .map(filePath => { + // @TODO + // so we have to force this here + // because glob's DefinitelyTyped doesn't use fs.Stats here though + // the docs indicate that is what's there :shrug: + const cacheEntry = globResult.statCache[filePath] as fs.Stats; + return { + filePath: URI.file(filePath).toString(), + mtime: Math.trunc(cacheEntry.mtime.getTime() / 1000), + size: cacheEntry.size, + }; + }), + ); + }); + }); + }; + + _getSchemaAndDocumentFilePatterns = (projectConfig: GraphQLProjectConfig) => { + const patterns: string[] = []; + + for (const pointer of [projectConfig.documents, projectConfig.schema]) { + if (pointer) { + if (typeof pointer === 'string') { + patterns.push(pointer); + } else if (Array.isArray(pointer)) { + patterns.push(...pointer); + } + } + } + + return patterns; + }; + + async _updateGraphQLFileListCache( + graphQLFileMap: Map, + metrics: { size: number; mtime: number }, + filePath: Uri, + exists: boolean, + ): Promise> { + const fileAndContent = exists + ? await this.promiseToReadGraphQLFile(filePath) + : null; + + const existingFile = graphQLFileMap.get(filePath); + + // 3 cases for the cache invalidation: create/modify/delete. + // For create/modify, swap the existing entry if available; + // otherwise, just push in the new entry created. + // For delete, check `exists` and splice the file out. + if (existingFile && !exists) { + graphQLFileMap.delete(filePath); + } else if (fileAndContent) { + const graphQLFileInfo = { ...fileAndContent, ...metrics }; + graphQLFileMap.set(filePath, graphQLFileInfo); + } + + return graphQLFileMap; + } + + async updateFragmentDefinition( + rootDir: Uri, + filePath: Uri, + contents: Array, + ): Promise { + const cache = this._fragmentDefinitionsCache.get(rootDir); + const asts = contents.map(({ query }) => { + try { + return { + ast: parse(query), + query, + }; + } catch { + return { ast: null, query }; + } + }); + if (cache) { + // first go through the fragment list to delete the ones from this file + for (const [key, value] of cache.entries()) { + if (value.filePath === filePath) { + cache.delete(key); + } + } + for (const { ast, query } of asts) { + if (!ast) { + continue; + } + for (const definition of ast.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + cache.set(definition.name.value, { + filePath, + content: query, + definition, + }); + } + } + } + } + } + + async updateFragmentDefinitionCache( + rootDir: Uri, + filePath: Uri, + exists: boolean, + ): Promise { + const fileAndContent = exists + ? await this.promiseToReadGraphQLFile(filePath) + : null; + // In the case of fragment definitions, the cache could just map the + // definition name to the parsed ast, whether or not it existed + // previously. + // For delete, remove the entry from the set. + if (!exists) { + const cache = this._fragmentDefinitionsCache.get(rootDir); + if (cache) { + cache.delete(filePath); + } + } else if (fileAndContent?.queries) { + await this.updateFragmentDefinition( + rootDir, + filePath, + fileAndContent.queries, + ); + } + } + + async updateObjectTypeDefinition( + rootDir: Uri, + filePath: Uri, + contents: Array, + ): Promise { + const cache = this._typeDefinitionsCache.get(rootDir); + const asts = contents.map(({ query }) => { + try { + return { + ast: parse(query), + query, + }; + } catch { + return { ast: null, query }; + } + }); + if (cache) { + // first go through the types list to delete the ones from this file + for (const [key, value] of cache.entries()) { + if (value.filePath === filePath) { + cache.delete(key); + } + } + for (const { ast, query } of asts) { + if (!ast) { + continue; + } + for (const definition of ast.definitions) { + if (isTypeDefinitionNode(definition)) { + cache.set(definition.name.value, { + filePath, + content: query, + definition, + }); + } + } + } + } + } + + async updateObjectTypeDefinitionCache( + rootDir: Uri, + filePath: Uri, + exists: boolean, + ): Promise { + const fileAndContent = exists + ? await this.promiseToReadGraphQLFile(filePath) + : null; + // In the case of type definitions, the cache could just map the + // definition name to the parsed ast, whether or not it existed + // previously. + // For delete, remove the entry from the set. + if (!exists) { + const cache = this._typeDefinitionsCache.get(rootDir); + if (cache) { + cache.delete(filePath); + } + } else if (fileAndContent?.queries) { + await this.updateObjectTypeDefinition( + rootDir, + filePath, + fileAndContent.queries, + ); + } + } + + _extendSchema( + schema: GraphQLSchema, + schemaPath: string | null, + schemaCacheKey: string | null, + ): GraphQLSchema { + const graphQLFileMap = this._graphQLFileListCache.get(this._configDir); + const typeExtensions: DefinitionNode[] = []; + + if (!graphQLFileMap) { + return schema; + } + for (const { filePath, asts } of graphQLFileMap.values()) { + for (const ast of asts) { + if (filePath === schemaPath) { + continue; + } + for (const definition of ast.definitions) { + switch (definition.kind) { + case Kind.OBJECT_TYPE_DEFINITION: + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.ENUM_TYPE_DEFINITION: + case Kind.UNION_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_DEFINITION: + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_EXTENSION: + case Kind.OBJECT_TYPE_EXTENSION: + case Kind.INTERFACE_TYPE_EXTENSION: + case Kind.UNION_TYPE_EXTENSION: + case Kind.ENUM_TYPE_EXTENSION: + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + case Kind.DIRECTIVE_DEFINITION: + typeExtensions.push(definition); + break; + } + } + } + } + + if (schemaCacheKey) { + const sorted = typeExtensions.sort((a: any, b: any) => { + const aName = a.definition ? a.definition.name.value : a.name.value; + const bName = b.definition ? b.definition.name.value : b.name.value; + return aName > bName ? 1 : -1; + }); + const hash = stringToHash(JSON.stringify(sorted)); + + if ( + this._typeExtensionMap.has(schemaCacheKey) && + this._typeExtensionMap.get(schemaCacheKey) === hash + ) { + return schema; + } + + this._typeExtensionMap.set(schemaCacheKey, hash); + } + + return extendSchema(schema, { + kind: Kind.DOCUMENT, + definitions: typeExtensions, + }); + } + + getSchema = async ( + appName?: string, + queryHasExtensions?: boolean | null, + ): Promise => { + const projectConfig = this._graphQLConfig.getProject(appName); + + if (!projectConfig) { + return null; + } + + const schemaPath = projectConfig.schema as string; + const schemaKey = this._getSchemaCacheKeyForProject(projectConfig); + + let schemaCacheKey = null; + let schema = null; + + if (schemaPath && schemaKey) { + schemaCacheKey = schemaKey as string; + + // Maybe use cache + if (this._schemaMap.has(schemaCacheKey)) { + schema = this._schemaMap.get(schemaCacheKey); + if (schema) { + return queryHasExtensions + ? this._extendSchema(schema, schemaPath, schemaCacheKey) + : schema; + } + } + + // Read from disk + schema = await projectConfig.getSchema(); + } + + const customDirectives = projectConfig?.extensions?.customDirectives; + if (customDirectives && schema) { + const directivesSDL = customDirectives.join('\n\n'); + schema = extendSchema(schema, parse(directivesSDL)); + } + + if (!schema) { + return null; + } + + if (this._graphQLFileListCache.has(this._configDir)) { + schema = this._extendSchema(schema, schemaPath, schemaCacheKey); + } + + if (schemaCacheKey) { + this._schemaMap.set(schemaCacheKey, schema); + } + return schema; + }; + + invalidateSchemaCacheForProject(projectConfig: GraphQLProjectConfig) { + const schemaKey = this._getSchemaCacheKeyForProject( + projectConfig, + ) as string; + if (schemaKey) { + this._schemaMap.delete(schemaKey); + } + } + + _getSchemaCacheKeyForProject( + projectConfig: GraphQLProjectConfig, + ): UnnormalizedTypeDefPointer { + return projectConfig.schema; + } + + _getProjectName(projectConfig: GraphQLProjectConfig) { + return projectConfig || 'default'; + } + + /** + * Given a list of GraphQL file metadata, read all files collected from watchman + * and create fragmentDefinitions and GraphQL files cache. + */ + readAllGraphQLFiles = async ( + list: Array, + ): Promise<{ + objectTypeDefinitions: Map; + fragmentDefinitions: Map; + graphQLFileMap: Map; + }> => { + const queue = list.slice(); // copy + const responses: GraphQLFileInfo[] = []; + while (queue.length) { + const chunk = queue.splice(0, MAX_READS); + const promises = chunk.map(async fileInfo => { + try { + const response = await this.promiseToReadGraphQLFile( + fileInfo.filePath, + ); + responses.push({ + ...response, + mtime: fileInfo.mtime, + size: fileInfo.size, + }); + } catch (error: any) { + // eslint-disable-next-line no-console + console.log('pro', error); + /** + * fs emits `EMFILE | ENFILE` error when there are too many + * open files - this can cause some fragment files not to be + * processed. Solve this case by implementing a queue to save + * files failed to be processed because of `EMFILE` error, + * and await on Promises created with the next batch from the + * queue. + */ + if (error.code === 'EMFILE' || error.code === 'ENFILE') { + queue.push(fileInfo); + } + } + }); + await Promise.all(promises); // eslint-disable-line no-await-in-loop + } + + return this.processGraphQLFiles(responses); + }; + + /** + * Takes an array of GraphQL File information and batch-processes into a + * map of fragmentDefinitions and GraphQL file cache. + */ + processGraphQLFiles = ( + responses: Array, + ): { + objectTypeDefinitions: Map; + fragmentDefinitions: Map; + graphQLFileMap: Map; + } => { + const objectTypeDefinitions = new Map(); + const fragmentDefinitions = new Map(); + const graphQLFileMap = new Map(); + + for (const response of responses) { + const { filePath, content, asts, mtime, size } = response; + + if (asts) { + for (const ast of asts) { + for (const definition of ast.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + fragmentDefinitions.set(definition.name.value, { + filePath, + content, + definition, + }); + } else if (isTypeDefinitionNode(definition)) { + objectTypeDefinitions.set(definition.name.value, { + filePath, + content, + definition, + }); + } + } + } + } + + // Relay the previous object whether or not ast exists. + graphQLFileMap.set(filePath, { + filePath, + content, + asts, + mtime, + size, + }); + } + + return { + objectTypeDefinitions, + fragmentDefinitions, + graphQLFileMap, + }; + }; + + /** + * Returns a Promise to read a GraphQL file and return a GraphQL metadata + * including a parsed AST. + */ + promiseToReadGraphQLFile = async ( + filePath: Uri, + ): Promise => { + const content = await readFile(URI.parse(filePath).fsPath, 'utf8'); + + const asts: DocumentNode[] = []; + let queries: CachedContent[] = []; + if (content.trim().length !== 0) { + try { + queries = this._parser(content, filePath); + if (queries.length === 0) { + // still resolve with an empty ast + return { + filePath, + content, + asts: [], + queries: [], + mtime: 0, + size: 0, + }; + } + + for (const { query } of queries) { + asts.push(parse(query)); + } + return { + filePath, + content, + asts, + queries, + mtime: 0, + size: 0, + }; + } catch { + // If query has syntax errors, go ahead and still resolve + // the filePath and the content, but leave ast empty. + return { + filePath, + content, + asts: [], + queries: [], + mtime: 0, + size: 0, + }; + } + } + return { filePath, content, asts, queries, mtime: 0, size: 0 }; + }; +} diff --git a/packages/graphql-language-service-server/src/GraphQLLanguageService.ts b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts new file mode 100644 index 00000000000..aeaa4c92a8e --- /dev/null +++ b/packages/graphql-language-service-server/src/GraphQLLanguageService.ts @@ -0,0 +1,489 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { + DocumentNode, + FragmentSpreadNode, + FragmentDefinitionNode, + TypeDefinitionNode, + NamedTypeNode, + ValidationRule, + FieldNode, + GraphQLError, + Kind, + parse, + print, + isTypeDefinitionNode, +} from 'graphql'; + +import { + CompletionItem, + Diagnostic, + Uri, + IPosition, + Outline, + OutlineTree, + GraphQLCache, + getAutocompleteSuggestions, + getHoverInformation, + HoverConfig, + validateQuery, + getRange, + DIAGNOSTIC_SEVERITY, + getOutline, + getDefinitionQueryResultForFragmentSpread, + getDefinitionQueryResultForDefinitionNode, + getDefinitionQueryResultForNamedType, + getDefinitionQueryResultForField, + DefinitionQueryResult, + getASTNodeAtPosition, + getTokenAtPosition, + getTypeInfo, +} from 'graphql-language-service'; + +import { GraphQLConfig, GraphQLProjectConfig } from 'graphql-config'; + +import type { Logger } from 'vscode-languageserver'; +import { + Hover, + SymbolInformation, + SymbolKind, +} from 'vscode-languageserver-types'; + +const KIND_TO_SYMBOL_KIND: { [key: string]: SymbolKind } = { + [Kind.FIELD]: SymbolKind.Field, + [Kind.OPERATION_DEFINITION]: SymbolKind.Class, + [Kind.FRAGMENT_DEFINITION]: SymbolKind.Class, + [Kind.FRAGMENT_SPREAD]: SymbolKind.Struct, + [Kind.OBJECT_TYPE_DEFINITION]: SymbolKind.Class, + [Kind.ENUM_TYPE_DEFINITION]: SymbolKind.Enum, + [Kind.ENUM_VALUE_DEFINITION]: SymbolKind.EnumMember, + [Kind.INPUT_OBJECT_TYPE_DEFINITION]: SymbolKind.Class, + [Kind.INPUT_VALUE_DEFINITION]: SymbolKind.Field, + [Kind.FIELD_DEFINITION]: SymbolKind.Field, + [Kind.INTERFACE_TYPE_DEFINITION]: SymbolKind.Interface, + [Kind.DOCUMENT]: SymbolKind.File, + // novel, for symbols only + FieldWithArguments: SymbolKind.Method, +}; + +function getKind(tree: OutlineTree) { + if ( + tree.kind === 'FieldDefinition' && + tree.children && + tree.children.length > 0 + ) { + return KIND_TO_SYMBOL_KIND.FieldWithArguments; + } + return KIND_TO_SYMBOL_KIND[tree.kind]; +} + +export class GraphQLLanguageService { + _graphQLCache: GraphQLCache; + _graphQLConfig: GraphQLConfig; + _logger: Logger; + + constructor(cache: GraphQLCache, logger: Logger) { + this._graphQLCache = cache; + this._graphQLConfig = cache.getGraphQLConfig(); + + this._logger = logger; + } + + getConfigForURI(uri: Uri) { + const config = this._graphQLCache.getProjectForFile(uri); + if (config) { + return config; + } + } + + public async getDiagnostics( + document: string, + uri: Uri, + isRelayCompatMode?: boolean, + ): Promise> { + // Perform syntax diagnostics first, as this doesn't require + // schema/fragment definitions, even the project configuration. + let documentHasExtensions = false; + const projectConfig = this.getConfigForURI(uri); + // skip validation when there's nothing to validate, prevents noisy unexpected EOF errors + if (!projectConfig || !document || document.trim().length < 2) { + return []; + } + const { schema: schemaPath, name: projectName, extensions } = projectConfig; + + try { + const documentAST = parse(document); + if (!schemaPath || uri !== schemaPath) { + documentHasExtensions = documentAST.definitions.some(definition => { + switch (definition.kind) { + case Kind.OBJECT_TYPE_DEFINITION: + case Kind.INTERFACE_TYPE_DEFINITION: + case Kind.ENUM_TYPE_DEFINITION: + case Kind.UNION_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_DEFINITION: + case Kind.INPUT_OBJECT_TYPE_DEFINITION: + case Kind.SCALAR_TYPE_EXTENSION: + case Kind.OBJECT_TYPE_EXTENSION: + case Kind.INTERFACE_TYPE_EXTENSION: + case Kind.UNION_TYPE_EXTENSION: + case Kind.ENUM_TYPE_EXTENSION: + case Kind.INPUT_OBJECT_TYPE_EXTENSION: + case Kind.DIRECTIVE_DEFINITION: + return true; + } + + return false; + }); + } + } catch (error) { + if (error instanceof GraphQLError) { + const range = getRange( + error.locations?.[0] ?? { column: 0, line: 0 }, + document, + ); + return [ + { + severity: DIAGNOSTIC_SEVERITY.Error, + message: error.message, + source: 'GraphQL: Syntax', + range, + }, + ]; + } + + throw error; + } + + // If there's a matching config, proceed to prepare to run validation + let source = document; + const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( + projectConfig, + ); + + const fragmentDependencies = + await this._graphQLCache.getFragmentDependencies( + document, + fragmentDefinitions, + ); + + const dependenciesSource = fragmentDependencies.reduce( + (prev, cur) => `${prev} ${print(cur.definition)}`, + '', + ); + + source = `${source} ${dependenciesSource}`; + + let validationAst = null; + try { + validationAst = parse(source); + } catch { + // the query string is already checked to be parsed properly - errors + // from this parse must be from corrupted fragment dependencies. + // For IDEs we don't care for errors outside of the currently edited + // query, so we return an empty array here. + return []; + } + + // Check if there are custom validation rules to be used + let customRules: ValidationRule[] | null = null; + if ( + extensions?.customValidationRules && + typeof extensions.customValidationRules === 'function' + ) { + customRules = extensions.customValidationRules(this._graphQLConfig); + + /* eslint-enable no-implicit-coercion */ + } + const schema = await this._graphQLCache.getSchema( + projectName, + documentHasExtensions, + ); + + if (!schema) { + return []; + } + + return validateQuery(validationAst, schema, customRules, isRelayCompatMode); + } + + public async getAutocompleteSuggestions( + query: string, + position: IPosition, + filePath: Uri, + ): Promise> { + const projectConfig = this.getConfigForURI(filePath); + if (!projectConfig) { + return []; + } + const schema = await this._graphQLCache.getSchema(projectConfig.name); + const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( + projectConfig, + ); + + const fragmentInfo = Array.from(fragmentDefinitions).map( + ([, info]) => info.definition, + ); + + if (schema) { + return getAutocompleteSuggestions( + schema, + query, + position, + undefined, + fragmentInfo, + { + uri: filePath, + fillLeafsOnComplete: + projectConfig?.extensions?.languageService?.fillLeafsOnComplete ?? + false, + }, + ); + } + return []; + } + + public async getHoverInformation( + query: string, + position: IPosition, + filePath: Uri, + options?: HoverConfig, + ): Promise { + const projectConfig = this.getConfigForURI(filePath); + if (!projectConfig) { + return ''; + } + const schema = await this._graphQLCache.getSchema(projectConfig.name); + + if (schema) { + return getHoverInformation(schema, query, position, undefined, options); + } + return ''; + } + + public async getDefinition( + query: string, + position: IPosition, + filePath: Uri, + ): Promise { + const projectConfig = this.getConfigForURI(filePath); + if (!projectConfig) { + return null; + } + + let ast; + try { + ast = parse(query); + } catch { + return null; + } + + const node = getASTNodeAtPosition(query, ast, position); + if (node) { + switch (node.kind) { + case Kind.FRAGMENT_SPREAD: + return this._getDefinitionForFragmentSpread( + query, + ast, + node, + filePath, + projectConfig, + ); + + case Kind.FRAGMENT_DEFINITION: + case Kind.OPERATION_DEFINITION: + return getDefinitionQueryResultForDefinitionNode( + filePath, + query, + node, + ); + + case Kind.NAMED_TYPE: + return this._getDefinitionForNamedType( + query, + ast, + node, + filePath, + projectConfig, + ); + + case Kind.FIELD: + return this._getDefinitionForField( + query, + ast, + node, + filePath, + projectConfig, + position, + ); + } + } + return null; + } + + public async getDocumentSymbols( + document: string, + filePath: Uri, + ): Promise { + const outline = await this.getOutline(document); + if (!outline) { + return []; + } + + const output: Array = []; + const input = outline.outlineTrees.map((tree: OutlineTree) => [null, tree]); + + while (input.length > 0) { + const res = input.pop(); + if (!res) { + return []; + } + const [parent, tree] = res; + if (!tree) { + return []; + } + + output.push({ + // @ts-ignore + name: tree.representativeName ?? 'Anonymous', + kind: getKind(tree), + location: { + uri: filePath, + range: { + start: tree.startPosition, + // @ts-ignore + end: tree.endPosition, + }, + }, + containerName: parent ? parent.representativeName : undefined, + }); + input.push(...tree.children.map(child => [tree, child])); + } + return output; + } + // + // public async getReferences( + // document: string, + // position: Position, + // filePath: Uri, + // ): Promise { + // + // } + + async _getDefinitionForNamedType( + query: string, + ast: DocumentNode, + node: NamedTypeNode, + filePath: Uri, + projectConfig: GraphQLProjectConfig, + ): Promise { + const objectTypeDefinitions = + await this._graphQLCache.getObjectTypeDefinitions(projectConfig); + + const dependencies = + await this._graphQLCache.getObjectTypeDependenciesForAST( + ast, + objectTypeDefinitions, + ); + + const localOperationDefinitionInfos = ast.definitions + .filter(isTypeDefinitionNode) + .map((definition: TypeDefinitionNode) => ({ + filePath, + content: query, + definition, + })); + + const result = await getDefinitionQueryResultForNamedType( + query, + node, + dependencies.concat(localOperationDefinitionInfos), + ); + + return result; + } + + async _getDefinitionForField( + query: string, + _ast: DocumentNode, + _node: FieldNode, + _filePath: Uri, + projectConfig: GraphQLProjectConfig, + position: IPosition, + ) { + const token = getTokenAtPosition(query, position); + const schema = await this._graphQLCache.getSchema(projectConfig.name); + + const typeInfo = getTypeInfo(schema!, token.state); + const fieldName = typeInfo.fieldDef?.name; + + if (typeInfo && fieldName) { + const parentTypeName = (typeInfo.parentType as any).toString(); + + const objectTypeDefinitions = + await this._graphQLCache.getObjectTypeDefinitions(projectConfig); + + // TODO: need something like getObjectTypeDependenciesForAST? + const dependencies = [...objectTypeDefinitions.values()]; + + const result = await getDefinitionQueryResultForField( + fieldName, + parentTypeName, + dependencies, + ); + + return result; + } + + return null; + } + + async _getDefinitionForFragmentSpread( + query: string, + ast: DocumentNode, + node: FragmentSpreadNode, + filePath: Uri, + projectConfig: GraphQLProjectConfig, + ): Promise { + const fragmentDefinitions = await this._graphQLCache.getFragmentDefinitions( + projectConfig, + ); + + const dependencies = await this._graphQLCache.getFragmentDependenciesForAST( + ast, + fragmentDefinitions, + ); + + const localFragDefinitions = ast.definitions.filter( + definition => definition.kind === Kind.FRAGMENT_DEFINITION, + ); + + const typeCastedDefs = + localFragDefinitions as any as Array; + + const localFragInfos = typeCastedDefs.map( + (definition: FragmentDefinitionNode) => ({ + filePath, + content: query, + definition, + }), + ); + + const result = await getDefinitionQueryResultForFragmentSpread( + query, + node, + dependencies.concat(localFragInfos), + ); + + return result; + } + async getOutline(documentText: string): Promise { + return getOutline(documentText); + } +} diff --git a/packages/graphql-language-service-server/src/Logger.ts b/packages/graphql-language-service-server/src/Logger.ts new file mode 100644 index 00000000000..ccc58defa81 --- /dev/null +++ b/packages/graphql-language-service-server/src/Logger.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { Logger as VSCodeLogger } from 'vscode-jsonrpc'; +import { Connection } from 'vscode-languageserver'; + +export class Logger implements VSCodeLogger { + constructor(private _connection: Connection) {} + + error(message: string): void { + this._connection.console.error(message); + } + + warn(message: string): void { + this._connection.console.warn(message); + } + + info(message: string): void { + this._connection.console.info(message); + } + + log(message: string): void { + this._connection.console.log(message); + } +} + +export class NoopLogger implements VSCodeLogger { + error() {} + warn() {} + info() {} + log() {} +} diff --git a/packages/graphql-language-service-server/src/MessageProcessor.ts b/packages/graphql-language-service-server/src/MessageProcessor.ts new file mode 100644 index 00000000000..007d78f862f --- /dev/null +++ b/packages/graphql-language-service-server/src/MessageProcessor.ts @@ -0,0 +1,1258 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import mkdirp from 'mkdirp'; +import { readFileSync, existsSync, writeFileSync } from 'node:fs'; +import { writeFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import glob from 'fast-glob'; +import { URI } from 'vscode-uri'; +import { + CachedContent, + Uri, + GraphQLConfig, + GraphQLProjectConfig, + FileChangeTypeKind, + Range, + Position, + IPosition, +} from 'graphql-language-service'; + +import { GraphQLLanguageService } from './GraphQLLanguageService'; + +import type { + CompletionParams, + FileEvent, + VersionedTextDocumentIdentifier, + DidSaveTextDocumentParams, + DidOpenTextDocumentParams, + DidChangeConfigurationParams, + Diagnostic, + CompletionItem, + CompletionList, + CancellationToken, + Hover, + InitializeResult, + Location, + PublishDiagnosticsParams, + DidChangeTextDocumentParams, + DidCloseTextDocumentParams, + DidChangeWatchedFilesParams, + InitializeParams, + Range as RangeType, + Position as VscodePosition, + TextDocumentPositionParams, + DocumentSymbolParams, + SymbolInformation, + WorkspaceSymbolParams, + Connection, + DidChangeConfigurationRegistrationOptions, + Logger, +} from 'vscode-languageserver/node'; + +import type { UnnormalizedTypeDefPointer } from '@graphql-tools/load'; + +import { getGraphQLCache, GraphQLCache } from './GraphQLCache'; +import { parseDocument, DEFAULT_SUPPORTED_EXTENSIONS } from './parseDocument'; + +import { printSchema, visit, parse, FragmentDefinitionNode } from 'graphql'; +import { tmpdir } from 'node:os'; +import { + ConfigEmptyError, + ConfigInvalidError, + ConfigNotFoundError, + GraphQLExtensionDeclaration, + LoaderNoResultError, + ProjectNotFoundError, +} from 'graphql-config'; +import type { LoadConfigOptions } from './types'; + +const configDocLink = + 'https://www.npmjs.com/package/graphql-language-service-server#user-content-graphql-configuration-file'; + +type CachedDocumentType = { + version: number; + contents: CachedContent[]; +}; +function toPosition(position: VscodePosition): IPosition { + return new Position(position.line, position.character); +} + +export class MessageProcessor { + _connection: Connection; + _graphQLCache!: GraphQLCache; + _graphQLConfig: GraphQLConfig | undefined; + _languageService!: GraphQLLanguageService; + _textDocumentCache = new Map(); + _isInitialized = false; + _isGraphQLConfigMissing: boolean | null = null; + _willShutdown = false; + _logger: Logger; + _extensions?: GraphQLExtensionDeclaration[]; + _parser: (text: string, uri: string) => CachedContent[]; + _tmpDir: string; + _tmpUriBase: string; + _tmpDirBase: string; + _loadConfigOptions: LoadConfigOptions; + _schemaCacheInit = false; + _rootPath: string = process.cwd(); + _settings: any; + + constructor({ + logger, + fileExtensions, + graphqlFileExtensions, + loadConfigOptions, + config, + parser, + tmpDir, + connection, + }: { + logger: Logger; + fileExtensions: string[]; + graphqlFileExtensions: string[]; + loadConfigOptions: LoadConfigOptions; + config?: GraphQLConfig; + parser?: typeof parseDocument; + tmpDir?: string; + connection: Connection; + }) { + this._connection = connection; + this._logger = logger; + this._graphQLConfig = config; + this._parser = (text, uri) => { + const p = parser ?? parseDocument; + return p(text, uri, fileExtensions, graphqlFileExtensions, this._logger); + }; + this._tmpDir = tmpDir || tmpdir(); + this._tmpDirBase = path.join(this._tmpDir, 'graphql-language-service'); + this._tmpUriBase = URI.file(this._tmpDirBase).toString(); + // use legacy mode by default for backwards compatibility + this._loadConfigOptions = { legacy: true, ...loadConfigOptions }; + if ( + loadConfigOptions.extensions && + loadConfigOptions.extensions?.length > 0 + ) { + this._extensions = loadConfigOptions.extensions; + } + + if (!existsSync(this._tmpDirBase)) { + void mkdirp(this._tmpDirBase); + } + } + get connection(): Connection { + return this._connection; + } + set connection(connection: Connection) { + this._connection = connection; + } + + async handleInitializeRequest( + params: InitializeParams, + _token?: CancellationToken, + configDir?: string, + ): Promise { + if (!params) { + throw new Error('`params` argument is required to initialize.'); + } + + const serverCapabilities: InitializeResult = { + capabilities: { + workspaceSymbolProvider: true, + documentSymbolProvider: true, + completionProvider: { + resolveProvider: true, + triggerCharacters: [' ', ':', '$', '(', '@'], + }, + definitionProvider: true, + textDocumentSync: 1, + hoverProvider: true, + workspace: { + workspaceFolders: { + supported: true, + changeNotifications: true, + }, + }, + }, + }; + + this._rootPath = configDir + ? configDir.trim() + : params.rootUri || this._rootPath; + if (!this._rootPath) { + this._logger.warn( + 'no rootPath configured in extension or server, defaulting to cwd', + ); + } + if (!serverCapabilities) { + throw new Error('GraphQL Language Server is not initialized.'); + } + + this._logger.info( + JSON.stringify({ + type: 'usage', + messageType: 'initialize', + }), + ); + + return serverCapabilities; + } + + async _updateGraphQLConfig() { + const settings = await this._connection.workspace.getConfiguration({ + section: 'graphql-config', + }); + const vscodeSettings = await this._connection.workspace.getConfiguration({ + section: 'vscode-graphql', + }); + if (settings?.dotEnvPath) { + require('dotenv').config({ path: settings.dotEnvPath }); + } + this._settings = { ...settings, ...vscodeSettings }; + const rootDir = this._settings?.load?.rootDir || this._rootPath; + this._rootPath = rootDir; + this._loadConfigOptions = { + ...Object.keys(this._settings?.load ?? {}).reduce((agg, key) => { + const value = this._settings?.load[key]; + if (value === undefined || value === null) { + delete agg[key]; + } + return agg; + }, this._settings.load ?? {}), + rootDir, + }; + try { + // reload the graphql cache + this._graphQLCache = await getGraphQLCache({ + parser: this._parser, + loadConfigOptions: this._loadConfigOptions, + + logger: this._logger, + }); + this._languageService = new GraphQLLanguageService( + this._graphQLCache, + this._logger, + ); + if (this._graphQLConfig || this._graphQLCache?.getGraphQLConfig) { + const config = + this._graphQLConfig ?? this._graphQLCache.getGraphQLConfig(); + await this._cacheAllProjectFiles(config); + } + this._isInitialized = true; + } catch (err) { + this._handleConfigError({ err }); + } + } + _handleConfigError({ err }: { err: unknown; uri?: string }) { + if (err instanceof ConfigNotFoundError || err instanceof ConfigEmptyError) { + // TODO: obviously this needs to become a map by workspace from uri + // for workspaces support + this._isGraphQLConfigMissing = true; + this._logConfigError(err.message); + } else if (err instanceof ProjectNotFoundError) { + // this is the only case where we don't invalidate config; + // TODO: per-project schema initialization status (PR is almost ready) + this._logConfigError( + 'Project not found for this file - make sure that a schema is present', + ); + } else if (err instanceof ConfigInvalidError) { + this._isGraphQLConfigMissing = true; + this._logConfigError(`Invalid configuration\n${err.message}`); + } else if (err instanceof LoaderNoResultError) { + this._isGraphQLConfigMissing = true; + this._logConfigError(err.message); + return; + } else { + // if it's another kind of error, + // lets just assume the config is missing and + // disable language features + this._isGraphQLConfigMissing = true; + this._logConfigError( + // @ts-expect-error + err?.message ?? err?.toString(), + ); + } + } + + _logConfigError(errorMessage: string) { + this._logger.error( + 'WARNING: graphql-config error, only highlighting is enabled:\n' + + errorMessage + + `\nfor more information on using 'graphql-config' with 'graphql-language-service-server', \nsee the documentation at ${configDocLink}`, + ); + } + + async handleDidOpenOrSaveNotification( + params: DidSaveTextDocumentParams | DidOpenTextDocumentParams, + ): Promise { + /** + * Initialize the LSP server when the first file is opened or saved, + * so that we can access the user settings for config rootDir, etc + */ + try { + if (!this._isInitialized || !this._graphQLCache) { + // don't try to initialize again if we've already tried + // and the graphql config file or package.json entry isn't even there + if (this._isGraphQLConfigMissing === true) { + return null; + } + // then initial call to update graphql config + await this._updateGraphQLConfig(); + } + } catch (err) { + this._logger.error(String(err)); + } + + // Here, we set the workspace settings in memory, + // and re-initialize the language service when a different + // root path is detected. + // We aren't able to use initialization event for this + // and the config change event is after the fact. + + if (!params?.textDocument) { + throw new Error('`textDocument` argument is required.'); + } + const { textDocument } = params; + const { uri } = textDocument; + + const diagnostics: Diagnostic[] = []; + + let contents: CachedContent[] = []; + const text = 'text' in textDocument && textDocument.text; + // Create/modify the cached entry if text is provided. + // Otherwise, try searching the cache to perform diagnostics. + if (text) { + // textDocument/didSave does not pass in the text content. + // Only run the below function if text is passed in. + contents = this._parser(text, uri); + + await this._invalidateCache(textDocument, uri, contents); + } else { + const configMatchers = [ + 'graphql.config', + 'graphqlrc', + 'graphqlconfig', + ].filter(Boolean); + if (this._settings?.load?.fileName) { + configMatchers.push(this._settings.load.fileName); + } + + const hasGraphQLConfigFile = configMatchers.some( + v => uri.match(v)?.length, + ); + const hasPackageGraphQLConfig = + uri.match('package.json')?.length && require(uri)?.graphql; + if (hasGraphQLConfigFile || hasPackageGraphQLConfig) { + this._logger.info('updating graphql config'); + await this._updateGraphQLConfig(); + return { uri, diagnostics: [] }; + } + return null; + } + if (!this._graphQLCache) { + return { uri, diagnostics }; + } + try { + const project = this._graphQLCache.getProjectForFile(uri); + if ( + this._isInitialized && + project?.extensions?.languageService?.enableValidation !== false + ) { + await Promise.all( + contents.map(async ({ query, range }) => { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), + ); + if (results && results.length > 0) { + diagnostics.push( + ...processDiagnosticsMessage(results, query, range), + ); + } + }), + ); + } + + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didOpenOrSave', + projectName: project?.name, + fileName: uri, + }), + ); + } catch (err) { + this._handleConfigError({ err, uri }); + } + + return { uri, diagnostics }; + } + + async handleDidChangeNotification( + params: DidChangeTextDocumentParams, + ): Promise { + if ( + this._isGraphQLConfigMissing || + !this._isInitialized || + !this._graphQLCache + ) { + return null; + } + // For every `textDocument/didChange` event, keep a cache of textDocuments + // with version information up-to-date, so that the textDocument contents + // may be used during performing language service features, + // e.g. auto-completions. + if (!params?.textDocument?.uri || !params.contentChanges) { + throw new Error( + '`textDocument.uri` and `contentChanges` arguments are required.', + ); + } + const { textDocument, contentChanges } = params; + const { uri } = textDocument; + const project = this._graphQLCache.getProjectForFile(uri); + try { + const contentChange = contentChanges.at(-1)!; + + // As `contentChanges` is an array, and we just want the + // latest update to the text, grab the last entry from the array. + + // If it's a .js file, try parsing the contents to see if GraphQL queries + // exist. If not found, delete from the cache. + const contents = this._parser(contentChange.text, uri); + // If it's a .graphql file, proceed normally and invalidate the cache. + await this._invalidateCache(textDocument, uri, contents); + + const cachedDocument = this._getCachedDocument(uri); + + if (!cachedDocument) { + return null; + } + + await this._updateFragmentDefinition(uri, contents); + await this._updateObjectTypeDefinition(uri, contents); + + const diagnostics: Diagnostic[] = []; + + if (project?.extensions?.languageService?.enableValidation !== false) { + // Send the diagnostics onChange as well + await Promise.all( + contents.map(async ({ query, range }) => { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), + ); + if (results && results.length > 0) { + diagnostics.push( + ...processDiagnosticsMessage(results, query, range), + ); + } + }), + ); + } + + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didChange', + projectName: project?.name, + fileName: uri, + }), + ); + + return { uri, diagnostics }; + } catch (err) { + this._handleConfigError({ err, uri }); + return { uri, diagnostics: [] }; + } + } + async handleDidChangeConfiguration( + _params: DidChangeConfigurationParams, + ): Promise { + await this._updateGraphQLConfig(); + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'workspace/didChangeConfiguration', + }), + ); + return {}; + } + + handleDidCloseNotification(params: DidCloseTextDocumentParams): void { + if (!this._isInitialized || !this._graphQLCache) { + return; + } + // For every `textDocument/didClose` event, delete the cached entry. + // This is to keep a low memory usage && switch the source of truth to + // the file on disk. + if (!params?.textDocument) { + throw new Error('`textDocument` is required.'); + } + const { textDocument } = params; + const { uri } = textDocument; + + if (this._textDocumentCache.has(uri)) { + this._textDocumentCache.delete(uri); + } + const project = this._graphQLCache.getProjectForFile(uri); + + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/didClose', + projectName: project?.name, + fileName: uri, + }), + ); + } + + handleShutdownRequest(): void { + this._willShutdown = true; + } + + handleExitNotification(): void { + process.exit(this._willShutdown ? 0 : 1); + } + + validateDocumentAndPosition(params: CompletionParams): void { + if (!params?.textDocument?.uri || !params.position) { + throw new Error( + '`textDocument.uri` and `position` arguments are required.', + ); + } + } + + async handleCompletionRequest( + params: CompletionParams, + ): Promise> { + if (!this._isInitialized || !this._graphQLCache) { + return []; + } + + this.validateDocumentAndPosition(params); + + const { textDocument, position } = params; + + // `textDocument/completion` event takes advantage of the fact that + // `textDocument/didChange` event always fires before, which would have + // updated the cache with the query text from the editor. + // Treat the computed list always complete. + + const cachedDocument = this._getCachedDocument(textDocument.uri); + if (!cachedDocument) { + return []; + } + + const found = cachedDocument.contents.find(content => { + const currentRange = content.range; + if (currentRange?.containsPosition(toPosition(position))) { + return true; + } + }); + + // If there is no GraphQL query in this file, return an empty result. + if (!found) { + return []; + } + + const { query, range } = found; + + if (range) { + position.line -= range.start.line; + } + const result = await this._languageService.getAutocompleteSuggestions( + query, + toPosition(position), + textDocument.uri, + ); + + const project = this._graphQLCache.getProjectForFile(textDocument.uri); + + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/completion', + projectName: project?.name, + fileName: textDocument.uri, + }), + ); + + return { items: result, isIncomplete: false }; + } + + async handleHoverRequest(params: TextDocumentPositionParams): Promise { + if (!this._isInitialized || !this._graphQLCache) { + return { contents: [] }; + } + + this.validateDocumentAndPosition(params); + + const { textDocument, position } = params; + + const cachedDocument = this._getCachedDocument(textDocument.uri); + if (!cachedDocument) { + return { contents: [] }; + } + + const found = cachedDocument.contents.find(content => { + const currentRange = content.range; + if (currentRange?.containsPosition(toPosition(position))) { + return true; + } + }); + + // If there is no GraphQL query in this file, return an empty result. + if (!found) { + return { contents: [] }; + } + + const { query, range } = found; + + if (range) { + position.line -= range.start.line; + } + const result = await this._languageService.getHoverInformation( + query, + toPosition(position), + textDocument.uri, + { useMarkdown: true }, + ); + + return { + contents: result, + }; + } + + async handleWatchedFilesChangedNotification( + params: DidChangeWatchedFilesParams, + ): Promise | null> { + if ( + this._isGraphQLConfigMissing || + !this._isInitialized || + !this._graphQLCache + ) { + return null; + } + + return Promise.all( + params.changes.map(async (change: FileEvent) => { + if ( + this._isGraphQLConfigMissing || + !this._isInitialized || + !this._graphQLCache + ) { + this._logger.warn('No cache available for handleWatchedFilesChanged'); + return; + } + if ( + change.type === FileChangeTypeKind.Created || + change.type === FileChangeTypeKind.Changed + ) { + const { uri } = change; + + const text = readFileSync(URI.parse(uri).fsPath, 'utf-8'); + const contents = this._parser(text, uri); + + await this._updateFragmentDefinition(uri, contents); + await this._updateObjectTypeDefinition(uri, contents); + + const project = this._graphQLCache.getProjectForFile(uri); + await this._updateSchemaIfChanged(project, uri); + + let diagnostics: Diagnostic[] = []; + + if ( + project?.extensions?.languageService?.enableValidation !== false + ) { + diagnostics = ( + await Promise.all( + contents.map(async ({ query, range }) => { + const results = await this._languageService.getDiagnostics( + query, + uri, + this._isRelayCompatMode(query), + ); + if (results && results.length > 0) { + return processDiagnosticsMessage(results, query, range); + } + return []; + }), + ) + ).reduce((left, right) => left.concat(right), diagnostics); + } + + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'workspace/didChangeWatchedFiles', + projectName: project?.name, + fileName: uri, + }), + ); + return { uri, diagnostics }; + } + if (change.type === FileChangeTypeKind.Deleted) { + await this._graphQLCache.updateFragmentDefinitionCache( + this._graphQLCache.getGraphQLConfig().dirpath, + change.uri, + false, + ); + await this._graphQLCache.updateObjectTypeDefinitionCache( + this._graphQLCache.getGraphQLConfig().dirpath, + change.uri, + false, + ); + } + }), + ); + } + + async handleDefinitionRequest( + params: TextDocumentPositionParams, + _token?: CancellationToken, + ): Promise> { + if (!this._isInitialized || !this._graphQLCache) { + return []; + } + + if (!params?.textDocument || !params.position) { + throw new Error('`textDocument` and `position` arguments are required.'); + } + const { textDocument, position } = params; + const project = this._graphQLCache.getProjectForFile(textDocument.uri); + if (project) { + await this._cacheSchemaFilesForProject(project); + } + const cachedDocument = this._getCachedDocument(textDocument.uri); + if (!cachedDocument) { + return []; + } + + const found = cachedDocument.contents.find(content => { + const currentRange = content.range; + if (currentRange?.containsPosition(toPosition(position))) { + return true; + } + }); + + // If there is no GraphQL query in this file, return an empty result. + if (!found) { + return []; + } + + const { query, range: parentRange } = found; + if (parentRange) { + position.line -= parentRange.start.line; + } + + let result = null; + + try { + result = await this._languageService.getDefinition( + query, + toPosition(position), + textDocument.uri, + ); + } catch { + // these thrown errors end up getting fired before the service is initialized, so lets cool down on that + } + + const inlineFragments: string[] = []; + try { + visit(parse(query), { + FragmentDefinition(node: FragmentDefinitionNode) { + inlineFragments.push(node.name.value); + }, + }); + } catch {} + + const formatted = result + ? result.definitions.map(res => { + const defRange = res.range as Range; + + if (parentRange && res.name) { + const isInline = inlineFragments.includes(res.name); + const isEmbedded = DEFAULT_SUPPORTED_EXTENSIONS.includes( + path.extname(textDocument.uri), + ); + if (isInline && isEmbedded) { + const vOffset = parentRange.start.line; + defRange.setStart( + (defRange.start.line += vOffset), + defRange.start.character, + ); + defRange.setEnd( + (defRange.end.line += vOffset), + defRange.end.character, + ); + } + } + return { + uri: res.path, + range: defRange, + } as Location; + }) + : []; + + this._logger.log( + JSON.stringify({ + type: 'usage', + messageType: 'textDocument/definition', + projectName: project?.name, + fileName: textDocument.uri, + }), + ); + return formatted; + } + + async handleDocumentSymbolRequest( + params: DocumentSymbolParams, + ): Promise> { + if (!this._isInitialized || !this._graphQLCache) { + return []; + } + + if (!params?.textDocument) { + throw new Error('`textDocument` argument is required.'); + } + + const { textDocument } = params; + const cachedDocument = this._getCachedDocument(textDocument.uri); + if (!cachedDocument?.contents[0]) { + return []; + } + + return this._languageService.getDocumentSymbols( + cachedDocument.contents[0].query, + textDocument.uri, + ); + } + + // async handleReferencesRequest(params: ReferenceParams): Promise { + // if (!this._isInitialized) { + // return []; + // } + + // if (!params?.textDocument) { + // throw new Error('`textDocument` argument is required.'); + // } + + // const textDocument = params.textDocument; + // const cachedDocument = this._getCachedDocument(textDocument.uri); + // if (!cachedDocument) { + // throw new Error('A cached document cannot be found.'); + // } + // return this._languageService.getReferences( + // cachedDocument.contents[0].query, + // params.position, + // textDocument.uri, + // ); + // } + + async handleWorkspaceSymbolRequest( + params: WorkspaceSymbolParams, + ): Promise> { + if (!this._isInitialized || !this._graphQLCache) { + return []; + } + // const config = await this._graphQLCache.getGraphQLConfig(); + // await this._cacheAllProjectFiles(config); + + if (params.query !== '') { + const documents = this._getTextDocuments(); + const symbols: SymbolInformation[] = []; + await Promise.all( + documents.map(async ([uri]) => { + const cachedDocument = this._getCachedDocument(uri); + if (!cachedDocument) { + return []; + } + const docSymbols = await this._languageService.getDocumentSymbols( + cachedDocument.contents[0].query, + uri, + ); + symbols.push(...docSymbols); + }), + ); + return symbols.filter( + symbol => symbol?.name && symbol.name.includes(params.query), + ); + } + + return []; + } + + _getTextDocuments() { + return Array.from(this._textDocumentCache); + } + + async _cacheSchemaText(uri: string, text: string, version: number) { + try { + const contents = this._parser(text, uri); + if (contents.length > 0) { + await this._invalidateCache({ version, uri }, uri, contents); + await this._updateObjectTypeDefinition(uri, contents); + } + } catch (err) { + this._logger.error(String(err)); + } + } + async _cacheSchemaFile( + _uri: UnnormalizedTypeDefPointer, + project: GraphQLProjectConfig, + ) { + const uri = _uri.toString(); + + const isFileUri = existsSync(uri); + let version = 1; + if (isFileUri) { + const schemaUri = URI.file(path.join(project.dirpath, uri)).toString(); + const schemaDocument = this._getCachedDocument(schemaUri); + + if (schemaDocument) { + version = schemaDocument.version++; + } + const schemaText = readFileSync(uri, 'utf8'); + await this._cacheSchemaText(schemaUri, schemaText, version); + } + } + _getTmpProjectPath( + project: GraphQLProjectConfig, + prependWithProtocol = true, + appendPath?: string, + ) { + const baseDir = this._graphQLCache.getGraphQLConfig().dirpath; + const workspaceName = path.basename(baseDir); + const basePath = path.join(this._tmpDirBase, workspaceName); + let projectTmpPath = path.join(basePath, 'projects', project.name); + if (!existsSync(projectTmpPath)) { + void mkdirp(projectTmpPath); + } + if (appendPath) { + projectTmpPath = path.join(projectTmpPath, appendPath); + } + if (prependWithProtocol) { + return URI.file(path.resolve(projectTmpPath)).toString(); + } + return path.resolve(projectTmpPath); + } + /** + * Safely attempts to cache schema files based on a glob or path + * Exits without warning in several cases because these strings can be almost + * anything! + * @param uri + * @param project + */ + async _cacheSchemaPath(uri: string, project: GraphQLProjectConfig) { + try { + const files = await glob(uri); + if (files && files.length > 0) { + await Promise.all( + files.map(uriPath => this._cacheSchemaFile(uriPath, project)), + ); + } else { + try { + await this._cacheSchemaFile(uri, project); + } catch { + // this string may be an SDL string even, how do we even evaluate this? + } + } + } catch {} + } + async _cacheObjectSchema( + pointer: { [key: string]: any }, + project: GraphQLProjectConfig, + ) { + await Promise.all( + Object.keys(pointer).map(async schemaUri => + this._cacheSchemaPath(schemaUri, project), + ), + ); + } + async _cacheArraySchema( + pointers: UnnormalizedTypeDefPointer[], + project: GraphQLProjectConfig, + ) { + await Promise.all( + pointers.map(async schemaEntry => { + if (typeof schemaEntry === 'string') { + await this._cacheSchemaPath(schemaEntry, project); + } else if (schemaEntry) { + await this._cacheObjectSchema(schemaEntry, project); + } + }), + ); + } + + async _cacheSchemaFilesForProject(project: GraphQLProjectConfig) { + const schema = project?.schema; + const config = project?.extensions?.languageService; + /** + * By default, we look for schema definitions in SDL files + * + * with the opt-in feature `cacheSchemaOutputFileForLookup` enabled, + * the resultant `graphql-config` .getSchema() schema output will be cached + * locally and available as a single file for definition lookup and peek + * + * this is helpful when your `graphql-config` `schema` input is: + * - a remote or local URL + * - compiled from graphql files and code sources + * - otherwise where you don't have schema SDL in the codebase or don't want to use it for lookup + * + * it is disabled by default + */ + const cacheSchemaFileForLookup = + config?.cacheSchemaFileForLookup ?? + this?._settings?.cacheSchemaFileForLookup ?? + false; + if (cacheSchemaFileForLookup) { + await this._cacheConfigSchema(project); + } else if (typeof schema === 'string') { + await this._cacheSchemaPath(schema, project); + } else if (Array.isArray(schema)) { + await this._cacheArraySchema(schema, project); + } else if (schema) { + await this._cacheObjectSchema(schema, project); + } + } + /** + * Cache the schema as represented by graphql-config, with extensions + * from GraphQLCache.getSchema() + * @param project {GraphQLProjectConfig} + */ + async _cacheConfigSchema(project: GraphQLProjectConfig) { + try { + const schema = await this._graphQLCache.getSchema(project.name); + if (schema) { + let schemaText = printSchema(schema); + // file:// protocol path + const uri = this._getTmpProjectPath( + project, + true, + 'generated-schema.graphql', + ); + + // no file:// protocol for fs.writeFileSync() + const fsPath = this._getTmpProjectPath( + project, + false, + 'generated-schema.graphql', + ); + schemaText = `# This is an automatically generated representation of your schema.\n# Any changes to this file will be overwritten and will not be\n# reflected in the resulting GraphQL schema\n\n${schemaText}`; + + const cachedSchemaDoc = this._getCachedDocument(uri); + + if (!cachedSchemaDoc) { + await writeFile(fsPath, schemaText, 'utf8'); + await this._cacheSchemaText(uri, schemaText, 1); + } + // do we have a change in the getSchema result? if so, update schema cache + if (cachedSchemaDoc) { + writeFileSync(fsPath, schemaText, 'utf8'); + await this._cacheSchemaText( + uri, + schemaText, + cachedSchemaDoc.version++, + ); + } + } + } catch (err) { + this._logger.error(String(err)); + } + } + /** + * Pre-cache all documents for a project. + * + * TODO: Maybe make this optional, where only schema needs to be pre-cached. + * + * @param project {GraphQLProjectConfig} + */ + async _cacheDocumentFilesforProject(project: GraphQLProjectConfig) { + try { + const documents = await project.getDocuments(); + return Promise.all( + documents.map(async document => { + if (!document.location || !document.rawSDL) { + return; + } + + let filePath = document.location; + if (!path.isAbsolute(filePath)) { + filePath = path.join(project.dirpath, document.location); + } + + // build full system URI path with protocol + const uri = URI.file(filePath).toString(); + + // I would use the already existing graphql-config AST, but there are a few reasons we can't yet + const contents = this._parser(document.rawSDL, uri); + if (!contents[0]?.query) { + return; + } + await this._updateObjectTypeDefinition(uri, contents); + await this._updateFragmentDefinition(uri, contents); + await this._invalidateCache({ version: 1, uri }, uri, contents); + }), + ); + } catch (err) { + this._logger.error( + `invalid/unknown file in graphql config documents entry:\n '${project.documents}'`, + ); + this._logger.error(String(err)); + } + } + /** + * This should only be run on initialize() really. + * Caching all the document files upfront could be expensive. + * @param config {GraphQLConfig} + */ + async _cacheAllProjectFiles(config: GraphQLConfig) { + if (config?.projects) { + return Promise.all( + Object.keys(config.projects).map(async projectName => { + const project = config.getProject(projectName); + await this._cacheSchemaFilesForProject(project); + await this._cacheDocumentFilesforProject(project); + }), + ); + } + } + _isRelayCompatMode(query: string): boolean { + return ( + query.includes('RelayCompat') || query.includes('react-relay/compat') + ); + } + + async _updateFragmentDefinition( + uri: Uri, + contents: CachedContent[], + ): Promise { + const rootDir = this._graphQLCache.getGraphQLConfig().dirpath; + + await this._graphQLCache.updateFragmentDefinition(rootDir, uri, contents); + } + + async _updateSchemaIfChanged( + project: GraphQLProjectConfig, + uri: Uri, + ): Promise { + await Promise.all( + this._unwrapProjectSchema(project).map(async schema => { + const schemaFilePath = path.resolve(project.dirpath, schema); + const uriFilePath = URI.parse(uri).fsPath; + if (uriFilePath === schemaFilePath) { + await this._graphQLCache.invalidateSchemaCacheForProject(project); + } + }), + ); + } + + _unwrapProjectSchema(project: GraphQLProjectConfig): string[] { + const projectSchema = project.schema; + + const schemas: string[] = []; + if (typeof projectSchema === 'string') { + schemas.push(projectSchema); + } else if (Array.isArray(projectSchema)) { + for (const schemaEntry of projectSchema) { + if (typeof schemaEntry === 'string') { + schemas.push(schemaEntry); + } else if (schemaEntry) { + schemas.push(...Object.keys(schemaEntry)); + } + } + } else { + schemas.push(...Object.keys(projectSchema)); + } + + return schemas; + } + + async _updateObjectTypeDefinition( + uri: Uri, + contents: CachedContent[], + ): Promise { + const rootDir = this._graphQLCache.getGraphQLConfig().dirpath; + + await this._graphQLCache.updateObjectTypeDefinition(rootDir, uri, contents); + } + + _getCachedDocument(uri: string): CachedDocumentType | null { + if (this._textDocumentCache.has(uri)) { + const cachedDocument = this._textDocumentCache.get(uri); + if (cachedDocument) { + return cachedDocument; + } + } + + return null; + } + async _invalidateCache( + textDocument: VersionedTextDocumentIdentifier, + uri: Uri, + contents: CachedContent[], + ): Promise | null> { + if (this._textDocumentCache.has(uri)) { + const cachedDocument = this._textDocumentCache.get(uri); + if ( + cachedDocument && + textDocument && + textDocument?.version && + cachedDocument.version < textDocument.version + ) { + // Current server capabilities specify the full sync of the contents. + // Therefore always overwrite the entire content. + return this._textDocumentCache.set(uri, { + version: textDocument.version, + contents, + }); + } + } + return this._textDocumentCache.set(uri, { + version: textDocument.version ?? 0, + contents, + }); + } +} + +function processDiagnosticsMessage( + results: Diagnostic[], + query: string, + range: RangeType | null, +): Diagnostic[] { + const queryLines = query.split('\n'); + const totalLines = queryLines.length; + const lastLineLength = queryLines[totalLines - 1].length; + const lastCharacterPosition = new Position(totalLines, lastLineLength); + const processedResults = results.filter(diagnostic => + // @ts-ignore + diagnostic.range.end.lessThanOrEqualTo(lastCharacterPosition), + ); + + if (range) { + const offset = range.start; + return processedResults.map(diagnostic => ({ + ...diagnostic, + range: new Range( + new Position( + diagnostic.range.start.line + offset.line, + diagnostic.range.start.character, + ), + new Position( + diagnostic.range.end.line + offset.line, + diagnostic.range.end.character, + ), + ), + })); + } + + return processedResults; +} diff --git a/packages/graphql-language-service-server/src/__tests__/.graphqlrc.yml b/packages/graphql-language-service-server/src/__tests__/.graphqlrc.yml new file mode 100644 index 00000000000..40af8518ed9 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/.graphqlrc.yml @@ -0,0 +1,34 @@ +projects: + testWithSchema: + schema: + - __schema__/StarWarsSchema.graphql + - 'directive @customDirective on FRAGMENT_SPREAD' + testWithGlobSchema: + schema: + - __schema__/*.graphql + - 'directive @customDirective on FRAGMENT_SPREAD' + testWithEndpoint: + schema: https://example.com/graphql + testWithEndpointAndSchema: + schema: + - __schema__/StarWarsSchema.graphql + - https://example.com/graphql + testWithoutSchema: + schema: '' + testWithCustomDirectives: + schema: + - __schema__/StarWarsSchema.graphql + - 'directive @customDirective on FIELD' + testSingularIncludesGlob: + schema: __schema__/StarWarsSchema.graphql + documents: __queries__/*.graphql + testMultipleIncludes: + schema: __schema__/StarWarsSchema.graphql + documents: + - __queries__/*.graphql + - __fragments__/*.graphql + testNoIncludes: + schema: __schema__/StarWarsSchema.graphql + testBadIncludes: + schema: __schema__/StarWarsSchema.graphql + documents: nope.nopeql diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts new file mode 100644 index 00000000000..54082249e3f --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLCache-test.ts @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { AbortController as MockAbortController } from 'node-abort-controller'; +import fetchMock from 'fetch-mock'; + +jest.mock('@whatwg-node/fetch', () => ({ + fetch: require('fetch-mock').fetchHandler, + AbortController: MockAbortController, + TextDecoder: global.TextDecoder, +})); + +import { loadConfig, GraphQLExtensionDeclaration } from 'graphql-config'; +import { + GraphQLSchema, + parse, + introspectionFromSchema, + FragmentDefinitionNode, + TypeDefinitionNode, +} from 'graphql'; +import { GraphQLCache, getGraphQLCache } from '../GraphQLCache'; +import { parseDocument } from '../parseDocument'; +import type { FragmentInfo, ObjectTypeInfo } from 'graphql-language-service'; +import { NoopLogger } from '../Logger'; + +function withoutASTNode(definition: any) { + const result = { ...definition }; + delete result.astNode; + return result; +} + +const logger = new NoopLogger(); +describe('GraphQLCache', () => { + const configDir = __dirname; + let graphQLRC; + let cache = new GraphQLCache({ + configDir, + config: graphQLRC, + parser: parseDocument, + logger, + }); + + beforeEach(async () => { + graphQLRC = await loadConfig({ rootDir: configDir }); + cache = new GraphQLCache({ + configDir, + config: graphQLRC, + parser: parseDocument, + logger, + }); + }); + + afterEach(() => { + fetchMock.restore(); + }); + + describe('getGraphQLCache', () => { + it('should apply extensions', async () => { + const extension: GraphQLExtensionDeclaration = _config => { + return { + name: 'extension-used', // Just adding a key to the config to demo extension usage + }; + }; + const extensions = [extension]; + const cacheWithExtensions = await getGraphQLCache({ + loadConfigOptions: { rootDir: configDir, extensions }, + parser: parseDocument, + logger, + }); + const config = cacheWithExtensions.getGraphQLConfig(); + expect('extensions' in config).toBe(true); + expect(config.extensions.has('extension-used')).toBeTruthy(); + expect(config.extensions.get('extension-used')).toEqual({ + name: 'extension-used', + }); + }); + }); + + describe('getSchema', () => { + it('generates the schema correctly for the test app config', async () => { + const schema = await cache.getSchema('testWithSchema'); + expect(schema instanceof GraphQLSchema).toEqual(true); + }); + + it('generates the schema correctly from endpoint', async () => { + const introspectionResult = { + data: introspectionFromSchema( + await graphQLRC.getProject('testWithSchema').getSchema(), + { descriptions: true }, + ), + }; + fetchMock.mock({ + matcher: '*', + response: { + headers: { + 'Content-Type': 'application/json', + }, + body: introspectionResult, + }, + }); + + const schema = await cache.getSchema('testWithEndpoint'); + expect(fetchMock.called('*')).toEqual(true); + expect(schema instanceof GraphQLSchema).toEqual(true); + }); + + it('does not generate a schema without a schema path or endpoint', async () => { + const schema = await cache.getSchema('testWithoutSchema'); + expect(schema instanceof GraphQLSchema).toEqual(false); + }); + + it('extend the schema with appropriate custom directive', async () => { + const schema = await cache.getSchema('testWithCustomDirectives'); + expect(withoutASTNode(schema.getDirective('customDirective'))).toEqual( + // objectContaining is used to pass this test without changing the code if more properties are added in GraphQLDirective class in the new version of graphql module. + expect.objectContaining({ + args: [], + description: undefined, + isRepeatable: false, + locations: ['FIELD'], + name: 'customDirective', + }), + ); + }); + + it('extend the schema with appropriate custom directive 2', async () => { + const schema = await cache.getSchema('testWithSchema'); + expect(withoutASTNode(schema.getDirective('customDirective'))).toEqual( + // objectContaining is used to pass this test without changing the code if more properties are added in GraphQLDirective class in the new version of graphql module. + expect.objectContaining({ + args: [], + description: undefined, + isRepeatable: false, + locations: ['FRAGMENT_SPREAD'], + name: 'customDirective', + }), + ); + }); + }); + + describe('getFragmentDependencies', () => { + const duckContent = `fragment Duck on Duck { + quack + }`; + const duckDefinition = parse(duckContent).definitions[0]; + + const catContent = `fragment Cat on Cat { + meow + }`; + + const catDefinition = parse(catContent).definitions[0]; + + const fragmentDefinitions = new Map(); + fragmentDefinitions.set('Duck', { + file: 'someFilePath', + content: duckContent, + definition: duckDefinition, + } as FragmentInfo); + fragmentDefinitions.set('Cat', { + file: 'someOtherFilePath', + content: catContent, + definition: catDefinition as FragmentDefinitionNode, + } as FragmentInfo); + + it('finds fragments referenced in Relay queries', async () => { + const text = + 'module.exports = Relay.createContainer(' + + 'DispatchResumeCard, {\n' + + ' fragments: {\n' + + ' candidate: () => graphql`\n' + + ' query A { ...Duck ...Cat }\n' + + ' `,\n' + + ' },\n' + + '});'; + const contents = parseDocument(text, 'test.js'); + const result = await cache.getFragmentDependenciesForAST( + parse(contents[0].query), + fragmentDefinitions, + ); + expect(result.length).toEqual(2); + }); + + it('finds fragments referenced from the query', async () => { + const ast = parse('query A { ...Duck }'); + + const result = await cache.getFragmentDependenciesForAST( + ast, + fragmentDefinitions, + ); + expect(result.length).toEqual(1); + }); + }); + + describe('getFragmentDefinitions', () => { + it('caches fragments found through single glob in `documents`', async () => { + const config = graphQLRC.getProject('testSingularIncludesGlob'); + const fragmentDefinitions = await cache.getFragmentDefinitions(config); + expect(fragmentDefinitions.get('testFragment')).not.toBeUndefined(); + }); + + it('caches fragments found through multiple globs in `documents`', async () => { + const config = graphQLRC.getProject('testMultipleIncludes'); + const fragmentDefinitions = await cache.getFragmentDefinitions(config); + expect(fragmentDefinitions.get('testFragment')).not.toBeUndefined(); + }); + + it('handles empty documents', async () => { + const config = graphQLRC.getProject('testNoIncludes'); + const fragmentDefinitions = await cache.getFragmentDefinitions(config); + expect(fragmentDefinitions.get('testFragment')).toBeUndefined(); + }); + + it('handles non-existent documents', async () => { + const config = graphQLRC.getProject('testBadIncludes'); + const fragmentDefinitions = await cache.getFragmentDefinitions(config); + expect(fragmentDefinitions.get('testFragment')).toBeUndefined(); + }); + }); + + describe('getNamedTypeDependencies', () => { + const query = `type Query { + hero(episode: Episode): Character + } + + type Episode { + id: ID! + } + `; + const parsedQuery = parse(query); + + const namedTypeDefinitions = new Map(); + namedTypeDefinitions.set('Character', { + file: 'someOtherFilePath', + content: query, + definition: { + kind: 'ObjectTypeDefinition', + name: { + kind: 'Name', + value: 'Character', + }, + loc: { + start: 0, + end: 0, + }, + } as TypeDefinitionNode, + } as ObjectTypeInfo); + + it('finds named types referenced from the SDL', async () => { + const result = await cache.getObjectTypeDependenciesForAST( + parsedQuery, + namedTypeDefinitions, + ); + expect(result.length).toEqual(1); + }); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts new file mode 100644 index 00000000000..0283c277174 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/GraphQLLanguageService-test.ts @@ -0,0 +1,224 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { join } from 'node:path'; + +import { GraphQLConfig } from 'graphql-config'; +import { GraphQLLanguageService } from '../GraphQLLanguageService'; +import { SymbolKind } from 'vscode-languageserver-protocol'; +import { Position } from 'graphql-language-service'; +import { NoopLogger } from '../Logger'; + +const MOCK_CONFIG = { + filepath: join(__dirname, '.graphqlrc.yml'), + config: { + schema: './__schema__/StarWarsSchema.graphql', + documents: ['./queries/**', '**/*.graphql'], + }, +}; + +describe('GraphQLLanguageService', () => { + const mockCache = { + async getSchema() { + const config = this.getGraphQLConfig(); + return config.getDefault()!.getSchema(); + }, + + getGraphQLConfig() { + return new GraphQLConfig(MOCK_CONFIG, []); + }, + + getProjectForFile(uri: string) { + return this.getGraphQLConfig().getProjectForFile(uri); + }, + + getObjectTypeDefinitions() { + const definitions = new Map(); + + definitions.set('Episode', { + filePath: 'fake file path', + content: 'fake file content', + definition: { + name: { + value: 'Episode', + }, + + loc: { + start: 293, + end: 335, + }, + }, + }); + + definitions.set('Human', { + filePath: 'fake file path', + content: 'fake file content', + definition: { + name: { + value: 'Human', + }, + + fields: [ + { + name: { value: 'name' }, + loc: { + start: 293, + end: 335, + }, + }, + ], + + loc: { + start: 293, + end: 335, + }, + }, + }); + + return definitions; + }, + + getObjectTypeDependenciesForAST() { + return [ + { + filePath: 'fake file path', + content: 'fake file content', + definition: { + name: { + value: 'Episode', + }, + + loc: { + start: 293, + end: 335, + }, + }, + }, + { + filePath: 'fake file path', + content: 'fake file content', + definition: { + name: { + value: 'Human', + }, + + loc: { + start: 293, + end: 335, + }, + }, + }, + ]; + }, + }; + + let languageService: GraphQLLanguageService; + beforeEach(() => { + languageService = new GraphQLLanguageService( + mockCache as any, + new NoopLogger(), + ); + }); + + it('runs diagnostic service as expected', async () => { + const diagnostics = await languageService.getDiagnostics( + 'invalidKeyword', + './queries/testQuery.graphql', + ); + expect(diagnostics.length).toEqual(1); + const diagnostic = diagnostics[0]; + expect(diagnostic.message).toEqual( + 'Syntax Error: Unexpected Name "invalidKeyword".', + ); + }); + + it('avoids reporting validation errors when not enough characters are present', async () => { + const diagnostics = await languageService.getDiagnostics( + ' \n \n \n\n', + './queries/testQuery.graphql', + ); + expect(diagnostics.length).toEqual(0); + }); + + it('still reports errors on empty anonymous op', async () => { + const diagnostics = await languageService.getDiagnostics( + ' \n {\n \n}\n\n', + './queries/testQuery.graphql', + ); + expect(diagnostics.length).toEqual(1); + expect(diagnostics[0].message).toEqual( + 'Syntax Error: Expected Name, found "}".', + ); + }); + + it('runs definition service as expected', async () => { + const definitionQueryResult = await languageService.getDefinition( + 'type Query { hero(episode: Episode): Character }', + { line: 0, character: 28 } as Position, + './queries/definitionQuery.graphql', + ); + expect(definitionQueryResult?.definitions.length).toEqual(1); + }); + + it('runs definition service on field as expected', async () => { + const definitionQueryResult = await languageService.getDefinition( + 'query XXX { human { name } }', + { line: 0, character: 21 } as Position, + './queries/definitionQuery.graphql', + ); + expect(definitionQueryResult?.definitions.length).toEqual(1); + }); + + it('can find a definition for a union', async () => { + const query = + 'union X = A | B\ntype A { x: String }\ntype B { x: String }\ntype Query { a: X }'; + const definitionQueryResult = await languageService.getDefinition( + query, + { line: 3, character: 16 } as Position, + './queries/definitionQuery.graphql', + ); + expect(definitionQueryResult?.definitions.length).toEqual(1); + }); + + it('runs hover service as expected', async () => { + const hoverInformation = await languageService.getHoverInformation( + 'type Query { hero(episode: String): String }', + { line: 0, character: 28 } as Position, + './queries/definitionQuery.graphql', + ); + expect(hoverInformation).toEqual( + 'String\n\nThe `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.', + ); + }); + + it('runs document symbol requests as expected', async () => { + const validQuery = ` + query OperationExample { + item(episode: EMPIRE){ + ...testFragment + } + } + `; + + const result = await languageService.getDocumentSymbols( + validQuery, + 'file://file.graphql', + ); + + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(3); + // expect(result[0].name).toEqual('item'); + expect(result[1].name).toEqual('item'); + expect(result[1].kind).toEqual(SymbolKind.Field); + expect(result[1].location.range.start.line).toEqual(2); + expect(result[1].location.range.start.character).toEqual(4); + expect(result[1].location.range.end.line).toEqual(4); + expect(result[1].location.range.end.character).toEqual(5); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts new file mode 100644 index 00000000000..16b639bb4f6 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor-test.ts @@ -0,0 +1,789 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ +import { SymbolKind } from 'vscode-languageserver'; +import { FileChangeType } from 'vscode-languageserver-protocol'; +import { Position, Range } from 'graphql-language-service'; + +import { MessageProcessor } from '../MessageProcessor'; +import { parseDocument } from '../parseDocument'; + +jest.mock('../Logger'); + +import { GraphQLCache } from '../GraphQLCache'; + +import { loadConfig } from 'graphql-config'; + +import type { DefinitionQueryResult, Outline } from 'graphql-language-service'; + +import { NoopLogger } from '../Logger'; +import { pathToFileURL } from 'node:url'; + +jest.mock('node:fs', () => ({ + ...jest.requireActual('fs'), + readFileSync: jest.fn(jest.requireActual('fs').readFileSync), +})); + +describe('MessageProcessor', () => { + const logger = new NoopLogger(); + const messageProcessor = new MessageProcessor({ + // @ts-ignore + connection: {}, + logger, + fileExtensions: ['js'], + graphqlFileExtensions: ['graphql'], + loadConfigOptions: { rootDir: __dirname }, + }); + + const queryPathUri = pathToFileURL(`${__dirname}/__queries__`); + const textDocumentTestString = ` + { + hero(episode: NEWHOPE){ + } + } + `; + + beforeEach(async () => { + const gqlConfig = await loadConfig({ rootDir: __dirname, extensions: [] }); + // loadConfig.mockRestore(); + messageProcessor._settings = { load: {} }; + messageProcessor._graphQLCache = new GraphQLCache({ + configDir: __dirname, + config: gqlConfig, + parser: parseDocument, + }); + messageProcessor._languageService = { + // @ts-ignore + getAutocompleteSuggestions(query, position, uri) { + return [{ label: `${query} at ${uri}` }]; + }, + // @ts-ignore + getDiagnostics(_query, _uri) { + return []; + }, + async getDocumentSymbols(_query: string, uri: string) { + return [ + { + name: 'item', + kind: SymbolKind.Field, + location: { + uri, + range: { + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }, + }, + }, + ]; + }, + async getOutline(_query: string): Promise { + return { + outlineTrees: [ + { + representativeName: 'item', + kind: 'Field', + startPosition: new Position(1, 2), + endPosition: new Position(1, 4), + children: [], + }, + ], + }; + }, + async getDefinition( + _query, + position, + uri, + ): Promise { + return { + queryRange: [new Range(position, position)], + definitions: [ + { + position, + path: uri, + }, + ], + }; + }, + }; + }); + + let getConfigurationReturnValue = {}; + // @ts-ignore + messageProcessor._connection = { + // @ts-ignore + get workspace() { + return { + async getConfiguration() { + return [getConfigurationReturnValue]; + }, + }; + }, + }; + + const initialDocument = { + textDocument: { + text: textDocumentTestString, + uri: `${queryPathUri}/test.graphql`, + version: 0, + }, + }; + + messageProcessor._isInitialized = true; + + it('initializes properly and opens a file', async () => { + const { capabilities } = await messageProcessor.handleInitializeRequest( + // @ts-ignore + { + rootPath: __dirname, + }, + null, + __dirname, + ); + expect(capabilities.definitionProvider).toEqual(true); + expect(capabilities.workspaceSymbolProvider).toEqual(true); + expect(capabilities.completionProvider.resolveProvider).toEqual(true); + expect(capabilities.textDocumentSync).toEqual(1); + }); + + it('runs completion requests properly', async () => { + const uri = `${queryPathUri}/test2.graphql`; + const query = 'test'; + messageProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query, + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + + const test = { + position: new Position(0, 0), + textDocument: { uri }, + }; + const result = await messageProcessor.handleCompletionRequest(test); + expect(result).toEqual({ + items: [{ label: `${query} at ${uri}` }], + isIncomplete: false, + }); + }); + + it('runs document symbol requests', async () => { + const uri = `${queryPathUri}/test3.graphql`; + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri, + version: 0, + }, + }; + + messageProcessor._textDocumentCache.set(uri, { + version: 0, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + + const test = { + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleDocumentSymbolRequest(test); + + expect(result).not.toBeUndefined(); + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('item'); + expect(result[0].kind).toEqual(SymbolKind.Field); + expect(result[0].location.range).toEqual({ + start: { line: 1, character: 2 }, + end: { line: 1, character: 4 }, + }); + }); + + it('properly changes the file cache with the didChange handler', async () => { + const uri = `${queryPathUri}/test.graphql`; + messageProcessor._textDocumentCache.set(uri, { + version: 1, + contents: [ + { + query: '', + range: new Range(new Position(0, 0), new Position(0, 0)), + }, + ], + }); + const textDocumentChangedString = ` + { + hero(episode: NEWHOPE){ + name + } + } + `; + + const result = await messageProcessor.handleDidChangeNotification({ + textDocument: { + // @ts-ignore + text: textDocumentTestString, + uri, + version: 1, + }, + contentChanges: [ + { text: textDocumentTestString }, + { text: textDocumentChangedString }, + ], + }); + // Query fixed, no more errors + expect(result.diagnostics.length).toEqual(0); + }); + + it('does not crash on null value returned in response to workspace configuration', async () => { + const previousConfigurationValue = getConfigurationReturnValue; + getConfigurationReturnValue = null; + await expect( + messageProcessor.handleDidChangeConfiguration(), + ).resolves.toStrictEqual({}); + getConfigurationReturnValue = previousConfigurationValue; + }); + + it('properly removes from the file cache with the didClose handler', async () => { + await messageProcessor.handleDidCloseNotification(initialDocument); + + const position = { line: 4, character: 5 }; + const params = { textDocument: initialDocument.textDocument, position }; + + // Should throw because file has been deleted from cache + try { + const result = await messageProcessor.handleCompletionRequest(params); + expect(result).toEqual(null); + } catch {} + }); + + // modified to work with jest.mock() of WatchmanClient + it('runs definition requests', async () => { + jest.setTimeout(10000); + const validQuery = ` + { + hero(episode: EMPIRE){ + ...testFragment + } + } + `; + + const newDocument = { + textDocument: { + text: validQuery, + uri: `${queryPathUri}/test3.graphql`, + version: 1, + }, + }; + messageProcessor._getCachedDocument = (_uri: string) => ({ + version: 1, + contents: [ + { + query: validQuery, + range: new Range(new Position(0, 0), new Position(20, 4)), + }, + ], + }); + + await messageProcessor.handleDidOpenOrSaveNotification(newDocument); + + const test = { + position: new Position(3, 15), + textDocument: newDocument.textDocument, + }; + + const result = await messageProcessor.handleDefinitionRequest(test); + await expect(result[0].uri).toEqual(`${queryPathUri}/test3.graphql`); + }); + + describe('handleDidOpenOrSaveNotification', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._updateGraphQLConfig = jest.fn(); + }); + it('updates config for standard config filename changes', async () => { + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/.graphql.config.js`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + }); + + it('updates config for custom config filename changes', async () => { + const customConfigName = 'custom-config-name.yml'; + messageProcessor._settings = { load: { fileName: customConfigName } }; + + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/${customConfigName}`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + }); + + it('handles config requests with no config', async () => { + messageProcessor._settings = {}; + + await messageProcessor.handleDidChangeConfiguration({ + settings: [], + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + + await messageProcessor.handleDidOpenOrSaveNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/.graphql.config.js`, + languageId: 'js', + version: 0, + text: '', + }, + }); + + expect(messageProcessor._updateGraphQLConfig).toHaveBeenCalled(); + }); + }); + + it('parseDocument finds queries in tagged templates', async () => { + const text = ` +// @flow +import {gql} from 'react-apollo'; +import type {B} from 'B'; +import A from './A'; + +const QUERY = gql\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = parseDocument(text, 'test.js'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('parseDocument finds queries in tagged templates using typescript', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; + +const QUERY: string = gql\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = parseDocument(text, 'test.ts'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('parseDocument finds queries in tagged templates using tsx', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; + +const QUERY: string = gql\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) { + return
    {QUERY}
    +}`; + + const contents = parseDocument(text, 'test.tsx'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('parseDocument finds queries in multi-expression tagged templates using tsx', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; +const someValue = 'value' +const QUERY: string = gql\` +query Test { + test { + value + $\{someValue} + ...FragmentsComment + } + $\{someValue} +}\` + +export function Example(arg: string) { + return
    {QUERY}
    +}`; + + const contents = parseDocument(text, 'test.tsx'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + + ...FragmentsComment + } + +}`); + }); + // TODO: why an extra line here? + it('parseDocument finds queries in multi-expression tagged template with declarations with using tsx', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; +const someValue = 'value' +type SomeType = { test: any } +const QUERY: string = gql\` +query Test { + test { + value + $\{someValue} + ...FragmentsComment + } + $\{someValue} +}\` + +export function Example(arg: string) { + return
    {QUERY}
    +}`; + + const contents = parseDocument(text, 'test.tsx'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + + ...FragmentsComment + } + +}`); + }); + + it('parseDocument finds queries in multi-expression template strings using tsx', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; +const someValue = 'value' +const QUERY: string = +/* GraphQL */ +\` +query Test { + test { + value + \${someValue} + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) { + return
    {QUERY}
    +}`; + + const contents = parseDocument(text, 'test.tsx'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + + ...FragmentsComment + } +} +`); + }); + + it('parseDocument finds queries in call expressions with template literals', async () => { + const text = ` +// @flow +import {gql} from 'react-apollo'; +import type {B} from 'B'; +import A from './A'; + +const QUERY = gql(\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\`); + +export function Example(arg: string) {}`; + + const contents = parseDocument(text, 'test.js'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('parseDocument finds queries in #graphql-annotated templates', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; + +const QUERY: string = \`#graphql +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = parseDocument(text, 'test.ts'); + expect(contents[0].query).toEqual(`#graphql +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('parseDocument finds queries in /*GraphQL*/-annotated templates', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; + +const QUERY: string = /* GraphQL */ \` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = parseDocument(text, 'test.ts'); + expect(contents[0].query).toEqual(` +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('parseDocument ignores non gql tagged templates', async () => { + const text = ` +// @flow +import randomThing from 'package'; +import type {B} from 'B'; +import A from './A'; + +const QUERY = randomThing\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = parseDocument(text, 'test.js'); + expect(contents.length).toEqual(0); + }); + + it('parseDocument ignores non gql call expressions with template literals', async () => { + const text = ` +// @flow +import randomthing from 'package'; +import type {B} from 'B'; +import A from './A'; + +const QUERY = randomthing(\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\`); + +export function Example(arg: string) {}`; + + const contents = parseDocument(text, 'test.js'); + expect(contents.length).toEqual(0); + }); + + it('an unparsable JS/TS file does not throw and bring down the server', async () => { + const text = ` +// @flow +import type randomThing fro 'package'; +import type {B} from 'B'; +im port A from './A'; + +con QUERY = randomThing\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.frag`; + + const contents = parseDocument(text, 'test.js'); + expect(contents.length).toEqual(0); + }); + + describe('handleWatchedFilesChangedNotification', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._updateGraphQLConfig = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: `${pathToFileURL('.')}/foo.graphql`, + type: FileChangeType.Changed, + }, + ], + }); + + expect(messageProcessor._updateGraphQLConfig).not.toHaveBeenCalled(); + }); + }); + + describe('handleWatchedFilesChangedNotification without graphql config', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._graphQLConfig = undefined; + messageProcessor._isGraphQLConfigMissing = true; + messageProcessor._parser = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleWatchedFilesChangedNotification({ + changes: [ + { + uri: `${pathToFileURL('.')}/foo.js`, + type: FileChangeType.Changed, + }, + ], + }); + expect(messageProcessor._parser).not.toHaveBeenCalled(); + }); + }); + + describe('handleDidChangedNotification without graphql config', () => { + const mockReadFileSync: jest.Mock = + jest.requireMock('node:fs').readFileSync; + + beforeEach(() => { + mockReadFileSync.mockReturnValue(''); + messageProcessor._graphQLConfig = undefined; + messageProcessor._isGraphQLConfigMissing = true; + messageProcessor._parser = jest.fn(); + }); + + it('skips config updates for normal file changes', async () => { + await messageProcessor.handleDidChangeNotification({ + textDocument: { + uri: `${pathToFileURL('.')}/foo.js`, + version: 1, + }, + contentChanges: [{ text: 'var something' }], + }); + expect(messageProcessor._parser).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/graphql-language-service-server/src/__tests__/__queries__/test.graphql b/packages/graphql-language-service-server/src/__tests__/__queries__/test.graphql new file mode 100644 index 00000000000..66efd22ef78 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__queries__/test.graphql @@ -0,0 +1,5 @@ +{ + hero(episode: NEWHOPE) { + secretBackstory + } +} diff --git a/packages/graphql-language-service-server/src/__tests__/__queries__/test2.graphql b/packages/graphql-language-service-server/src/__tests__/__queries__/test2.graphql new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/graphql-language-service-server/src/__tests__/__queries__/test3.graphql b/packages/graphql-language-service-server/src/__tests__/__queries__/test3.graphql new file mode 100644 index 00000000000..eb5ef397e7d --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__queries__/test3.graphql @@ -0,0 +1,5 @@ +{ + hero(episode: EMPIRE) { + ...testFragment + } +} diff --git a/packages/graphql-language-service-server/src/__tests__/__queries__/testFragment.graphql b/packages/graphql-language-service-server/src/__tests__/__queries__/testFragment.graphql new file mode 100644 index 00000000000..1d4555df64b --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__queries__/testFragment.graphql @@ -0,0 +1,7 @@ +fragment testFragment on Character { + name + appearsIn + friends { + name + } +} diff --git a/packages/graphql-language-service-server/src/__tests__/__schema__/StarWarsSchema.graphql b/packages/graphql-language-service-server/src/__tests__/__schema__/StarWarsSchema.graphql new file mode 100644 index 00000000000..c26eab0adb8 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/__schema__/StarWarsSchema.graphql @@ -0,0 +1,54 @@ +# Copyright (c) 2021 GraphQL Contributors + +schema { + query: Query +} + +directive @test(testArg: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +enum Episode { + NEWHOPE + EMPIRE + JEDI +} + +interface Character { + id: String! + name: String + friends: [Character] + appearsIn: [Episode] + secretBackstory: String +} + +type Human implements Character { + id: String! + name: String + friends: [Character] + appearsIn: [Episode] + secretBackstory: String +} + +type Droid implements Character { + id: String! + name: String + friends: [Character] + appearsIn: [Episode] + secretBackstory: String + primaryFunction: String +} + +input InputType { + key: String! + value: Int = 42 +} + +type TestType { + testField: String +} + +type Query { + hero(episode: Episode): Character + human(id: String!): Human + droid(id: String!): Droid + inputTypeTest(args: InputType = { key: "key" }): TestType +} diff --git a/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts b/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts new file mode 100644 index 00000000000..8596991dc01 --- /dev/null +++ b/packages/graphql-language-service-server/src/__tests__/findGraphQLTags-test.ts @@ -0,0 +1,451 @@ +/** + * Copyright (c) 2022 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { findGraphQLTags as baseFindGraphQLTags } from '../findGraphQLTags'; + +jest.mock('../Logger'); + +import { NoopLogger } from '../Logger'; + +describe('findGraphQLTags', () => { + const logger = new NoopLogger(); + const findGraphQLTags = (text: string, ext: string) => + baseFindGraphQLTags(text, ext, '', logger); + + it('finds queries in tagged templates', async () => { + const text = ` +// @flow +import {gql} from 'react-apollo'; +import type {B} from 'B'; +import A from './A'; + +const QUERY = gql\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = findGraphQLTags(text, '.js'); + expect(contents[0].template).toEqual(` +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('finds queries in call expressions with template literals', async () => { + const text = ` + // @flow + import {gql} from 'react-apollo'; + import type {B} from 'B'; + import A from './A'; + + const QUERY = gql(\` + query Test { + test { + value + ...FragmentsComment + } + } + \${A.fragments.test} + \`); + + export function Example(arg: string) {}`; + + const contents = findGraphQLTags(text, '.js'); + expect(contents[0].template).toEqual(` + query Test { + test { + value + ...FragmentsComment + } + } + `); + }); + + it('finds queries in #graphql-annotated templates', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; + +const QUERY: string = \`#graphql +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = findGraphQLTags(text, '.ts'); + expect(contents[0].template).toEqual(`#graphql +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('finds queries in /* GraphQL */ prefixed templates', async () => { + const text = ` +import {gql} from 'react-apollo'; +import {B} from 'B'; +import A from './A'; + + +const QUERY: string = +/* GraphQL */ +\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = findGraphQLTags(text, '.ts'); + expect(contents[0].template).toEqual(` +query Test { + test { + value + ...FragmentsComment + } +} +`); + }); + + it('finds queries with nested graphql.experimental template tag expression', async () => { + const text = 'const query = graphql.experimental` query {} `'; + + const contents = findGraphQLTags(text, '.ts'); + expect(contents[0].template).toEqual(' query {} '); + }); + + it('finds queries with spec decorators', async () => { + const text = ` + + @a class A {} + const query = graphql\` query {} \` + + `; + const contents = findGraphQLTags(text, '.ts'); + + expect(contents[0].template).toEqual(' query {} '); + }); + + it('finds queries with es7 decorators', async () => { + const text = ` + + class C { + state = {isLoading: true} + @enumerable(false) + method() {} + @something + onChange() {} + } + + // 'legacy-decorators' does'nt like this this. thus why the modes are incompatible + class MyClass1 extends Component { + state = {isLoading: true} + + @something + onChange() {} + + @something() + handleSubmit() {} + } + + @isTestable(true) + class MyClass {} + + @Module({ + imports: [ + GraphQLModule.forRoot({ + debug: false, + playground: false, + }), + ], + }) + + class A {} + +@Decorator.a.b() +class Todo {} + +@Decorator.d().e +class Todo2{} + + @a + class AppModule {} + const query = graphql\` query {} \` + `; + const contents = findGraphQLTags(text, '.ts'); + + expect(contents[0].template).toEqual(' query {} '); + }); + + it('finds queries with nested template tag expressions', async () => { + const text = `export default { + else: () => gql\` query {} \` +}`; + + const contents = findGraphQLTags(text, '.ts'); + expect(contents[0].template).toEqual(' query {} '); + }); + + it('finds queries with template tags inside call expressions', async () => { + const text = `something({ + else: () => graphql\` query {} \` +})`; + + const contents = findGraphQLTags(text, '.ts'); + expect(contents[0].template).toEqual(' query {} '); + }); + + it('finds queries in tagged templates in Vue SFC using +`; + const contents = findGraphQLTags(text, '.vue'); + expect(contents[0].template).toEqual(` +query {id}`); + expect(contents[0].range.start.line).toEqual(2); + expect(contents[0].range.end.line).toEqual(4); + }); + + it('finds queries in tagged templates in Vue SFC using +`; + const contents = findGraphQLTags(text, '.vue'); + expect(contents[0].template).toEqual(` +query {id}`); + expect(contents[0].range.start.line).toEqual(4); + expect(contents[0].range.end.line).toEqual(6); + }); + + it('finds queries in tagged templates in Vue SFC using normal +`; + const contents = findGraphQLTags(text, '.vue'); + expect(contents[0].template).toEqual(` +query {id}`); + expect(contents[0].range.start.line).toEqual(2); + expect(contents[0].range.end.line).toEqual(4); + }); + + it('finds queries in tagged templates in Vue SFC using normal +`; + const contents = findGraphQLTags(text, '.vue'); + expect(contents[0].template).toEqual(` +query {id}`); + expect(contents[0].range.start.line).toEqual(4); + expect(contents[0].range.end.line).toEqual(6); + }); + + it('finds queries in tagged templates in Vue SFC using +`; + + const contents = findGraphQLTags(text, '.vue'); + expect(contents[0].template).toEqual(` +query {id}`); + }); + + it('finds queries in tagged templates in Svelte using normal +`; + const contents = findGraphQLTags(text, '.svelte'); + expect(contents[0].template).toEqual(` +query {id}`); + }); + + it('no crash in Svelte files without '; + + const consoleErrorSpy = jest + .spyOn(process.stderr, 'write') + .mockImplementation(() => true); + + const contents = baseFindGraphQLTags(text, '.svelte', '', new NoopLogger()); + // We should have no contents + expect(contents).toMatchObject([]); + + // Nothing should be logged as it's a managed error + expect(consoleErrorSpy.mock.calls.length).toBe(0); + + consoleErrorSpy.mockRestore(); + }); + + it('no crash in Svelte files with empty '; + + const consoleErrorSpy = jest + .spyOn(process.stderr, 'write') + .mockImplementation(() => true); + + const contents = baseFindGraphQLTags(text, '.svelte', '', new NoopLogger()); + // We should have no contents + expect(contents).toMatchObject([]); + + // Nothing should be logged as it's a managed error + expect(consoleErrorSpy.mock.calls.length).toBe(0); + + consoleErrorSpy.mockRestore(); + }); + + it('finds multiple queries in a single file', async () => { + const text = `something({ + else: () => gql\` query {} \` +}) +const query = graphql\`query myQuery {}\``; + + const contents = findGraphQLTags(text, '.ts'); + + expect(contents.length).toEqual(2); + + // let's double check that we're properly + // extracting the positions of each embedded string + expect(contents[0].range.start.line).toEqual(1); + expect(contents[0].range.start.character).toEqual(18); + expect(contents[0].range.end.line).toEqual(1); + expect(contents[0].range.end.character).toEqual(28); + expect(contents[0].template).toEqual(' query {} '); + + // and the second string, with correct positional information! + expect(contents[1].range.start.line).toEqual(3); + expect(contents[1].range.start.character).toEqual(22); + expect(contents[1].range.end.line).toEqual(3); + expect(contents[1].range.end.character).toEqual(38); + expect(contents[1].template).toEqual('query myQuery {}'); + }); + + it('ignores non gql tagged templates', async () => { + const text = ` +// @flow +import randomthing from 'package'; +import type {B} from 'B'; +import A from './A'; + +const QUERY = randomthing\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\` + +export function Example(arg: string) {}`; + + const contents = findGraphQLTags(text, '.js'); + expect(contents.length).toEqual(0); + }); + + it('ignores non gql call expressions with template literals', async () => { + const text = ` +// @flow +import randomthing from 'package'; +import type {B} from 'B'; +import A from './A'; + +const QUERY = randomthing(\` +query Test { + test { + value + ...FragmentsComment + } +} +\${A.fragments.test} +\`); + +export function Example(arg: string) {}`; + + const contents = findGraphQLTags(text, '.js'); + expect(contents.length).toEqual(0); + }); +}); diff --git a/packages/graphql-language-service-server/src/findGraphQLTags.ts b/packages/graphql-language-service-server/src/findGraphQLTags.ts new file mode 100644 index 00000000000..a1f0a64830d --- /dev/null +++ b/packages/graphql-language-service-server/src/findGraphQLTags.ts @@ -0,0 +1,326 @@ +/** + * Copyright (c) 2021 GraphQL Contributors + * All rights reserved. + * + * This source code is licensed under the license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { + Expression, + TaggedTemplateExpression, + TemplateLiteral, +} from '@babel/types'; + +import { Position, Range } from 'graphql-language-service'; + +import { parse, ParserOptions, ParserPlugin } from '@babel/parser'; +import * as VueParser from '@vue/compiler-sfc'; +import type { Logger } from 'vscode-languageserver'; + +// Attempt to be as inclusive as possible of source text. +const PARSER_OPTIONS: ParserOptions = { + allowImportExportEverywhere: true, + allowReturnOutsideFunction: true, + allowSuperOutsideMethod: true, + allowAwaitOutsideFunction: true, + // important! this allows babel to keep parsing when there are issues + errorRecovery: true, + sourceType: 'module', + strictMode: false, +}; + +const DEFAULT_STABLE_TAGS = ['graphql', 'graphqls', 'gql']; +export const DEFAULT_TAGS = [...DEFAULT_STABLE_TAGS, 'graphql.experimental']; + +type TagResult = { tag: string; template: string; range: Range }; + +interface TagVisitors { + [type: string]: (node: any) => void; +} + +const BABEL_PLUGINS: ParserPlugin[] = [ + 'asyncDoExpressions', + 'asyncGenerators', + 'bigInt', + 'classProperties', + 'classPrivateProperties', + 'classPrivateMethods', + 'classStaticBlock', + 'doExpressions', + 'decimal', + 'decorators-legacy', + 'destructuringPrivate', + 'dynamicImport', + 'exportDefaultFrom', + 'exportNamespaceFrom', + 'functionBind', + 'functionSent', + 'importMeta', + 'importAssertions', + 'jsx', + 'logicalAssignment', + 'moduleBlocks', + 'moduleStringNames', + 'nullishCoalescingOperator', + 'numericSeparator', + 'objectRestSpread', + 'optionalCatchBinding', + 'optionalChaining', + // ['pipelineOperator', { proposal: 'hack' }], + 'privateIn', + 'regexpUnicodeSets', + 'throwExpressions', + 'topLevelAwait', +]; + +type ParseVueSFCResult = + | { type: 'error'; errors: Error[] } + | { + type: 'ok'; + scriptOffset: number; + scriptSetupAst?: import('@babel/types').Statement[]; + scriptAst?: import('@babel/types').Statement[]; + }; + +function parseVueSFC(source: string): ParseVueSFCResult { + const { errors, descriptor } = VueParser.parse(source); + + if (errors.length !== 0) { + return { type: 'error', errors }; + } + + let scriptBlock: VueParser.SFCScriptBlock | null = null; + try { + scriptBlock = VueParser.compileScript(descriptor, { id: 'foobar' }); + } catch (error) { + if ( + error instanceof Error && + error.message === '[@vue/compiler-sfc] SFC contains no - - - - - - - - - - - - - - - - - - -
    Loading...
    - - - diff --git a/test/schema.js b/test/schema.js deleted file mode 100644 index c0d011ee9e7..00000000000 --- a/test/schema.js +++ /dev/null @@ -1,196 +0,0 @@ -/** - * 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. - */ - -const { - GraphQLSchema, - GraphQLObjectType, - GraphQLUnionType, - GraphQLEnumType, - GraphQLInputObjectType, - GraphQLInterfaceType, - GraphQLBoolean, - GraphQLInt, - GraphQLFloat, - GraphQLString, - GraphQLID, - GraphQLList, -} = require('graphql'); - -// Test Schema -const TestEnum = new GraphQLEnumType({ - name: 'TestEnum', - values: { - RED: { description: 'A rosy color' }, - GREEN: { description: 'The color of martians and slime' }, - BLUE: { description: 'A feeling you might have if you can\'t use GraphQL' }, - } -}); - -const TestInputObject = new GraphQLInputObjectType({ - name: 'TestInput', - fields: () => ({ - string: { - type: GraphQLString, - description: 'Repeats back this string' - }, - 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) }, - }) -}); - -const TestInterface = new GraphQLInterfaceType({ - name: 'TestInterface', - description: 'Test interface.', - fields: () => ({ - name: { - type: GraphQLString, - description: 'Common name string.' - } - }), - resolveType: check => { - return check ? UnionFirst : UnionSecond; - } -}); - -const UnionFirst = new GraphQLObjectType({ - name: 'First', - fields: () => ({ - name: { - type: GraphQLString, - description: 'Common name string for UnionFirst.' - }, - first: { - type: new GraphQLList(TestInterface), - resolve: () => { return true; } - } - }), - interfaces: [ TestInterface ] -}); - -const UnionSecond = new GraphQLObjectType({ - name: 'Second', - fields: () => ({ - name: { - type: GraphQLString, - description: 'Common name string for UnionFirst.' - }, - second: { - type: TestInterface, - resolve: () => { return false; } - } - }), - interfaces: [ TestInterface ] -}); - -const TestUnion = new GraphQLUnionType({ - name: 'TestUnion', - types: [ UnionFirst, UnionSecond ], - resolveType() { - return UnionFirst; - } -}); - -const TestType = new GraphQLObjectType({ - name: 'Test', - fields: () => ({ - test: { - type: TestType, - description: '`test` field from `Test` type.', - resolve: () => ({}) - }, - union: { - type: TestUnion, - description: '> union field from Test type, block-quoted.', - resolve: () => ({}) - }, - id: { - type: GraphQLID, - description: 'id field from Test type.', - resolve: () => 'abc123', - }, - isTest: { - type: GraphQLBoolean, - description: 'Is this a test schema? Sure it is.', - 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) }, - } - }, - }) -}); - -const 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 TestSubscriptionType = new GraphQLObjectType({ - name: 'SubscriptionType', - description: 'This is a simple subscription type', - fields: { - subscribeToTest: { - type: TestType, - description: 'Subscribe to the test type', - args: { - id: { type: GraphQLString } - } - } - } -}); - -const myTestSchema = new GraphQLSchema({ - query: TestType, - mutation: TestMutationType, - subscription: TestSubscriptionType -}); - -module.exports = myTestSchema; diff --git a/test/server.js b/test/server.js deleted file mode 100644 index 4a63eb5f1b8..00000000000 --- a/test/server.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * 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. - */ - - /* eslint-disable no-console */ -import express from 'express'; -import path from 'path'; -import fs from 'fs'; -import browserify from 'browserify'; -import browserifyShim from 'browserify-shim'; -import watchify from 'watchify'; -import babelify from 'babelify'; -import graphqlHTTP from 'express-graphql'; - -import schema from './schema'; - -const app = express(); - -// Server -app.use('/graphql', graphqlHTTP({ schema })); - -// Client -let bundleBuffer; - -const b = browserify({ - entries: [ path.join(__dirname, '../src/index.js') ], - cache: {}, - packageCache: {}, - transform: [ babelify, browserifyShim ], - plugin: [ watchify ], - standalone: 'GraphiQL', - globalTransform: 'browserify-shim' -}); - -b.on('update', () => makeBundle()); -b.on('log', msg => console.log(`graphiql.js: ${msg}`)); - -function makeBundle(callback) { - b.bundle((err, buffer) => { - if (err) { - console.error('Error building graphiql.js'); - console.error(err); - return; - } - bundleBuffer = buffer; - callback && callback(); - }); -} - -app.use('/graphiql.js', (req, res) => { - res.end(bundleBuffer); -}); - -app.use('/css', express.static(path.join(__dirname, '../css'))); -app.use(express.static(__dirname)); - -console.log('Initial build...'); -makeBundle(() => { - app.listen(8080, () => console.log('Started on http://localhost:8080/')); -}); diff --git a/test/vendor/es6-promise.auto.js b/test/vendor/es6-promise.auto.js deleted file mode 100644 index 19e6c13a655..00000000000 --- a/test/vendor/es6-promise.auto.js +++ /dev/null @@ -1,1159 +0,0 @@ -/*! - * @overview es6-promise - a tiny implementation of Promises/A+. - * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) - * @license Licensed under MIT license - * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE - * @version 4.0.5 - */ - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.ES6Promise = factory()); -}(this, (function () { 'use strict'; - -function objectOrFunction(x) { - return typeof x === 'function' || typeof x === 'object' && x !== null; -} - -function isFunction(x) { - return typeof x === 'function'; -} - -var _isArray = undefined; -if (!Array.isArray) { - _isArray = function (x) { - return Object.prototype.toString.call(x) === '[object Array]'; - }; -} else { - _isArray = Array.isArray; -} - -var isArray = _isArray; - -var len = 0; -var vertxNext = undefined; -var customSchedulerFn = undefined; - -var asap = function asap(callback, arg) { - queue[len] = callback; - queue[len + 1] = arg; - len += 2; - if (len === 2) { - // If len is 2, that means that we need to schedule an async flush. - // If additional callbacks are queued before the queue is flushed, they - // will be processed by this flush that we are scheduling. - if (customSchedulerFn) { - customSchedulerFn(flush); - } else { - scheduleFlush(); - } - } -}; - -function setScheduler(scheduleFn) { - customSchedulerFn = scheduleFn; -} - -function setAsap(asapFn) { - asap = asapFn; -} - -var browserWindow = typeof window !== 'undefined' ? window : undefined; -var browserGlobal = browserWindow || {}; -var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; -var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]'; - -// test for web worker but not in IE10 -var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined'; - -// node -function useNextTick() { - // node version 0.10.x displays a deprecation warning when nextTick is used recursively - // see https://github.com/cujojs/when/issues/410 for details - return function () { - return process.nextTick(flush); - }; -} - -// vertx -function useVertxTimer() { - if (typeof vertxNext !== 'undefined') { - return function () { - vertxNext(flush); - }; - } - - return useSetTimeout(); -} - -function useMutationObserver() { - var iterations = 0; - var observer = new BrowserMutationObserver(flush); - var node = document.createTextNode(''); - observer.observe(node, { characterData: true }); - - return function () { - node.data = iterations = ++iterations % 2; - }; -} - -// web worker -function useMessageChannel() { - var channel = new MessageChannel(); - channel.port1.onmessage = flush; - return function () { - return channel.port2.postMessage(0); - }; -} - -function useSetTimeout() { - // Store setTimeout reference so es6-promise will be unaffected by - // other code modifying setTimeout (like sinon.useFakeTimers()) - var globalSetTimeout = setTimeout; - return function () { - return globalSetTimeout(flush, 1); - }; -} - -var queue = new Array(1000); -function flush() { - for (var i = 0; i < len; i += 2) { - var callback = queue[i]; - var arg = queue[i + 1]; - - callback(arg); - - queue[i] = undefined; - queue[i + 1] = undefined; - } - - len = 0; -} - -function attemptVertx() { - try { - var r = require; - var vertx = r('vertx'); - vertxNext = vertx.runOnLoop || vertx.runOnContext; - return useVertxTimer(); - } catch (e) { - return useSetTimeout(); - } -} - -var scheduleFlush = undefined; -// Decide what async method to use to triggering processing of queued callbacks: -if (isNode) { - scheduleFlush = useNextTick(); -} else if (BrowserMutationObserver) { - scheduleFlush = useMutationObserver(); -} else if (isWorker) { - scheduleFlush = useMessageChannel(); -} else if (browserWindow === undefined && typeof require === 'function') { - scheduleFlush = attemptVertx(); -} else { - scheduleFlush = useSetTimeout(); -} - -function then(onFulfillment, onRejection) { - var _arguments = arguments; - - var parent = this; - - var child = new this.constructor(noop); - - if (child[PROMISE_ID] === undefined) { - makePromise(child); - } - - var _state = parent._state; - - if (_state) { - (function () { - var callback = _arguments[_state - 1]; - asap(function () { - return invokeCallback(_state, child, callback, parent._result); - }); - })(); - } else { - subscribe(parent, child, onFulfillment, onRejection); - } - - return child; -} - -/** - `Promise.resolve` returns a promise that will become resolved with the - passed `value`. It is shorthand for the following: - - ```javascript - let promise = new Promise(function(resolve, reject){ - resolve(1); - }); - - promise.then(function(value){ - // value === 1 - }); - ``` - - Instead of writing the above, your code now simply becomes the following: - - ```javascript - let promise = Promise.resolve(1); - - promise.then(function(value){ - // value === 1 - }); - ``` - - @method resolve - @static - @param {Any} value value that the returned promise will be resolved with - Useful for tooling. - @return {Promise} a promise that will become fulfilled with the given - `value` -*/ -function resolve(object) { - /*jshint validthis:true */ - var Constructor = this; - - if (object && typeof object === 'object' && object.constructor === Constructor) { - return object; - } - - var promise = new Constructor(noop); - _resolve(promise, object); - return promise; -} - -var PROMISE_ID = Math.random().toString(36).substring(16); - -function noop() {} - -var PENDING = void 0; -var FULFILLED = 1; -var REJECTED = 2; - -var GET_THEN_ERROR = new ErrorObject(); - -function selfFulfillment() { - return new TypeError("You cannot resolve a promise with itself"); -} - -function cannotReturnOwn() { - return new TypeError('A promises callback cannot return that same promise.'); -} - -function getThen(promise) { - try { - return promise.then; - } catch (error) { - GET_THEN_ERROR.error = error; - return GET_THEN_ERROR; - } -} - -function tryThen(then, value, fulfillmentHandler, rejectionHandler) { - try { - then.call(value, fulfillmentHandler, rejectionHandler); - } catch (e) { - return e; - } -} - -function handleForeignThenable(promise, thenable, then) { - asap(function (promise) { - var sealed = false; - var error = tryThen(then, thenable, function (value) { - if (sealed) { - return; - } - sealed = true; - if (thenable !== value) { - _resolve(promise, value); - } else { - fulfill(promise, value); - } - }, function (reason) { - if (sealed) { - return; - } - sealed = true; - - _reject(promise, reason); - }, 'Settle: ' + (promise._label || ' unknown promise')); - - if (!sealed && error) { - sealed = true; - _reject(promise, error); - } - }, promise); -} - -function handleOwnThenable(promise, thenable) { - if (thenable._state === FULFILLED) { - fulfill(promise, thenable._result); - } else if (thenable._state === REJECTED) { - _reject(promise, thenable._result); - } else { - subscribe(thenable, undefined, function (value) { - return _resolve(promise, value); - }, function (reason) { - return _reject(promise, reason); - }); - } -} - -function handleMaybeThenable(promise, maybeThenable, then$$) { - if (maybeThenable.constructor === promise.constructor && then$$ === then && maybeThenable.constructor.resolve === resolve) { - handleOwnThenable(promise, maybeThenable); - } else { - if (then$$ === GET_THEN_ERROR) { - _reject(promise, GET_THEN_ERROR.error); - } else if (then$$ === undefined) { - fulfill(promise, maybeThenable); - } else if (isFunction(then$$)) { - handleForeignThenable(promise, maybeThenable, then$$); - } else { - fulfill(promise, maybeThenable); - } - } -} - -function _resolve(promise, value) { - if (promise === value) { - _reject(promise, selfFulfillment()); - } else if (objectOrFunction(value)) { - handleMaybeThenable(promise, value, getThen(value)); - } else { - fulfill(promise, value); - } -} - -function publishRejection(promise) { - if (promise._onerror) { - promise._onerror(promise._result); - } - - publish(promise); -} - -function fulfill(promise, value) { - if (promise._state !== PENDING) { - return; - } - - promise._result = value; - promise._state = FULFILLED; - - if (promise._subscribers.length !== 0) { - asap(publish, promise); - } -} - -function _reject(promise, reason) { - if (promise._state !== PENDING) { - return; - } - promise._state = REJECTED; - promise._result = reason; - - asap(publishRejection, promise); -} - -function subscribe(parent, child, onFulfillment, onRejection) { - var _subscribers = parent._subscribers; - var length = _subscribers.length; - - parent._onerror = null; - - _subscribers[length] = child; - _subscribers[length + FULFILLED] = onFulfillment; - _subscribers[length + REJECTED] = onRejection; - - if (length === 0 && parent._state) { - asap(publish, parent); - } -} - -function publish(promise) { - var subscribers = promise._subscribers; - var settled = promise._state; - - if (subscribers.length === 0) { - return; - } - - var child = undefined, - callback = undefined, - detail = promise._result; - - for (var i = 0; i < subscribers.length; i += 3) { - child = subscribers[i]; - callback = subscribers[i + settled]; - - if (child) { - invokeCallback(settled, child, callback, detail); - } else { - callback(detail); - } - } - - promise._subscribers.length = 0; -} - -function ErrorObject() { - this.error = null; -} - -var TRY_CATCH_ERROR = new ErrorObject(); - -function tryCatch(callback, detail) { - try { - return callback(detail); - } catch (e) { - TRY_CATCH_ERROR.error = e; - return TRY_CATCH_ERROR; - } -} - -function invokeCallback(settled, promise, callback, detail) { - var hasCallback = isFunction(callback), - value = undefined, - error = undefined, - succeeded = undefined, - failed = undefined; - - if (hasCallback) { - value = tryCatch(callback, detail); - - if (value === TRY_CATCH_ERROR) { - failed = true; - error = value.error; - value = null; - } else { - succeeded = true; - } - - if (promise === value) { - _reject(promise, cannotReturnOwn()); - return; - } - } else { - value = detail; - succeeded = true; - } - - if (promise._state !== PENDING) { - // noop - } else if (hasCallback && succeeded) { - _resolve(promise, value); - } else if (failed) { - _reject(promise, error); - } else if (settled === FULFILLED) { - fulfill(promise, value); - } else if (settled === REJECTED) { - _reject(promise, value); - } -} - -function initializePromise(promise, resolver) { - try { - resolver(function resolvePromise(value) { - _resolve(promise, value); - }, function rejectPromise(reason) { - _reject(promise, reason); - }); - } catch (e) { - _reject(promise, e); - } -} - -var id = 0; -function nextId() { - return id++; -} - -function makePromise(promise) { - promise[PROMISE_ID] = id++; - promise._state = undefined; - promise._result = undefined; - promise._subscribers = []; -} - -function Enumerator(Constructor, input) { - this._instanceConstructor = Constructor; - this.promise = new Constructor(noop); - - if (!this.promise[PROMISE_ID]) { - makePromise(this.promise); - } - - if (isArray(input)) { - this._input = input; - this.length = input.length; - this._remaining = input.length; - - this._result = new Array(this.length); - - if (this.length === 0) { - fulfill(this.promise, this._result); - } else { - this.length = this.length || 0; - this._enumerate(); - if (this._remaining === 0) { - fulfill(this.promise, this._result); - } - } - } else { - _reject(this.promise, validationError()); - } -} - -function validationError() { - return new Error('Array Methods must be provided an Array'); -}; - -Enumerator.prototype._enumerate = function () { - var length = this.length; - var _input = this._input; - - for (var i = 0; this._state === PENDING && i < length; i++) { - this._eachEntry(_input[i], i); - } -}; - -Enumerator.prototype._eachEntry = function (entry, i) { - var c = this._instanceConstructor; - var resolve$$ = c.resolve; - - if (resolve$$ === resolve) { - var _then = getThen(entry); - - if (_then === then && entry._state !== PENDING) { - this._settledAt(entry._state, i, entry._result); - } else if (typeof _then !== 'function') { - this._remaining--; - this._result[i] = entry; - } else if (c === Promise) { - var promise = new c(noop); - handleMaybeThenable(promise, entry, _then); - this._willSettleAt(promise, i); - } else { - this._willSettleAt(new c(function (resolve$$) { - return resolve$$(entry); - }), i); - } - } else { - this._willSettleAt(resolve$$(entry), i); - } -}; - -Enumerator.prototype._settledAt = function (state, i, value) { - var promise = this.promise; - - if (promise._state === PENDING) { - this._remaining--; - - if (state === REJECTED) { - _reject(promise, value); - } else { - this._result[i] = value; - } - } - - if (this._remaining === 0) { - fulfill(promise, this._result); - } -}; - -Enumerator.prototype._willSettleAt = function (promise, i) { - var enumerator = this; - - subscribe(promise, undefined, function (value) { - return enumerator._settledAt(FULFILLED, i, value); - }, function (reason) { - return enumerator._settledAt(REJECTED, i, reason); - }); -}; - -/** - `Promise.all` accepts an array of promises, and returns a new promise which - is fulfilled with an array of fulfillment values for the passed promises, or - rejected with the reason of the first passed promise to be rejected. It casts all - elements of the passed iterable to promises as it runs this algorithm. - - Example: - - ```javascript - let promise1 = resolve(1); - let promise2 = resolve(2); - let promise3 = resolve(3); - let promises = [ promise1, promise2, promise3 ]; - - Promise.all(promises).then(function(array){ - // The array here would be [ 1, 2, 3 ]; - }); - ``` - - If any of the `promises` given to `all` are rejected, the first promise - that is rejected will be given as an argument to the returned promises's - rejection handler. For example: - - Example: - - ```javascript - let promise1 = resolve(1); - let promise2 = reject(new Error("2")); - let promise3 = reject(new Error("3")); - let promises = [ promise1, promise2, promise3 ]; - - Promise.all(promises).then(function(array){ - // Code here never runs because there are rejected promises! - }, function(error) { - // error.message === "2" - }); - ``` - - @method all - @static - @param {Array} entries array of promises - @param {String} label optional string for labeling the promise. - Useful for tooling. - @return {Promise} promise that is fulfilled when all `promises` have been - fulfilled, or rejected if any of them become rejected. - @static -*/ -function all(entries) { - return new Enumerator(this, entries).promise; -} - -/** - `Promise.race` returns a new promise which is settled in the same way as the - first passed promise to settle. - - Example: - - ```javascript - let promise1 = new Promise(function(resolve, reject){ - setTimeout(function(){ - resolve('promise 1'); - }, 200); - }); - - let promise2 = new Promise(function(resolve, reject){ - setTimeout(function(){ - resolve('promise 2'); - }, 100); - }); - - Promise.race([promise1, promise2]).then(function(result){ - // result === 'promise 2' because it was resolved before promise1 - // was resolved. - }); - ``` - - `Promise.race` is deterministic in that only the state of the first - settled promise matters. For example, even if other promises given to the - `promises` array argument are resolved, but the first settled promise has - become rejected before the other promises became fulfilled, the returned - promise will become rejected: - - ```javascript - let promise1 = new Promise(function(resolve, reject){ - setTimeout(function(){ - resolve('promise 1'); - }, 200); - }); - - let promise2 = new Promise(function(resolve, reject){ - setTimeout(function(){ - reject(new Error('promise 2')); - }, 100); - }); - - Promise.race([promise1, promise2]).then(function(result){ - // Code here never runs - }, function(reason){ - // reason.message === 'promise 2' because promise 2 became rejected before - // promise 1 became fulfilled - }); - ``` - - An example real-world use case is implementing timeouts: - - ```javascript - Promise.race([ajax('foo.json'), timeout(5000)]) - ``` - - @method race - @static - @param {Array} promises array of promises to observe - Useful for tooling. - @return {Promise} a promise which settles in the same way as the first passed - promise to settle. -*/ -function race(entries) { - /*jshint validthis:true */ - var Constructor = this; - - if (!isArray(entries)) { - return new Constructor(function (_, reject) { - return reject(new TypeError('You must pass an array to race.')); - }); - } else { - return new Constructor(function (resolve, reject) { - var length = entries.length; - for (var i = 0; i < length; i++) { - Constructor.resolve(entries[i]).then(resolve, reject); - } - }); - } -} - -/** - `Promise.reject` returns a promise rejected with the passed `reason`. - It is shorthand for the following: - - ```javascript - let promise = new Promise(function(resolve, reject){ - reject(new Error('WHOOPS')); - }); - - promise.then(function(value){ - // Code here doesn't run because the promise is rejected! - }, function(reason){ - // reason.message === 'WHOOPS' - }); - ``` - - Instead of writing the above, your code now simply becomes the following: - - ```javascript - let promise = Promise.reject(new Error('WHOOPS')); - - promise.then(function(value){ - // Code here doesn't run because the promise is rejected! - }, function(reason){ - // reason.message === 'WHOOPS' - }); - ``` - - @method reject - @static - @param {Any} reason value that the returned promise will be rejected with. - Useful for tooling. - @return {Promise} a promise rejected with the given `reason`. -*/ -function reject(reason) { - /*jshint validthis:true */ - var Constructor = this; - var promise = new Constructor(noop); - _reject(promise, reason); - return promise; -} - -function needsResolver() { - throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); -} - -function needsNew() { - throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); -} - -/** - Promise objects represent the eventual result of an asynchronous operation. The - primary way of interacting with a promise is through its `then` method, which - registers callbacks to receive either a promise's eventual value or the reason - why the promise cannot be fulfilled. - - Terminology - ----------- - - - `promise` is an object or function with a `then` method whose behavior conforms to this specification. - - `thenable` is an object or function that defines a `then` method. - - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). - - `exception` is a value that is thrown using the throw statement. - - `reason` is a value that indicates why a promise was rejected. - - `settled` the final resting state of a promise, fulfilled or rejected. - - A promise can be in one of three states: pending, fulfilled, or rejected. - - Promises that are fulfilled have a fulfillment value and are in the fulfilled - state. Promises that are rejected have a rejection reason and are in the - rejected state. A fulfillment value is never a thenable. - - Promises can also be said to *resolve* a value. If this value is also a - promise, then the original promise's settled state will match the value's - settled state. So a promise that *resolves* a promise that rejects will - itself reject, and a promise that *resolves* a promise that fulfills will - itself fulfill. - - - Basic Usage: - ------------ - - ```js - let promise = new Promise(function(resolve, reject) { - // on success - resolve(value); - - // on failure - reject(reason); - }); - - promise.then(function(value) { - // on fulfillment - }, function(reason) { - // on rejection - }); - ``` - - Advanced Usage: - --------------- - - Promises shine when abstracting away asynchronous interactions such as - `XMLHttpRequest`s. - - ```js - function getJSON(url) { - return new Promise(function(resolve, reject){ - let xhr = new XMLHttpRequest(); - - xhr.open('GET', url); - xhr.onreadystatechange = handler; - xhr.responseType = 'json'; - xhr.setRequestHeader('Accept', 'application/json'); - xhr.send(); - - function handler() { - if (this.readyState === this.DONE) { - if (this.status === 200) { - resolve(this.response); - } else { - reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); - } - } - }; - }); - } - - getJSON('/posts.json').then(function(json) { - // on fulfillment - }, function(reason) { - // on rejection - }); - ``` - - Unlike callbacks, promises are great composable primitives. - - ```js - Promise.all([ - getJSON('/posts'), - getJSON('/comments') - ]).then(function(values){ - values[0] // => postsJSON - values[1] // => commentsJSON - - return values; - }); - ``` - - @class Promise - @param {function} resolver - Useful for tooling. - @constructor -*/ -function Promise(resolver) { - this[PROMISE_ID] = nextId(); - this._result = this._state = undefined; - this._subscribers = []; - - if (noop !== resolver) { - typeof resolver !== 'function' && needsResolver(); - this instanceof Promise ? initializePromise(this, resolver) : needsNew(); - } -} - -Promise.all = all; -Promise.race = race; -Promise.resolve = resolve; -Promise.reject = reject; -Promise._setScheduler = setScheduler; -Promise._setAsap = setAsap; -Promise._asap = asap; - -Promise.prototype = { - constructor: Promise, - - /** - The primary way of interacting with a promise is through its `then` method, - which registers callbacks to receive either a promise's eventual value or the - reason why the promise cannot be fulfilled. - - ```js - findUser().then(function(user){ - // user is available - }, function(reason){ - // user is unavailable, and you are given the reason why - }); - ``` - - Chaining - -------- - - The return value of `then` is itself a promise. This second, 'downstream' - promise is resolved with the return value of the first promise's fulfillment - or rejection handler, or rejected if the handler throws an exception. - - ```js - findUser().then(function (user) { - return user.name; - }, function (reason) { - return 'default name'; - }).then(function (userName) { - // If `findUser` fulfilled, `userName` will be the user's name, otherwise it - // will be `'default name'` - }); - - findUser().then(function (user) { - throw new Error('Found user, but still unhappy'); - }, function (reason) { - throw new Error('`findUser` rejected and we're unhappy'); - }).then(function (value) { - // never reached - }, function (reason) { - // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. - // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. - }); - ``` - If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. - - ```js - findUser().then(function (user) { - throw new PedagogicalException('Upstream error'); - }).then(function (value) { - // never reached - }).then(function (value) { - // never reached - }, function (reason) { - // The `PedgagocialException` is propagated all the way down to here - }); - ``` - - Assimilation - ------------ - - Sometimes the value you want to propagate to a downstream promise can only be - retrieved asynchronously. This can be achieved by returning a promise in the - fulfillment or rejection handler. The downstream promise will then be pending - until the returned promise is settled. This is called *assimilation*. - - ```js - findUser().then(function (user) { - return findCommentsByAuthor(user); - }).then(function (comments) { - // The user's comments are now available - }); - ``` - - If the assimliated promise rejects, then the downstream promise will also reject. - - ```js - findUser().then(function (user) { - return findCommentsByAuthor(user); - }).then(function (comments) { - // If `findCommentsByAuthor` fulfills, we'll have the value here - }, function (reason) { - // If `findCommentsByAuthor` rejects, we'll have the reason here - }); - ``` - - Simple Example - -------------- - - Synchronous Example - - ```javascript - let result; - - try { - result = findResult(); - // success - } catch(reason) { - // failure - } - ``` - - Errback Example - - ```js - findResult(function(result, err){ - if (err) { - // failure - } else { - // success - } - }); - ``` - - Promise Example; - - ```javascript - findResult().then(function(result){ - // success - }, function(reason){ - // failure - }); - ``` - - Advanced Example - -------------- - - Synchronous Example - - ```javascript - let author, books; - - try { - author = findAuthor(); - books = findBooksByAuthor(author); - // success - } catch(reason) { - // failure - } - ``` - - Errback Example - - ```js - - function foundBooks(books) { - - } - - function failure(reason) { - - } - - findAuthor(function(author, err){ - if (err) { - failure(err); - // failure - } else { - try { - findBoooksByAuthor(author, function(books, err) { - if (err) { - failure(err); - } else { - try { - foundBooks(books); - } catch(reason) { - failure(reason); - } - } - }); - } catch(error) { - failure(err); - } - // success - } - }); - ``` - - Promise Example; - - ```javascript - findAuthor(). - then(findBooksByAuthor). - then(function(books){ - // found books - }).catch(function(reason){ - // something went wrong - }); - ``` - - @method then - @param {Function} onFulfilled - @param {Function} onRejected - Useful for tooling. - @return {Promise} - */ - then: then, - - /** - `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same - as the catch block of a try/catch statement. - - ```js - function findAuthor(){ - throw new Error('couldn't find that author'); - } - - // synchronous - try { - findAuthor(); - } catch(reason) { - // something went wrong - } - - // async with promises - findAuthor().catch(function(reason){ - // something went wrong - }); - ``` - - @method catch - @param {Function} onRejection - Useful for tooling. - @return {Promise} - */ - 'catch': function _catch(onRejection) { - return this.then(null, onRejection); - } -}; - -function polyfill() { - var local = undefined; - - if (typeof global !== 'undefined') { - local = global; - } else if (typeof self !== 'undefined') { - local = self; - } else { - try { - local = Function('return this')(); - } catch (e) { - throw new Error('polyfill failed because global object is unavailable in this environment'); - } - } - - var P = local.Promise; - - if (P) { - var promiseToString = null; - try { - promiseToString = Object.prototype.toString.call(P.resolve()); - } catch (e) { - // silently ignored - } - - if (promiseToString === '[object Promise]' && !P.cast) { - return; - } - } - - local.Promise = Promise; -} - -// Strange compat.. -Promise.polyfill = polyfill; -Promise.Promise = Promise; - -return Promise; - -}))); - -ES6Promise.polyfill(); -//# sourceMappingURL=es6-promise.auto.map diff --git a/test/vendor/fetch.min.js b/test/vendor/fetch.min.js deleted file mode 100644 index 6a1c8c86dd2..00000000000 --- a/test/vendor/fetch.min.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";function t(t){if("string"!=typeof t&&(t=t.toString()),/[^a-z0-9\-#$%&'*+.\^_`|~]/i.test(t))throw new TypeError("Invalid character in header field name");return t.toLowerCase()}function e(t){return"string"!=typeof t&&(t=t.toString()),t}function r(t){this.map={},t instanceof r?t.forEach(function(t,e){this.append(e,t)},this):t&&Object.getOwnPropertyNames(t).forEach(function(e){this.append(e,t[e])},this)}function o(t){return t.bodyUsed?Promise.reject(new TypeError("Already read")):void(t.bodyUsed=!0)}function n(t){return new Promise(function(e,r){t.onload=function(){e(t.result)},t.onerror=function(){r(t.error)}})}function s(t){var e=new FileReader;return e.readAsArrayBuffer(t),n(e)}function i(t){var e=new FileReader;return e.readAsText(t),n(e)}function a(){return this.bodyUsed=!1,this._initBody=function(t){if(this._bodyInit=t,"string"==typeof t)this._bodyText=t;else if(p.blob&&Blob.prototype.isPrototypeOf(t))this._bodyBlob=t;else if(p.formData&&FormData.prototype.isPrototypeOf(t))this._bodyFormData=t;else{if(t)throw new Error("unsupported BodyInit type");this._bodyText=""}},p.blob?(this.blob=function(){var t=o(this);if(t)return t;if(this._bodyBlob)return Promise.resolve(this._bodyBlob);if(this._bodyFormData)throw new Error("could not read FormData body as blob");return Promise.resolve(new Blob([this._bodyText]))},this.arrayBuffer=function(){return this.blob().then(s)},this.text=function(){var t=o(this);if(t)return t;if(this._bodyBlob)return i(this._bodyBlob);if(this._bodyFormData)throw new Error("could not read FormData body as text");return Promise.resolve(this._bodyText)}):this.text=function(){var t=o(this);return t?t:Promise.resolve(this._bodyText)},p.formData&&(this.formData=function(){return this.text().then(h)}),this.json=function(){return this.text().then(JSON.parse)},this}function u(t){var e=t.toUpperCase();return c.indexOf(e)>-1?e:t}function f(t,e){if(e=e||{},this.url=t,this.credentials=e.credentials||"omit",this.headers=new r(e.headers),this.method=u(e.method||"GET"),this.mode=e.mode||null,this.referrer=null,("GET"===this.method||"HEAD"===this.method)&&e.body)throw new TypeError("Body not allowed for GET or HEAD requests");this._initBody(e.body)}function h(t){var e=new FormData;return t.trim().split("&").forEach(function(t){if(t){var r=t.split("="),o=r.shift().replace(/\+/g," "),n=r.join("=").replace(/\+/g," ");e.append(decodeURIComponent(o),decodeURIComponent(n))}}),e}function d(t){var e=new r,o=t.getAllResponseHeaders().trim().split("\n");return o.forEach(function(t){var r=t.trim().split(":"),o=r.shift().trim(),n=r.join(":").trim();e.append(o,n)}),e}function l(t,e){e||(e={}),this._initBody(t),this.type="default",this.url=null,this.status=e.status,this.ok=this.status>=200&&this.status<300,this.statusText=e.statusText,this.headers=e.headers instanceof r?e.headers:new r(e.headers),this.url=e.url||""}if(!self.fetch){r.prototype.append=function(r,o){r=t(r),o=e(o);var n=this.map[r];n||(n=[],this.map[r]=n),n.push(o)},r.prototype["delete"]=function(e){delete this.map[t(e)]},r.prototype.get=function(e){var r=this.map[t(e)];return r?r[0]:null},r.prototype.getAll=function(e){return this.map[t(e)]||[]},r.prototype.has=function(e){return this.map.hasOwnProperty(t(e))},r.prototype.set=function(r,o){this.map[t(r)]=[e(o)]},r.prototype.forEach=function(t,e){Object.getOwnPropertyNames(this.map).forEach(function(r){this.map[r].forEach(function(o){t.call(e,o,r,this)},this)},this)};var p={blob:"FileReader"in self&&"Blob"in self&&function(){try{return new Blob,!0}catch(t){return!1}}(),formData:"FormData"in self},c=["DELETE","GET","HEAD","OPTIONS","POST","PUT"];a.call(f.prototype),a.call(l.prototype),self.Headers=r,self.Request=f,self.Response=l,self.fetch=function(t,e){var r;return r=f.prototype.isPrototypeOf(t)&&!e?t:new f(t,e),new Promise(function(t,e){function o(){return"responseURL"in n?n.responseURL:/^X-Request-URL:/m.test(n.getAllResponseHeaders())?n.getResponseHeader("X-Request-URL"):void 0}var n=new XMLHttpRequest;n.onload=function(){var r=1223===n.status?204:n.status;if(100>r||r>599)return void e(new TypeError("Network request failed"));var s={status:r,statusText:n.statusText,headers:d(n),url:o()},i="response"in n?n.response:n.responseText;t(new l(i,s))},n.onerror=function(){e(new TypeError("Network request failed"))},n.open(r.method,r.url,!0),"include"===r.credentials&&(n.withCredentials=!0),"responseType"in n&&p.blob&&(n.responseType="blob"),r.headers.forEach(function(t,e){n.setRequestHeader(e,t)}),n.send("undefined"==typeof r._bodyInit?null:r._bodyInit)})},self.fetch.polyfill=!0}}(); \ No newline at end of file diff --git a/test/vendor/react-15.3.2.js b/test/vendor/react-15.3.2.js deleted file mode 100644 index 692b5fac62e..00000000000 --- a/test/vendor/react-15.3.2.js +++ /dev/null @@ -1,20595 +0,0 @@ - /** - * React v15.3.2 - */ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 8 && documentMode <= 11); - -/** - * Opera <= 12 includes TextEvent in window, but does not fire - * text input events. Rely on keypress instead. - */ -function isPresto() { - var opera = window.opera; - return typeof opera === 'object' && typeof opera.version === 'function' && parseInt(opera.version(), 10) <= 12; -} - -var SPACEBAR_CODE = 32; -var SPACEBAR_CHAR = String.fromCharCode(SPACEBAR_CODE); - -var topLevelTypes = EventConstants.topLevelTypes; - -// Events and their corresponding property names. -var eventTypes = { - beforeInput: { - phasedRegistrationNames: { - bubbled: keyOf({ onBeforeInput: null }), - captured: keyOf({ onBeforeInputCapture: null }) - }, - dependencies: [topLevelTypes.topCompositionEnd, topLevelTypes.topKeyPress, topLevelTypes.topTextInput, topLevelTypes.topPaste] - }, - compositionEnd: { - phasedRegistrationNames: { - bubbled: keyOf({ onCompositionEnd: null }), - captured: keyOf({ onCompositionEndCapture: null }) - }, - dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionEnd, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown] - }, - compositionStart: { - phasedRegistrationNames: { - bubbled: keyOf({ onCompositionStart: null }), - captured: keyOf({ onCompositionStartCapture: null }) - }, - dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionStart, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown] - }, - compositionUpdate: { - phasedRegistrationNames: { - bubbled: keyOf({ onCompositionUpdate: null }), - captured: keyOf({ onCompositionUpdateCapture: null }) - }, - dependencies: [topLevelTypes.topBlur, topLevelTypes.topCompositionUpdate, topLevelTypes.topKeyDown, topLevelTypes.topKeyPress, topLevelTypes.topKeyUp, topLevelTypes.topMouseDown] - } -}; - -// Track whether we've ever handled a keypress on the space key. -var hasSpaceKeypress = false; - -/** - * Return whether a native keypress event is assumed to be a command. - * This is required because Firefox fires `keypress` events for key commands - * (cut, copy, select-all, etc.) even though no character is inserted. - */ -function isKeypressCommand(nativeEvent) { - return (nativeEvent.ctrlKey || nativeEvent.altKey || nativeEvent.metaKey) && - // ctrlKey && altKey is equivalent to AltGr, and is not a command. - !(nativeEvent.ctrlKey && nativeEvent.altKey); -} - -/** - * Translate native top level events into event types. - * - * @param {string} topLevelType - * @return {object} - */ -function getCompositionEventType(topLevelType) { - switch (topLevelType) { - case topLevelTypes.topCompositionStart: - return eventTypes.compositionStart; - case topLevelTypes.topCompositionEnd: - return eventTypes.compositionEnd; - case topLevelTypes.topCompositionUpdate: - return eventTypes.compositionUpdate; - } -} - -/** - * Does our fallback best-guess model think this event signifies that - * composition has begun? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackCompositionStart(topLevelType, nativeEvent) { - return topLevelType === topLevelTypes.topKeyDown && nativeEvent.keyCode === START_KEYCODE; -} - -/** - * Does our fallback mode think that this event is the end of composition? - * - * @param {string} topLevelType - * @param {object} nativeEvent - * @return {boolean} - */ -function isFallbackCompositionEnd(topLevelType, nativeEvent) { - switch (topLevelType) { - case topLevelTypes.topKeyUp: - // Command keys insert or clear IME input. - return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1; - case topLevelTypes.topKeyDown: - // Expect IME keyCode on each keydown. If we get any other - // code we must have exited earlier. - return nativeEvent.keyCode !== START_KEYCODE; - case topLevelTypes.topKeyPress: - case topLevelTypes.topMouseDown: - case topLevelTypes.topBlur: - // Events are not possible without cancelling IME. - return true; - default: - return false; - } -} - -/** - * Google Input Tools provides composition data via a CustomEvent, - * with the `data` property populated in the `detail` object. If this - * is available on the event object, use it. If not, this is a plain - * composition event and we have nothing special to extract. - * - * @param {object} nativeEvent - * @return {?string} - */ -function getDataFromCustomEvent(nativeEvent) { - var detail = nativeEvent.detail; - if (typeof detail === 'object' && 'data' in detail) { - return detail.data; - } - return null; -} - -// Track the current IME composition fallback object, if any. -var currentComposition = null; - -/** - * @return {?object} A SyntheticCompositionEvent. - */ -function extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget) { - var eventType; - var fallbackData; - - if (canUseCompositionEvent) { - eventType = getCompositionEventType(topLevelType); - } else if (!currentComposition) { - if (isFallbackCompositionStart(topLevelType, nativeEvent)) { - eventType = eventTypes.compositionStart; - } - } else if (isFallbackCompositionEnd(topLevelType, nativeEvent)) { - eventType = eventTypes.compositionEnd; - } - - if (!eventType) { - return null; - } - - if (useFallbackCompositionData) { - // The current composition is stored statically and must not be - // overwritten while composition continues. - if (!currentComposition && eventType === eventTypes.compositionStart) { - currentComposition = FallbackCompositionState.getPooled(nativeEventTarget); - } else if (eventType === eventTypes.compositionEnd) { - if (currentComposition) { - fallbackData = currentComposition.getData(); - } - } - } - - var event = SyntheticCompositionEvent.getPooled(eventType, targetInst, nativeEvent, nativeEventTarget); - - if (fallbackData) { - // Inject data generated from fallback path into the synthetic event. - // This matches the property of native CompositionEventInterface. - event.data = fallbackData; - } else { - var customData = getDataFromCustomEvent(nativeEvent); - if (customData !== null) { - event.data = customData; - } - } - - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; -} - -/** - * @param {string} topLevelType Record from `EventConstants`. - * @param {object} nativeEvent Native browser event. - * @return {?string} The string corresponding to this `beforeInput` event. - */ -function getNativeBeforeInputChars(topLevelType, nativeEvent) { - switch (topLevelType) { - case topLevelTypes.topCompositionEnd: - return getDataFromCustomEvent(nativeEvent); - case topLevelTypes.topKeyPress: - /** - * If native `textInput` events are available, our goal is to make - * use of them. However, there is a special case: the spacebar key. - * In Webkit, preventing default on a spacebar `textInput` event - * cancels character insertion, but it *also* causes the browser - * to fall back to its default spacebar behavior of scrolling the - * page. - * - * Tracking at: - * https://code.google.com/p/chromium/issues/detail?id=355103 - * - * To avoid this issue, use the keypress event as if no `textInput` - * event is available. - */ - var which = nativeEvent.which; - if (which !== SPACEBAR_CODE) { - return null; - } - - hasSpaceKeypress = true; - return SPACEBAR_CHAR; - - case topLevelTypes.topTextInput: - // Record the characters to be added to the DOM. - var chars = nativeEvent.data; - - // If it's a spacebar character, assume that we have already handled - // it at the keypress level and bail immediately. Android Chrome - // doesn't give us keycodes, so we need to blacklist it. - if (chars === SPACEBAR_CHAR && hasSpaceKeypress) { - return null; - } - - return chars; - - default: - // For other native event types, do nothing. - return null; - } -} - -/** - * For browsers that do not provide the `textInput` event, extract the - * appropriate string to use for SyntheticInputEvent. - * - * @param {string} topLevelType Record from `EventConstants`. - * @param {object} nativeEvent Native browser event. - * @return {?string} The fallback string for this `beforeInput` event. - */ -function getFallbackBeforeInputChars(topLevelType, nativeEvent) { - // If we are currently composing (IME) and using a fallback to do so, - // try to extract the composed characters from the fallback object. - // If composition event is available, we extract a string only at - // compositionevent, otherwise extract it at fallback events. - if (currentComposition) { - if (topLevelType === topLevelTypes.topCompositionEnd || !canUseCompositionEvent && isFallbackCompositionEnd(topLevelType, nativeEvent)) { - var chars = currentComposition.getData(); - FallbackCompositionState.release(currentComposition); - currentComposition = null; - return chars; - } - return null; - } - - switch (topLevelType) { - case topLevelTypes.topPaste: - // If a paste event occurs after a keypress, throw out the input - // chars. Paste events should not lead to BeforeInput events. - return null; - case topLevelTypes.topKeyPress: - /** - * As of v27, Firefox may fire keypress events even when no character - * will be inserted. A few possibilities: - * - * - `which` is `0`. Arrow keys, Esc key, etc. - * - * - `which` is the pressed key code, but no char is available. - * Ex: 'AltGr + d` in Polish. There is no modified character for - * this key combination and no character is inserted into the - * document, but FF fires the keypress for char code `100` anyway. - * No `input` event will occur. - * - * - `which` is the pressed key code, but a command combination is - * being used. Ex: `Cmd+C`. No character is inserted, and no - * `input` event will occur. - */ - if (nativeEvent.which && !isKeypressCommand(nativeEvent)) { - return String.fromCharCode(nativeEvent.which); - } - return null; - case topLevelTypes.topCompositionEnd: - return useFallbackCompositionData ? null : nativeEvent.data; - default: - return null; - } -} - -/** - * Extract a SyntheticInputEvent for `beforeInput`, based on either native - * `textInput` or fallback behavior. - * - * @return {?object} A SyntheticInputEvent. - */ -function extractBeforeInputEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget) { - var chars; - - if (canUseTextInputEvent) { - chars = getNativeBeforeInputChars(topLevelType, nativeEvent); - } else { - chars = getFallbackBeforeInputChars(topLevelType, nativeEvent); - } - - // If no characters are being inserted, no BeforeInput event should - // be fired. - if (!chars) { - return null; - } - - var event = SyntheticInputEvent.getPooled(eventTypes.beforeInput, targetInst, nativeEvent, nativeEventTarget); - - event.data = chars; - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; -} - -/** - * Create an `onBeforeInput` event to match - * http://www.w3.org/TR/2013/WD-DOM-Level-3-Events-20131105/#events-inputevents. - * - * This event plugin is based on the native `textInput` event - * available in Chrome, Safari, Opera, and IE. This event fires after - * `onKeyPress` and `onCompositionEnd`, but before `onInput`. - * - * `beforeInput` is spec'd but not implemented in any browsers, and - * the `input` event does not provide any useful information about what has - * actually been added, contrary to the spec. Thus, `textInput` is the best - * available event to identify the characters that have actually been inserted - * into the target node. - * - * This plugin is also responsible for emitting `composition` events, thus - * allowing us to share composition fallback code for both `beforeInput` and - * `composition` event types. - */ -var BeforeInputEventPlugin = { - - eventTypes: eventTypes, - - extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { - return [extractCompositionEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget), extractBeforeInputEvent(topLevelType, targetInst, nativeEvent, nativeEventTarget)]; - } -}; - -module.exports = BeforeInputEventPlugin; -},{"103":103,"107":107,"148":148,"16":16,"166":166,"20":20,"21":21}],3:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule CSSProperty - */ - -'use strict'; - -/** - * CSS properties which accept numbers but are not in units of "px". - */ - -var isUnitlessNumber = { - animationIterationCount: true, - borderImageOutset: true, - borderImageSlice: true, - borderImageWidth: true, - boxFlex: true, - boxFlexGroup: true, - boxOrdinalGroup: true, - columnCount: true, - flex: true, - flexGrow: true, - flexPositive: true, - flexShrink: true, - flexNegative: true, - flexOrder: true, - gridRow: true, - gridColumn: true, - fontWeight: true, - lineClamp: true, - lineHeight: true, - opacity: true, - order: true, - orphans: true, - tabSize: true, - widows: true, - zIndex: true, - zoom: true, - - // SVG-related properties - fillOpacity: true, - floodOpacity: true, - stopOpacity: true, - strokeDasharray: true, - strokeDashoffset: true, - strokeMiterlimit: true, - strokeOpacity: true, - strokeWidth: true -}; - -/** - * @param {string} prefix vendor-specific prefix, eg: Webkit - * @param {string} key style name, eg: transitionDuration - * @return {string} style name prefixed with `prefix`, properly camelCased, eg: - * WebkitTransitionDuration - */ -function prefixKey(prefix, key) { - return prefix + key.charAt(0).toUpperCase() + key.substring(1); -} - -/** - * Support style names that may come passed in prefixed by adding permutations - * of vendor prefixes. - */ -var prefixes = ['Webkit', 'ms', 'Moz', 'O']; - -// Using Object.keys here, or else the vanilla for-in loop makes IE8 go into an -// infinite loop, because it iterates over the newly added props too. -Object.keys(isUnitlessNumber).forEach(function (prop) { - prefixes.forEach(function (prefix) { - isUnitlessNumber[prefixKey(prefix, prop)] = isUnitlessNumber[prop]; - }); -}); - -/** - * Most style properties can be unset by doing .style[prop] = '' but IE8 - * doesn't like doing that with shorthand properties so for the properties that - * IE8 breaks on, which are listed here, we instead unset each of the - * individual properties. See http://bugs.jquery.com/ticket/12385. - * The 4-value 'clock' properties like margin, padding, border-width seem to - * behave without any problems. Curiously, list-style works too without any - * special prodding. - */ -var shorthandPropertyExpansions = { - background: { - backgroundAttachment: true, - backgroundColor: true, - backgroundImage: true, - backgroundPositionX: true, - backgroundPositionY: true, - backgroundRepeat: true - }, - backgroundPosition: { - backgroundPositionX: true, - backgroundPositionY: true - }, - border: { - borderWidth: true, - borderStyle: true, - borderColor: true - }, - borderBottom: { - borderBottomWidth: true, - borderBottomStyle: true, - borderBottomColor: true - }, - borderLeft: { - borderLeftWidth: true, - borderLeftStyle: true, - borderLeftColor: true - }, - borderRight: { - borderRightWidth: true, - borderRightStyle: true, - borderRightColor: true - }, - borderTop: { - borderTopWidth: true, - borderTopStyle: true, - borderTopColor: true - }, - font: { - fontStyle: true, - fontVariant: true, - fontWeight: true, - fontSize: true, - lineHeight: true, - fontFamily: true - }, - outline: { - outlineWidth: true, - outlineStyle: true, - outlineColor: true - } -}; - -var CSSProperty = { - isUnitlessNumber: isUnitlessNumber, - shorthandPropertyExpansions: shorthandPropertyExpansions -}; - -module.exports = CSSProperty; -},{}],4:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule CSSPropertyOperations - */ - -'use strict'; - -var CSSProperty = _dereq_(3); -var ExecutionEnvironment = _dereq_(148); -var ReactInstrumentation = _dereq_(73); - -var camelizeStyleName = _dereq_(150); -var dangerousStyleValue = _dereq_(121); -var hyphenateStyleName = _dereq_(161); -var memoizeStringOnly = _dereq_(167); -var warning = _dereq_(171); - -var processStyleName = memoizeStringOnly(function (styleName) { - return hyphenateStyleName(styleName); -}); - -var hasShorthandPropertyBug = false; -var styleFloatAccessor = 'cssFloat'; -if (ExecutionEnvironment.canUseDOM) { - var tempStyle = document.createElement('div').style; - try { - // IE8 throws "Invalid argument." if resetting shorthand style properties. - tempStyle.font = ''; - } catch (e) { - hasShorthandPropertyBug = true; - } - // IE8 only supports accessing cssFloat (standard) as styleFloat - if (document.documentElement.style.cssFloat === undefined) { - styleFloatAccessor = 'styleFloat'; - } -} - -if ("development" !== 'production') { - // 'msTransform' is correct, but the other prefixes should be capitalized - var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/; - - // style values shouldn't contain a semicolon - var badStyleValueWithSemicolonPattern = /;\s*$/; - - var warnedStyleNames = {}; - var warnedStyleValues = {}; - var warnedForNaNValue = false; - - var warnHyphenatedStyleName = function (name, owner) { - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { - return; - } - - warnedStyleNames[name] = true; - "development" !== 'production' ? warning(false, 'Unsupported style property %s. Did you mean %s?%s', name, camelizeStyleName(name), checkRenderMessage(owner)) : void 0; - }; - - var warnBadVendoredStyleName = function (name, owner) { - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { - return; - } - - warnedStyleNames[name] = true; - "development" !== 'production' ? warning(false, 'Unsupported vendor-prefixed style property %s. Did you mean %s?%s', name, name.charAt(0).toUpperCase() + name.slice(1), checkRenderMessage(owner)) : void 0; - }; - - var warnStyleValueWithSemicolon = function (name, value, owner) { - if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) { - return; - } - - warnedStyleValues[value] = true; - "development" !== 'production' ? warning(false, 'Style property values shouldn\'t contain a semicolon.%s ' + 'Try "%s: %s" instead.', checkRenderMessage(owner), name, value.replace(badStyleValueWithSemicolonPattern, '')) : void 0; - }; - - var warnStyleValueIsNaN = function (name, value, owner) { - if (warnedForNaNValue) { - return; - } - - warnedForNaNValue = true; - "development" !== 'production' ? warning(false, '`NaN` is an invalid value for the `%s` css style property.%s', name, checkRenderMessage(owner)) : void 0; - }; - - var checkRenderMessage = function (owner) { - if (owner) { - var name = owner.getName(); - if (name) { - return ' Check the render method of `' + name + '`.'; - } - } - return ''; - }; - - /** - * @param {string} name - * @param {*} value - * @param {ReactDOMComponent} component - */ - var warnValidStyle = function (name, value, component) { - var owner; - if (component) { - owner = component._currentElement._owner; - } - if (name.indexOf('-') > -1) { - warnHyphenatedStyleName(name, owner); - } else if (badVendoredStyleNamePattern.test(name)) { - warnBadVendoredStyleName(name, owner); - } else if (badStyleValueWithSemicolonPattern.test(value)) { - warnStyleValueWithSemicolon(name, value, owner); - } - - if (typeof value === 'number' && isNaN(value)) { - warnStyleValueIsNaN(name, value, owner); - } - }; -} - -/** - * Operations for dealing with CSS properties. - */ -var CSSPropertyOperations = { - - /** - * Serializes a mapping of style properties for use as inline styles: - * - * > createMarkupForStyles({width: '200px', height: 0}) - * "width:200px;height:0;" - * - * Undefined values are ignored so that declarative programming is easier. - * The result should be HTML-escaped before insertion into the DOM. - * - * @param {object} styles - * @param {ReactDOMComponent} component - * @return {?string} - */ - createMarkupForStyles: function (styles, component) { - var serialized = ''; - for (var styleName in styles) { - if (!styles.hasOwnProperty(styleName)) { - continue; - } - var styleValue = styles[styleName]; - if ("development" !== 'production') { - warnValidStyle(styleName, styleValue, component); - } - if (styleValue != null) { - serialized += processStyleName(styleName) + ':'; - serialized += dangerousStyleValue(styleName, styleValue, component) + ';'; - } - } - return serialized || null; - }, - - /** - * Sets the value for multiple styles on a node. If a value is specified as - * '' (empty string), the corresponding style property will be unset. - * - * @param {DOMElement} node - * @param {object} styles - * @param {ReactDOMComponent} component - */ - setValueForStyles: function (node, styles, component) { - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(component._debugID, 'update styles', styles); - } - - var style = node.style; - for (var styleName in styles) { - if (!styles.hasOwnProperty(styleName)) { - continue; - } - if ("development" !== 'production') { - warnValidStyle(styleName, styles[styleName], component); - } - var styleValue = dangerousStyleValue(styleName, styles[styleName], component); - if (styleName === 'float' || styleName === 'cssFloat') { - styleName = styleFloatAccessor; - } - if (styleValue) { - style[styleName] = styleValue; - } else { - var expansion = hasShorthandPropertyBug && CSSProperty.shorthandPropertyExpansions[styleName]; - if (expansion) { - // Shorthand property that IE8 won't like unsetting, so unset each - // component to placate it - for (var individualStyleName in expansion) { - style[individualStyleName] = ''; - } - } else { - style[styleName] = ''; - } - } - } - } - -}; - -module.exports = CSSPropertyOperations; -},{"121":121,"148":148,"150":150,"161":161,"167":167,"171":171,"3":3,"73":73}],5:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule CallbackQueue - */ - -'use strict'; - -var _prodInvariant = _dereq_(140), - _assign = _dereq_(172); - -var PooledClass = _dereq_(25); - -var invariant = _dereq_(162); - -/** - * A specialized pseudo-event module to help keep track of components waiting to - * be notified when their DOM representations are available for use. - * - * This implements `PooledClass`, so you should never need to instantiate this. - * Instead, use `CallbackQueue.getPooled()`. - * - * @class ReactMountReady - * @implements PooledClass - * @internal - */ -function CallbackQueue() { - this._callbacks = null; - this._contexts = null; -} - -_assign(CallbackQueue.prototype, { - - /** - * Enqueues a callback to be invoked when `notifyAll` is invoked. - * - * @param {function} callback Invoked when `notifyAll` is invoked. - * @param {?object} context Context to call `callback` with. - * @internal - */ - enqueue: function (callback, context) { - this._callbacks = this._callbacks || []; - this._contexts = this._contexts || []; - this._callbacks.push(callback); - this._contexts.push(context); - }, - - /** - * Invokes all enqueued callbacks and clears the queue. This is invoked after - * the DOM representation of a component has been created or updated. - * - * @internal - */ - notifyAll: function () { - var callbacks = this._callbacks; - var contexts = this._contexts; - if (callbacks) { - !(callbacks.length === contexts.length) ? "development" !== 'production' ? invariant(false, 'Mismatched list of contexts in callback queue') : _prodInvariant('24') : void 0; - this._callbacks = null; - this._contexts = null; - for (var i = 0; i < callbacks.length; i++) { - callbacks[i].call(contexts[i]); - } - callbacks.length = 0; - contexts.length = 0; - } - }, - - checkpoint: function () { - return this._callbacks ? this._callbacks.length : 0; - }, - - rollback: function (len) { - if (this._callbacks) { - this._callbacks.length = len; - this._contexts.length = len; - } - }, - - /** - * Resets the internal queue. - * - * @internal - */ - reset: function () { - this._callbacks = null; - this._contexts = null; - }, - - /** - * `PooledClass` looks for this. - */ - destructor: function () { - this.reset(); - } - -}); - -PooledClass.addPoolingTo(CallbackQueue); - -module.exports = CallbackQueue; -},{"140":140,"162":162,"172":172,"25":25}],6:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ChangeEventPlugin - */ - -'use strict'; - -var EventConstants = _dereq_(16); -var EventPluginHub = _dereq_(17); -var EventPropagators = _dereq_(20); -var ExecutionEnvironment = _dereq_(148); -var ReactDOMComponentTree = _dereq_(42); -var ReactUpdates = _dereq_(96); -var SyntheticEvent = _dereq_(105); - -var getEventTarget = _dereq_(129); -var isEventSupported = _dereq_(136); -var isTextInputElement = _dereq_(137); -var keyOf = _dereq_(166); - -var topLevelTypes = EventConstants.topLevelTypes; - -var eventTypes = { - change: { - phasedRegistrationNames: { - bubbled: keyOf({ onChange: null }), - captured: keyOf({ onChangeCapture: null }) - }, - dependencies: [topLevelTypes.topBlur, topLevelTypes.topChange, topLevelTypes.topClick, topLevelTypes.topFocus, topLevelTypes.topInput, topLevelTypes.topKeyDown, topLevelTypes.topKeyUp, topLevelTypes.topSelectionChange] - } -}; - -/** - * For IE shims - */ -var activeElement = null; -var activeElementInst = null; -var activeElementValue = null; -var activeElementValueProp = null; - -/** - * SECTION: handle `change` event - */ -function shouldUseChangeEvent(elem) { - var nodeName = elem.nodeName && elem.nodeName.toLowerCase(); - return nodeName === 'select' || nodeName === 'input' && elem.type === 'file'; -} - -var doesChangeEventBubble = false; -if (ExecutionEnvironment.canUseDOM) { - // See `handleChange` comment below - doesChangeEventBubble = isEventSupported('change') && (!document.documentMode || document.documentMode > 8); -} - -function manualDispatchChangeEvent(nativeEvent) { - var event = SyntheticEvent.getPooled(eventTypes.change, activeElementInst, nativeEvent, getEventTarget(nativeEvent)); - EventPropagators.accumulateTwoPhaseDispatches(event); - - // If change and propertychange bubbled, we'd just bind to it like all the - // other events and have it go through ReactBrowserEventEmitter. Since it - // doesn't, we manually listen for the events and so we have to enqueue and - // process the abstract event manually. - // - // Batching is necessary here in order to ensure that all event handlers run - // before the next rerender (including event handlers attached to ancestor - // elements instead of directly on the input). Without this, controlled - // components don't work properly in conjunction with event bubbling because - // the component is rerendered and the value reverted before all the event - // handlers can run. See https://github.com/facebook/react/issues/708. - ReactUpdates.batchedUpdates(runEventInBatch, event); -} - -function runEventInBatch(event) { - EventPluginHub.enqueueEvents(event); - EventPluginHub.processEventQueue(false); -} - -function startWatchingForChangeEventIE8(target, targetInst) { - activeElement = target; - activeElementInst = targetInst; - activeElement.attachEvent('onchange', manualDispatchChangeEvent); -} - -function stopWatchingForChangeEventIE8() { - if (!activeElement) { - return; - } - activeElement.detachEvent('onchange', manualDispatchChangeEvent); - activeElement = null; - activeElementInst = null; -} - -function getTargetInstForChangeEvent(topLevelType, targetInst) { - if (topLevelType === topLevelTypes.topChange) { - return targetInst; - } -} -function handleEventsForChangeEventIE8(topLevelType, target, targetInst) { - if (topLevelType === topLevelTypes.topFocus) { - // stopWatching() should be a noop here but we call it just in case we - // missed a blur event somehow. - stopWatchingForChangeEventIE8(); - startWatchingForChangeEventIE8(target, targetInst); - } else if (topLevelType === topLevelTypes.topBlur) { - stopWatchingForChangeEventIE8(); - } -} - -/** - * SECTION: handle `input` event - */ -var isInputEventSupported = false; -if (ExecutionEnvironment.canUseDOM) { - // IE9 claims to support the input event but fails to trigger it when - // deleting text, so we ignore its input events. - // IE10+ fire input events to often, such when a placeholder - // changes or when an input with a placeholder is focused. - isInputEventSupported = isEventSupported('input') && (!document.documentMode || document.documentMode > 11); -} - -/** - * (For IE <=11) Replacement getter/setter for the `value` property that gets - * set on the active element. - */ -var newValueProp = { - get: function () { - return activeElementValueProp.get.call(this); - }, - set: function (val) { - // Cast to a string so we can do equality checks. - activeElementValue = '' + val; - activeElementValueProp.set.call(this, val); - } -}; - -/** - * (For IE <=11) Starts tracking propertychange events on the passed-in element - * and override the value property so that we can distinguish user events from - * value changes in JS. - */ -function startWatchingForValueChange(target, targetInst) { - activeElement = target; - activeElementInst = targetInst; - activeElementValue = target.value; - activeElementValueProp = Object.getOwnPropertyDescriptor(target.constructor.prototype, 'value'); - - // Not guarded in a canDefineProperty check: IE8 supports defineProperty only - // on DOM elements - Object.defineProperty(activeElement, 'value', newValueProp); - if (activeElement.attachEvent) { - activeElement.attachEvent('onpropertychange', handlePropertyChange); - } else { - activeElement.addEventListener('propertychange', handlePropertyChange, false); - } -} - -/** - * (For IE <=11) Removes the event listeners from the currently-tracked element, - * if any exists. - */ -function stopWatchingForValueChange() { - if (!activeElement) { - return; - } - - // delete restores the original property definition - delete activeElement.value; - - if (activeElement.detachEvent) { - activeElement.detachEvent('onpropertychange', handlePropertyChange); - } else { - activeElement.removeEventListener('propertychange', handlePropertyChange, false); - } - - activeElement = null; - activeElementInst = null; - activeElementValue = null; - activeElementValueProp = null; -} - -/** - * (For IE <=11) Handles a propertychange event, sending a `change` event if - * the value of the active element has changed. - */ -function handlePropertyChange(nativeEvent) { - if (nativeEvent.propertyName !== 'value') { - return; - } - var value = nativeEvent.srcElement.value; - if (value === activeElementValue) { - return; - } - activeElementValue = value; - - manualDispatchChangeEvent(nativeEvent); -} - -/** - * If a `change` event should be fired, returns the target's ID. - */ -function getTargetInstForInputEvent(topLevelType, targetInst) { - if (topLevelType === topLevelTypes.topInput) { - // In modern browsers (i.e., not IE8 or IE9), the input event is exactly - // what we want so fall through here and trigger an abstract event - return targetInst; - } -} - -function handleEventsForInputEventIE(topLevelType, target, targetInst) { - if (topLevelType === topLevelTypes.topFocus) { - // In IE8, we can capture almost all .value changes by adding a - // propertychange handler and looking for events with propertyName - // equal to 'value' - // In IE9-11, propertychange fires for most input events but is buggy and - // doesn't fire when text is deleted, but conveniently, selectionchange - // appears to fire in all of the remaining cases so we catch those and - // forward the event if the value has changed - // In either case, we don't want to call the event handler if the value - // is changed from JS so we redefine a setter for `.value` that updates - // our activeElementValue variable, allowing us to ignore those changes - // - // stopWatching() should be a noop here but we call it just in case we - // missed a blur event somehow. - stopWatchingForValueChange(); - startWatchingForValueChange(target, targetInst); - } else if (topLevelType === topLevelTypes.topBlur) { - stopWatchingForValueChange(); - } -} - -// For IE8 and IE9. -function getTargetInstForInputEventIE(topLevelType, targetInst) { - if (topLevelType === topLevelTypes.topSelectionChange || topLevelType === topLevelTypes.topKeyUp || topLevelType === topLevelTypes.topKeyDown) { - // On the selectionchange event, the target is just document which isn't - // helpful for us so just check activeElement instead. - // - // 99% of the time, keydown and keyup aren't necessary. IE8 fails to fire - // propertychange on the first input event after setting `value` from a - // script and fires only keydown, keypress, keyup. Catching keyup usually - // gets it and catching keydown lets us fire an event for the first - // keystroke if user does a key repeat (it'll be a little delayed: right - // before the second keystroke). Other input methods (e.g., paste) seem to - // fire selectionchange normally. - if (activeElement && activeElement.value !== activeElementValue) { - activeElementValue = activeElement.value; - return activeElementInst; - } - } -} - -/** - * SECTION: handle `click` event - */ -function shouldUseClickEvent(elem) { - // Use the `click` event to detect changes to checkbox and radio inputs. - // This approach works across all browsers, whereas `change` does not fire - // until `blur` in IE8. - return elem.nodeName && elem.nodeName.toLowerCase() === 'input' && (elem.type === 'checkbox' || elem.type === 'radio'); -} - -function getTargetInstForClickEvent(topLevelType, targetInst) { - if (topLevelType === topLevelTypes.topClick) { - return targetInst; - } -} - -/** - * This plugin creates an `onChange` event that normalizes change events - * across form elements. This event fires at a time when it's possible to - * change the element's value without seeing a flicker. - * - * Supported elements are: - * - input (see `isTextInputElement`) - * - textarea - * - select - */ -var ChangeEventPlugin = { - - eventTypes: eventTypes, - - extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { - var targetNode = targetInst ? ReactDOMComponentTree.getNodeFromInstance(targetInst) : window; - - var getTargetInstFunc, handleEventFunc; - if (shouldUseChangeEvent(targetNode)) { - if (doesChangeEventBubble) { - getTargetInstFunc = getTargetInstForChangeEvent; - } else { - handleEventFunc = handleEventsForChangeEventIE8; - } - } else if (isTextInputElement(targetNode)) { - if (isInputEventSupported) { - getTargetInstFunc = getTargetInstForInputEvent; - } else { - getTargetInstFunc = getTargetInstForInputEventIE; - handleEventFunc = handleEventsForInputEventIE; - } - } else if (shouldUseClickEvent(targetNode)) { - getTargetInstFunc = getTargetInstForClickEvent; - } - - if (getTargetInstFunc) { - var inst = getTargetInstFunc(topLevelType, targetInst); - if (inst) { - var event = SyntheticEvent.getPooled(eventTypes.change, inst, nativeEvent, nativeEventTarget); - event.type = 'change'; - EventPropagators.accumulateTwoPhaseDispatches(event); - return event; - } - } - - if (handleEventFunc) { - handleEventFunc(topLevelType, targetNode, targetInst); - } - } - -}; - -module.exports = ChangeEventPlugin; -},{"105":105,"129":129,"136":136,"137":137,"148":148,"16":16,"166":166,"17":17,"20":20,"42":42,"96":96}],7:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule DOMChildrenOperations - */ - -'use strict'; - -var DOMLazyTree = _dereq_(8); -var Danger = _dereq_(12); -var ReactMultiChildUpdateTypes = _dereq_(78); -var ReactDOMComponentTree = _dereq_(42); -var ReactInstrumentation = _dereq_(73); - -var createMicrosoftUnsafeLocalFunction = _dereq_(120); -var setInnerHTML = _dereq_(142); -var setTextContent = _dereq_(143); - -function getNodeAfter(parentNode, node) { - // Special case for text components, which return [open, close] comments - // from getHostNode. - if (Array.isArray(node)) { - node = node[1]; - } - return node ? node.nextSibling : parentNode.firstChild; -} - -/** - * Inserts `childNode` as a child of `parentNode` at the `index`. - * - * @param {DOMElement} parentNode Parent node in which to insert. - * @param {DOMElement} childNode Child node to insert. - * @param {number} index Index at which to insert the child. - * @internal - */ -var insertChildAt = createMicrosoftUnsafeLocalFunction(function (parentNode, childNode, referenceNode) { - // We rely exclusively on `insertBefore(node, null)` instead of also using - // `appendChild(node)`. (Using `undefined` is not allowed by all browsers so - // we are careful to use `null`.) - parentNode.insertBefore(childNode, referenceNode); -}); - -function insertLazyTreeChildAt(parentNode, childTree, referenceNode) { - DOMLazyTree.insertTreeBefore(parentNode, childTree, referenceNode); -} - -function moveChild(parentNode, childNode, referenceNode) { - if (Array.isArray(childNode)) { - moveDelimitedText(parentNode, childNode[0], childNode[1], referenceNode); - } else { - insertChildAt(parentNode, childNode, referenceNode); - } -} - -function removeChild(parentNode, childNode) { - if (Array.isArray(childNode)) { - var closingComment = childNode[1]; - childNode = childNode[0]; - removeDelimitedText(parentNode, childNode, closingComment); - parentNode.removeChild(closingComment); - } - parentNode.removeChild(childNode); -} - -function moveDelimitedText(parentNode, openingComment, closingComment, referenceNode) { - var node = openingComment; - while (true) { - var nextNode = node.nextSibling; - insertChildAt(parentNode, node, referenceNode); - if (node === closingComment) { - break; - } - node = nextNode; - } -} - -function removeDelimitedText(parentNode, startNode, closingComment) { - while (true) { - var node = startNode.nextSibling; - if (node === closingComment) { - // The closing comment is removed by ReactMultiChild. - break; - } else { - parentNode.removeChild(node); - } - } -} - -function replaceDelimitedText(openingComment, closingComment, stringText) { - var parentNode = openingComment.parentNode; - var nodeAfterComment = openingComment.nextSibling; - if (nodeAfterComment === closingComment) { - // There are no text nodes between the opening and closing comments; insert - // a new one if stringText isn't empty. - if (stringText) { - insertChildAt(parentNode, document.createTextNode(stringText), nodeAfterComment); - } - } else { - if (stringText) { - // Set the text content of the first node after the opening comment, and - // remove all following nodes up until the closing comment. - setTextContent(nodeAfterComment, stringText); - removeDelimitedText(parentNode, nodeAfterComment, closingComment); - } else { - removeDelimitedText(parentNode, openingComment, closingComment); - } - } - - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(ReactDOMComponentTree.getInstanceFromNode(openingComment)._debugID, 'replace text', stringText); - } -} - -var dangerouslyReplaceNodeWithMarkup = Danger.dangerouslyReplaceNodeWithMarkup; -if ("development" !== 'production') { - dangerouslyReplaceNodeWithMarkup = function (oldChild, markup, prevInstance) { - Danger.dangerouslyReplaceNodeWithMarkup(oldChild, markup); - if (prevInstance._debugID !== 0) { - ReactInstrumentation.debugTool.onHostOperation(prevInstance._debugID, 'replace with', markup.toString()); - } else { - var nextInstance = ReactDOMComponentTree.getInstanceFromNode(markup.node); - if (nextInstance._debugID !== 0) { - ReactInstrumentation.debugTool.onHostOperation(nextInstance._debugID, 'mount', markup.toString()); - } - } - }; -} - -/** - * Operations for updating with DOM children. - */ -var DOMChildrenOperations = { - - dangerouslyReplaceNodeWithMarkup: dangerouslyReplaceNodeWithMarkup, - - replaceDelimitedText: replaceDelimitedText, - - /** - * Updates a component's children by processing a series of updates. The - * update configurations are each expected to have a `parentNode` property. - * - * @param {array} updates List of update configurations. - * @internal - */ - processUpdates: function (parentNode, updates) { - if ("development" !== 'production') { - var parentNodeDebugID = ReactDOMComponentTree.getInstanceFromNode(parentNode)._debugID; - } - - for (var k = 0; k < updates.length; k++) { - var update = updates[k]; - switch (update.type) { - case ReactMultiChildUpdateTypes.INSERT_MARKUP: - insertLazyTreeChildAt(parentNode, update.content, getNodeAfter(parentNode, update.afterNode)); - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(parentNodeDebugID, 'insert child', { toIndex: update.toIndex, content: update.content.toString() }); - } - break; - case ReactMultiChildUpdateTypes.MOVE_EXISTING: - moveChild(parentNode, update.fromNode, getNodeAfter(parentNode, update.afterNode)); - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(parentNodeDebugID, 'move child', { fromIndex: update.fromIndex, toIndex: update.toIndex }); - } - break; - case ReactMultiChildUpdateTypes.SET_MARKUP: - setInnerHTML(parentNode, update.content); - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(parentNodeDebugID, 'replace children', update.content.toString()); - } - break; - case ReactMultiChildUpdateTypes.TEXT_CONTENT: - setTextContent(parentNode, update.content); - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(parentNodeDebugID, 'replace text', update.content.toString()); - } - break; - case ReactMultiChildUpdateTypes.REMOVE_NODE: - removeChild(parentNode, update.fromNode); - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(parentNodeDebugID, 'remove child', { fromIndex: update.fromIndex }); - } - break; - } - } - } - -}; - -module.exports = DOMChildrenOperations; -},{"12":12,"120":120,"142":142,"143":143,"42":42,"73":73,"78":78,"8":8}],8:[function(_dereq_,module,exports){ -/** - * Copyright 2015-present, 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. - * - * @providesModule DOMLazyTree - */ - -'use strict'; - -var DOMNamespaces = _dereq_(9); -var setInnerHTML = _dereq_(142); - -var createMicrosoftUnsafeLocalFunction = _dereq_(120); -var setTextContent = _dereq_(143); - -var ELEMENT_NODE_TYPE = 1; -var DOCUMENT_FRAGMENT_NODE_TYPE = 11; - -/** - * In IE (8-11) and Edge, appending nodes with no children is dramatically - * faster than appending a full subtree, so we essentially queue up the - * .appendChild calls here and apply them so each node is added to its parent - * before any children are added. - * - * In other browsers, doing so is slower or neutral compared to the other order - * (in Firefox, twice as slow) so we only do this inversion in IE. - * - * See https://github.com/spicyj/innerhtml-vs-createelement-vs-clonenode. - */ -var enableLazy = typeof document !== 'undefined' && typeof document.documentMode === 'number' || typeof navigator !== 'undefined' && typeof navigator.userAgent === 'string' && /\bEdge\/\d/.test(navigator.userAgent); - -function insertTreeChildren(tree) { - if (!enableLazy) { - return; - } - var node = tree.node; - var children = tree.children; - if (children.length) { - for (var i = 0; i < children.length; i++) { - insertTreeBefore(node, children[i], null); - } - } else if (tree.html != null) { - setInnerHTML(node, tree.html); - } else if (tree.text != null) { - setTextContent(node, tree.text); - } -} - -var insertTreeBefore = createMicrosoftUnsafeLocalFunction(function (parentNode, tree, referenceNode) { - // DocumentFragments aren't actually part of the DOM after insertion so - // appending children won't update the DOM. We need to ensure the fragment - // is properly populated first, breaking out of our lazy approach for just - // this level. Also, some plugins (like Flash Player) will read - // nodes immediately upon insertion into the DOM, so - // must also be populated prior to insertion into the DOM. - if (tree.node.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE || tree.node.nodeType === ELEMENT_NODE_TYPE && tree.node.nodeName.toLowerCase() === 'object' && (tree.node.namespaceURI == null || tree.node.namespaceURI === DOMNamespaces.html)) { - insertTreeChildren(tree); - parentNode.insertBefore(tree.node, referenceNode); - } else { - parentNode.insertBefore(tree.node, referenceNode); - insertTreeChildren(tree); - } -}); - -function replaceChildWithTree(oldNode, newTree) { - oldNode.parentNode.replaceChild(newTree.node, oldNode); - insertTreeChildren(newTree); -} - -function queueChild(parentTree, childTree) { - if (enableLazy) { - parentTree.children.push(childTree); - } else { - parentTree.node.appendChild(childTree.node); - } -} - -function queueHTML(tree, html) { - if (enableLazy) { - tree.html = html; - } else { - setInnerHTML(tree.node, html); - } -} - -function queueText(tree, text) { - if (enableLazy) { - tree.text = text; - } else { - setTextContent(tree.node, text); - } -} - -function toString() { - return this.node.nodeName; -} - -function DOMLazyTree(node) { - return { - node: node, - children: [], - html: null, - text: null, - toString: toString - }; -} - -DOMLazyTree.insertTreeBefore = insertTreeBefore; -DOMLazyTree.replaceChildWithTree = replaceChildWithTree; -DOMLazyTree.queueChild = queueChild; -DOMLazyTree.queueHTML = queueHTML; -DOMLazyTree.queueText = queueText; - -module.exports = DOMLazyTree; -},{"120":120,"142":142,"143":143,"9":9}],9:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule DOMNamespaces - */ - -'use strict'; - -var DOMNamespaces = { - html: 'http://www.w3.org/1999/xhtml', - mathml: 'http://www.w3.org/1998/Math/MathML', - svg: 'http://www.w3.org/2000/svg' -}; - -module.exports = DOMNamespaces; -},{}],10:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule DOMProperty - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var invariant = _dereq_(162); - -function checkMask(value, bitmask) { - return (value & bitmask) === bitmask; -} - -var DOMPropertyInjection = { - /** - * Mapping from normalized, camelcased property names to a configuration that - * specifies how the associated DOM property should be accessed or rendered. - */ - MUST_USE_PROPERTY: 0x1, - HAS_BOOLEAN_VALUE: 0x4, - HAS_NUMERIC_VALUE: 0x8, - HAS_POSITIVE_NUMERIC_VALUE: 0x10 | 0x8, - HAS_OVERLOADED_BOOLEAN_VALUE: 0x20, - - /** - * Inject some specialized knowledge about the DOM. This takes a config object - * with the following properties: - * - * isCustomAttribute: function that given an attribute name will return true - * if it can be inserted into the DOM verbatim. Useful for data-* or aria-* - * attributes where it's impossible to enumerate all of the possible - * attribute names, - * - * Properties: object mapping DOM property name to one of the - * DOMPropertyInjection constants or null. If your attribute isn't in here, - * it won't get written to the DOM. - * - * DOMAttributeNames: object mapping React attribute name to the DOM - * attribute name. Attribute names not specified use the **lowercase** - * normalized name. - * - * DOMAttributeNamespaces: object mapping React attribute name to the DOM - * attribute namespace URL. (Attribute names not specified use no namespace.) - * - * DOMPropertyNames: similar to DOMAttributeNames but for DOM properties. - * Property names not specified use the normalized name. - * - * DOMMutationMethods: Properties that require special mutation methods. If - * `value` is undefined, the mutation method should unset the property. - * - * @param {object} domPropertyConfig the config as described above. - */ - injectDOMPropertyConfig: function (domPropertyConfig) { - var Injection = DOMPropertyInjection; - var Properties = domPropertyConfig.Properties || {}; - var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {}; - var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {}; - var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {}; - var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {}; - - if (domPropertyConfig.isCustomAttribute) { - DOMProperty._isCustomAttributeFunctions.push(domPropertyConfig.isCustomAttribute); - } - - for (var propName in Properties) { - !!DOMProperty.properties.hasOwnProperty(propName) ? "development" !== 'production' ? invariant(false, 'injectDOMPropertyConfig(...): You\'re trying to inject DOM property \'%s\' which has already been injected. You may be accidentally injecting the same DOM property config twice, or you may be injecting two configs that have conflicting property names.', propName) : _prodInvariant('48', propName) : void 0; - - var lowerCased = propName.toLowerCase(); - var propConfig = Properties[propName]; - - var propertyInfo = { - attributeName: lowerCased, - attributeNamespace: null, - propertyName: propName, - mutationMethod: null, - - mustUseProperty: checkMask(propConfig, Injection.MUST_USE_PROPERTY), - hasBooleanValue: checkMask(propConfig, Injection.HAS_BOOLEAN_VALUE), - hasNumericValue: checkMask(propConfig, Injection.HAS_NUMERIC_VALUE), - hasPositiveNumericValue: checkMask(propConfig, Injection.HAS_POSITIVE_NUMERIC_VALUE), - hasOverloadedBooleanValue: checkMask(propConfig, Injection.HAS_OVERLOADED_BOOLEAN_VALUE) - }; - !(propertyInfo.hasBooleanValue + propertyInfo.hasNumericValue + propertyInfo.hasOverloadedBooleanValue <= 1) ? "development" !== 'production' ? invariant(false, 'DOMProperty: Value can be one of boolean, overloaded boolean, or numeric value, but not a combination: %s', propName) : _prodInvariant('50', propName) : void 0; - - if ("development" !== 'production') { - DOMProperty.getPossibleStandardName[lowerCased] = propName; - } - - if (DOMAttributeNames.hasOwnProperty(propName)) { - var attributeName = DOMAttributeNames[propName]; - propertyInfo.attributeName = attributeName; - if ("development" !== 'production') { - DOMProperty.getPossibleStandardName[attributeName] = propName; - } - } - - if (DOMAttributeNamespaces.hasOwnProperty(propName)) { - propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName]; - } - - if (DOMPropertyNames.hasOwnProperty(propName)) { - propertyInfo.propertyName = DOMPropertyNames[propName]; - } - - if (DOMMutationMethods.hasOwnProperty(propName)) { - propertyInfo.mutationMethod = DOMMutationMethods[propName]; - } - - DOMProperty.properties[propName] = propertyInfo; - } - } -}; - -/* eslint-disable max-len */ -var ATTRIBUTE_NAME_START_CHAR = ':A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD'; -/* eslint-enable max-len */ - -/** - * DOMProperty exports lookup objects that can be used like functions: - * - * > DOMProperty.isValid['id'] - * true - * > DOMProperty.isValid['foobar'] - * undefined - * - * Although this may be confusing, it performs better in general. - * - * @see http://jsperf.com/key-exists - * @see http://jsperf.com/key-missing - */ -var DOMProperty = { - - ID_ATTRIBUTE_NAME: 'data-reactid', - ROOT_ATTRIBUTE_NAME: 'data-reactroot', - - ATTRIBUTE_NAME_START_CHAR: ATTRIBUTE_NAME_START_CHAR, - ATTRIBUTE_NAME_CHAR: ATTRIBUTE_NAME_START_CHAR + '\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040', - - /** - * Map from property "standard name" to an object with info about how to set - * the property in the DOM. Each object contains: - * - * attributeName: - * Used when rendering markup or with `*Attribute()`. - * attributeNamespace - * propertyName: - * Used on DOM node instances. (This includes properties that mutate due to - * external factors.) - * mutationMethod: - * If non-null, used instead of the property or `setAttribute()` after - * initial render. - * mustUseProperty: - * Whether the property must be accessed and mutated as an object property. - * hasBooleanValue: - * Whether the property should be removed when set to a falsey value. - * hasNumericValue: - * Whether the property must be numeric or parse as a numeric and should be - * removed when set to a falsey value. - * hasPositiveNumericValue: - * Whether the property must be positive numeric or parse as a positive - * numeric and should be removed when set to a falsey value. - * hasOverloadedBooleanValue: - * Whether the property can be used as a flag as well as with a value. - * Removed when strictly equal to false; present without a value when - * strictly equal to true; present with a value otherwise. - */ - properties: {}, - - /** - * Mapping from lowercase property names to the properly cased version, used - * to warn in the case of missing properties. Available only in __DEV__. - * @type {Object} - */ - getPossibleStandardName: "development" !== 'production' ? {} : null, - - /** - * All of the isCustomAttribute() functions that have been injected. - */ - _isCustomAttributeFunctions: [], - - /** - * Checks whether a property name is a custom attribute. - * @method - */ - isCustomAttribute: function (attributeName) { - for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) { - var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i]; - if (isCustomAttributeFn(attributeName)) { - return true; - } - } - return false; - }, - - injection: DOMPropertyInjection -}; - -module.exports = DOMProperty; -},{"140":140,"162":162}],11:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule DOMPropertyOperations - */ - -'use strict'; - -var DOMProperty = _dereq_(10); -var ReactDOMComponentTree = _dereq_(42); -var ReactInstrumentation = _dereq_(73); - -var quoteAttributeValueForBrowser = _dereq_(139); -var warning = _dereq_(171); - -var VALID_ATTRIBUTE_NAME_REGEX = new RegExp('^[' + DOMProperty.ATTRIBUTE_NAME_START_CHAR + '][' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$'); -var illegalAttributeNameCache = {}; -var validatedAttributeNameCache = {}; - -function isAttributeNameSafe(attributeName) { - if (validatedAttributeNameCache.hasOwnProperty(attributeName)) { - return true; - } - if (illegalAttributeNameCache.hasOwnProperty(attributeName)) { - return false; - } - if (VALID_ATTRIBUTE_NAME_REGEX.test(attributeName)) { - validatedAttributeNameCache[attributeName] = true; - return true; - } - illegalAttributeNameCache[attributeName] = true; - "development" !== 'production' ? warning(false, 'Invalid attribute name: `%s`', attributeName) : void 0; - return false; -} - -function shouldIgnoreValue(propertyInfo, value) { - return value == null || propertyInfo.hasBooleanValue && !value || propertyInfo.hasNumericValue && isNaN(value) || propertyInfo.hasPositiveNumericValue && value < 1 || propertyInfo.hasOverloadedBooleanValue && value === false; -} - -/** - * Operations for dealing with DOM properties. - */ -var DOMPropertyOperations = { - - /** - * Creates markup for the ID property. - * - * @param {string} id Unescaped ID. - * @return {string} Markup string. - */ - createMarkupForID: function (id) { - return DOMProperty.ID_ATTRIBUTE_NAME + '=' + quoteAttributeValueForBrowser(id); - }, - - setAttributeForID: function (node, id) { - node.setAttribute(DOMProperty.ID_ATTRIBUTE_NAME, id); - }, - - createMarkupForRoot: function () { - return DOMProperty.ROOT_ATTRIBUTE_NAME + '=""'; - }, - - setAttributeForRoot: function (node) { - node.setAttribute(DOMProperty.ROOT_ATTRIBUTE_NAME, ''); - }, - - /** - * Creates markup for a property. - * - * @param {string} name - * @param {*} value - * @return {?string} Markup string, or null if the property was invalid. - */ - createMarkupForProperty: function (name, value) { - var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; - if (propertyInfo) { - if (shouldIgnoreValue(propertyInfo, value)) { - return ''; - } - var attributeName = propertyInfo.attributeName; - if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) { - return attributeName + '=""'; - } - return attributeName + '=' + quoteAttributeValueForBrowser(value); - } else if (DOMProperty.isCustomAttribute(name)) { - if (value == null) { - return ''; - } - return name + '=' + quoteAttributeValueForBrowser(value); - } - return null; - }, - - /** - * Creates markup for a custom property. - * - * @param {string} name - * @param {*} value - * @return {string} Markup string, or empty string if the property was invalid. - */ - createMarkupForCustomAttribute: function (name, value) { - if (!isAttributeNameSafe(name) || value == null) { - return ''; - } - return name + '=' + quoteAttributeValueForBrowser(value); - }, - - /** - * Sets the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - * @param {*} value - */ - setValueForProperty: function (node, name, value) { - var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; - if (propertyInfo) { - var mutationMethod = propertyInfo.mutationMethod; - if (mutationMethod) { - mutationMethod(node, value); - } else if (shouldIgnoreValue(propertyInfo, value)) { - this.deleteValueForProperty(node, name); - return; - } else if (propertyInfo.mustUseProperty) { - // Contrary to `setAttribute`, object properties are properly - // `toString`ed by IE8/9. - node[propertyInfo.propertyName] = value; - } else { - var attributeName = propertyInfo.attributeName; - var namespace = propertyInfo.attributeNamespace; - // `setAttribute` with objects becomes only `[object]` in IE8/9, - // ('' + value) makes it output the correct toString()-value. - if (namespace) { - node.setAttributeNS(namespace, attributeName, '' + value); - } else if (propertyInfo.hasBooleanValue || propertyInfo.hasOverloadedBooleanValue && value === true) { - node.setAttribute(attributeName, ''); - } else { - node.setAttribute(attributeName, '' + value); - } - } - } else if (DOMProperty.isCustomAttribute(name)) { - DOMPropertyOperations.setValueForAttribute(node, name, value); - return; - } - - if ("development" !== 'production') { - var payload = {}; - payload[name] = value; - ReactInstrumentation.debugTool.onHostOperation(ReactDOMComponentTree.getInstanceFromNode(node)._debugID, 'update attribute', payload); - } - }, - - setValueForAttribute: function (node, name, value) { - if (!isAttributeNameSafe(name)) { - return; - } - if (value == null) { - node.removeAttribute(name); - } else { - node.setAttribute(name, '' + value); - } - - if ("development" !== 'production') { - var payload = {}; - payload[name] = value; - ReactInstrumentation.debugTool.onHostOperation(ReactDOMComponentTree.getInstanceFromNode(node)._debugID, 'update attribute', payload); - } - }, - - /** - * Deletes an attributes from a node. - * - * @param {DOMElement} node - * @param {string} name - */ - deleteValueForAttribute: function (node, name) { - node.removeAttribute(name); - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(ReactDOMComponentTree.getInstanceFromNode(node)._debugID, 'remove attribute', name); - } - }, - - /** - * Deletes the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - */ - deleteValueForProperty: function (node, name) { - var propertyInfo = DOMProperty.properties.hasOwnProperty(name) ? DOMProperty.properties[name] : null; - if (propertyInfo) { - var mutationMethod = propertyInfo.mutationMethod; - if (mutationMethod) { - mutationMethod(node, undefined); - } else if (propertyInfo.mustUseProperty) { - var propName = propertyInfo.propertyName; - if (propertyInfo.hasBooleanValue) { - node[propName] = false; - } else { - node[propName] = ''; - } - } else { - node.removeAttribute(propertyInfo.attributeName); - } - } else if (DOMProperty.isCustomAttribute(name)) { - node.removeAttribute(name); - } - - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onHostOperation(ReactDOMComponentTree.getInstanceFromNode(node)._debugID, 'remove attribute', name); - } - } - -}; - -module.exports = DOMPropertyOperations; -},{"10":10,"139":139,"171":171,"42":42,"73":73}],12:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule Danger - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var DOMLazyTree = _dereq_(8); -var ExecutionEnvironment = _dereq_(148); - -var createNodesFromMarkup = _dereq_(153); -var emptyFunction = _dereq_(154); -var invariant = _dereq_(162); - -var Danger = { - - /** - * Replaces a node with a string of markup at its current position within its - * parent. The markup must render into a single root node. - * - * @param {DOMElement} oldChild Child node to replace. - * @param {string} markup Markup to render in place of the child node. - * @internal - */ - dangerouslyReplaceNodeWithMarkup: function (oldChild, markup) { - !ExecutionEnvironment.canUseDOM ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot render markup in a worker thread. Make sure `window` and `document` are available globally before requiring React when unit testing or use ReactDOMServer.renderToString() for server rendering.') : _prodInvariant('56') : void 0; - !markup ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.') : _prodInvariant('57') : void 0; - !(oldChild.nodeName !== 'HTML') ? "development" !== 'production' ? invariant(false, 'dangerouslyReplaceNodeWithMarkup(...): Cannot replace markup of the node. This is because browser quirks make this unreliable and/or slow. If you want to render to the root you must use server rendering. See ReactDOMServer.renderToString().') : _prodInvariant('58') : void 0; - - if (typeof markup === 'string') { - var newChild = createNodesFromMarkup(markup, emptyFunction)[0]; - oldChild.parentNode.replaceChild(newChild, oldChild); - } else { - DOMLazyTree.replaceChildWithTree(oldChild, markup); - } - } - -}; - -module.exports = Danger; -},{"140":140,"148":148,"153":153,"154":154,"162":162,"8":8}],13:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule DefaultEventPluginOrder - */ - -'use strict'; - -var keyOf = _dereq_(166); - -/** - * Module that is injectable into `EventPluginHub`, that specifies a - * deterministic ordering of `EventPlugin`s. A convenient way to reason about - * plugins, without having to package every one of them. This is better than - * having plugins be ordered in the same order that they are injected because - * that ordering would be influenced by the packaging order. - * `ResponderEventPlugin` must occur before `SimpleEventPlugin` so that - * preventing default on events is convenient in `SimpleEventPlugin` handlers. - */ -var DefaultEventPluginOrder = [keyOf({ ResponderEventPlugin: null }), keyOf({ SimpleEventPlugin: null }), keyOf({ TapEventPlugin: null }), keyOf({ EnterLeaveEventPlugin: null }), keyOf({ ChangeEventPlugin: null }), keyOf({ SelectEventPlugin: null }), keyOf({ BeforeInputEventPlugin: null })]; - -module.exports = DefaultEventPluginOrder; -},{"166":166}],14:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule DisabledInputUtils - */ - -'use strict'; - -var disableableMouseListenerNames = { - onClick: true, - onDoubleClick: true, - onMouseDown: true, - onMouseMove: true, - onMouseUp: true, - - onClickCapture: true, - onDoubleClickCapture: true, - onMouseDownCapture: true, - onMouseMoveCapture: true, - onMouseUpCapture: true -}; - -/** - * Implements a host component that does not receive mouse events - * when `disabled` is set. - */ -var DisabledInputUtils = { - getHostProps: function (inst, props) { - if (!props.disabled) { - return props; - } - - // Copy the props, except the mouse listeners - var hostProps = {}; - for (var key in props) { - if (!disableableMouseListenerNames[key] && props.hasOwnProperty(key)) { - hostProps[key] = props[key]; - } - } - - return hostProps; - } -}; - -module.exports = DisabledInputUtils; -},{}],15:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule EnterLeaveEventPlugin - */ - -'use strict'; - -var EventConstants = _dereq_(16); -var EventPropagators = _dereq_(20); -var ReactDOMComponentTree = _dereq_(42); -var SyntheticMouseEvent = _dereq_(109); - -var keyOf = _dereq_(166); - -var topLevelTypes = EventConstants.topLevelTypes; - -var eventTypes = { - mouseEnter: { - registrationName: keyOf({ onMouseEnter: null }), - dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver] - }, - mouseLeave: { - registrationName: keyOf({ onMouseLeave: null }), - dependencies: [topLevelTypes.topMouseOut, topLevelTypes.topMouseOver] - } -}; - -var EnterLeaveEventPlugin = { - - eventTypes: eventTypes, - - /** - * For almost every interaction we care about, there will be both a top-level - * `mouseover` and `mouseout` event that occurs. Only use `mouseout` so that - * we do not extract duplicate events. However, moving the mouse into the - * browser from outside will not fire a `mouseout` event. In this case, we use - * the `mouseover` top-level event. - */ - extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { - if (topLevelType === topLevelTypes.topMouseOver && (nativeEvent.relatedTarget || nativeEvent.fromElement)) { - return null; - } - if (topLevelType !== topLevelTypes.topMouseOut && topLevelType !== topLevelTypes.topMouseOver) { - // Must not be a mouse in or mouse out - ignoring. - return null; - } - - var win; - if (nativeEventTarget.window === nativeEventTarget) { - // `nativeEventTarget` is probably a window object. - win = nativeEventTarget; - } else { - // TODO: Figure out why `ownerDocument` is sometimes undefined in IE8. - var doc = nativeEventTarget.ownerDocument; - if (doc) { - win = doc.defaultView || doc.parentWindow; - } else { - win = window; - } - } - - var from; - var to; - if (topLevelType === topLevelTypes.topMouseOut) { - from = targetInst; - var related = nativeEvent.relatedTarget || nativeEvent.toElement; - to = related ? ReactDOMComponentTree.getClosestInstanceFromNode(related) : null; - } else { - // Moving to a node from outside the window. - from = null; - to = targetInst; - } - - if (from === to) { - // Nothing pertains to our managed components. - return null; - } - - var fromNode = from == null ? win : ReactDOMComponentTree.getNodeFromInstance(from); - var toNode = to == null ? win : ReactDOMComponentTree.getNodeFromInstance(to); - - var leave = SyntheticMouseEvent.getPooled(eventTypes.mouseLeave, from, nativeEvent, nativeEventTarget); - leave.type = 'mouseleave'; - leave.target = fromNode; - leave.relatedTarget = toNode; - - var enter = SyntheticMouseEvent.getPooled(eventTypes.mouseEnter, to, nativeEvent, nativeEventTarget); - enter.type = 'mouseenter'; - enter.target = toNode; - enter.relatedTarget = fromNode; - - EventPropagators.accumulateEnterLeaveDispatches(leave, enter, from, to); - - return [leave, enter]; - } - -}; - -module.exports = EnterLeaveEventPlugin; -},{"109":109,"16":16,"166":166,"20":20,"42":42}],16:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule EventConstants - */ - -'use strict'; - -var keyMirror = _dereq_(165); - -var PropagationPhases = keyMirror({ bubbled: null, captured: null }); - -/** - * Types of raw signals from the browser caught at the top level. - */ -var topLevelTypes = keyMirror({ - topAbort: null, - topAnimationEnd: null, - topAnimationIteration: null, - topAnimationStart: null, - topBlur: null, - topCanPlay: null, - topCanPlayThrough: null, - topChange: null, - topClick: null, - topCompositionEnd: null, - topCompositionStart: null, - topCompositionUpdate: null, - topContextMenu: null, - topCopy: null, - topCut: null, - topDoubleClick: null, - topDrag: null, - topDragEnd: null, - topDragEnter: null, - topDragExit: null, - topDragLeave: null, - topDragOver: null, - topDragStart: null, - topDrop: null, - topDurationChange: null, - topEmptied: null, - topEncrypted: null, - topEnded: null, - topError: null, - topFocus: null, - topInput: null, - topInvalid: null, - topKeyDown: null, - topKeyPress: null, - topKeyUp: null, - topLoad: null, - topLoadedData: null, - topLoadedMetadata: null, - topLoadStart: null, - topMouseDown: null, - topMouseMove: null, - topMouseOut: null, - topMouseOver: null, - topMouseUp: null, - topPaste: null, - topPause: null, - topPlay: null, - topPlaying: null, - topProgress: null, - topRateChange: null, - topReset: null, - topScroll: null, - topSeeked: null, - topSeeking: null, - topSelectionChange: null, - topStalled: null, - topSubmit: null, - topSuspend: null, - topTextInput: null, - topTimeUpdate: null, - topTouchCancel: null, - topTouchEnd: null, - topTouchMove: null, - topTouchStart: null, - topTransitionEnd: null, - topVolumeChange: null, - topWaiting: null, - topWheel: null -}); - -var EventConstants = { - topLevelTypes: topLevelTypes, - PropagationPhases: PropagationPhases -}; - -module.exports = EventConstants; -},{"165":165}],17:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule EventPluginHub - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var EventPluginRegistry = _dereq_(18); -var EventPluginUtils = _dereq_(19); -var ReactErrorUtils = _dereq_(64); - -var accumulateInto = _dereq_(116); -var forEachAccumulated = _dereq_(125); -var invariant = _dereq_(162); - -/** - * Internal store for event listeners - */ -var listenerBank = {}; - -/** - * Internal queue of events that have accumulated their dispatches and are - * waiting to have their dispatches executed. - */ -var eventQueue = null; - -/** - * Dispatches an event and releases it back into the pool, unless persistent. - * - * @param {?object} event Synthetic event to be dispatched. - * @param {boolean} simulated If the event is simulated (changes exn behavior) - * @private - */ -var executeDispatchesAndRelease = function (event, simulated) { - if (event) { - EventPluginUtils.executeDispatchesInOrder(event, simulated); - - if (!event.isPersistent()) { - event.constructor.release(event); - } - } -}; -var executeDispatchesAndReleaseSimulated = function (e) { - return executeDispatchesAndRelease(e, true); -}; -var executeDispatchesAndReleaseTopLevel = function (e) { - return executeDispatchesAndRelease(e, false); -}; - -var getDictionaryKey = function (inst) { - // Prevents V8 performance issue: - // https://github.com/facebook/react/pull/7232 - return '.' + inst._rootNodeID; -}; - -/** - * This is a unified interface for event plugins to be installed and configured. - * - * Event plugins can implement the following properties: - * - * `extractEvents` {function(string, DOMEventTarget, string, object): *} - * Required. When a top-level event is fired, this method is expected to - * extract synthetic events that will in turn be queued and dispatched. - * - * `eventTypes` {object} - * Optional, plugins that fire events must publish a mapping of registration - * names that are used to register listeners. Values of this mapping must - * be objects that contain `registrationName` or `phasedRegistrationNames`. - * - * `executeDispatch` {function(object, function, string)} - * Optional, allows plugins to override how an event gets dispatched. By - * default, the listener is simply invoked. - * - * Each plugin that is injected into `EventsPluginHub` is immediately operable. - * - * @public - */ -var EventPluginHub = { - - /** - * Methods for injecting dependencies. - */ - injection: { - - /** - * @param {array} InjectedEventPluginOrder - * @public - */ - injectEventPluginOrder: EventPluginRegistry.injectEventPluginOrder, - - /** - * @param {object} injectedNamesToPlugins Map from names to plugin modules. - */ - injectEventPluginsByName: EventPluginRegistry.injectEventPluginsByName - - }, - - /** - * Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent. - * - * @param {object} inst The instance, which is the source of events. - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @param {function} listener The callback to store. - */ - putListener: function (inst, registrationName, listener) { - !(typeof listener === 'function') ? "development" !== 'production' ? invariant(false, 'Expected %s listener to be a function, instead got type %s', registrationName, typeof listener) : _prodInvariant('94', registrationName, typeof listener) : void 0; - - var key = getDictionaryKey(inst); - var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); - bankForRegistrationName[key] = listener; - - var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; - if (PluginModule && PluginModule.didPutListener) { - PluginModule.didPutListener(inst, registrationName, listener); - } - }, - - /** - * @param {object} inst The instance, which is the source of events. - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @return {?function} The stored callback. - */ - getListener: function (inst, registrationName) { - var bankForRegistrationName = listenerBank[registrationName]; - var key = getDictionaryKey(inst); - return bankForRegistrationName && bankForRegistrationName[key]; - }, - - /** - * Deletes a listener from the registration bank. - * - * @param {object} inst The instance, which is the source of events. - * @param {string} registrationName Name of listener (e.g. `onClick`). - */ - deleteListener: function (inst, registrationName) { - var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; - if (PluginModule && PluginModule.willDeleteListener) { - PluginModule.willDeleteListener(inst, registrationName); - } - - var bankForRegistrationName = listenerBank[registrationName]; - // TODO: This should never be null -- when is it? - if (bankForRegistrationName) { - var key = getDictionaryKey(inst); - delete bankForRegistrationName[key]; - } - }, - - /** - * Deletes all listeners for the DOM element with the supplied ID. - * - * @param {object} inst The instance, which is the source of events. - */ - deleteAllListeners: function (inst) { - var key = getDictionaryKey(inst); - for (var registrationName in listenerBank) { - if (!listenerBank.hasOwnProperty(registrationName)) { - continue; - } - - if (!listenerBank[registrationName][key]) { - continue; - } - - var PluginModule = EventPluginRegistry.registrationNameModules[registrationName]; - if (PluginModule && PluginModule.willDeleteListener) { - PluginModule.willDeleteListener(inst, registrationName); - } - - delete listenerBank[registrationName][key]; - } - }, - - /** - * Allows registered plugins an opportunity to extract events from top-level - * native browser events. - * - * @return {*} An accumulation of synthetic events. - * @internal - */ - extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) { - var events; - var plugins = EventPluginRegistry.plugins; - for (var i = 0; i < plugins.length; i++) { - // Not every plugin in the ordering may be loaded at runtime. - var possiblePlugin = plugins[i]; - if (possiblePlugin) { - var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget); - if (extractedEvents) { - events = accumulateInto(events, extractedEvents); - } - } - } - return events; - }, - - /** - * Enqueues a synthetic event that should be dispatched when - * `processEventQueue` is invoked. - * - * @param {*} events An accumulation of synthetic events. - * @internal - */ - enqueueEvents: function (events) { - if (events) { - eventQueue = accumulateInto(eventQueue, events); - } - }, - - /** - * Dispatches all synthetic events on the event queue. - * - * @internal - */ - processEventQueue: function (simulated) { - // Set `eventQueue` to null before processing it so that we can tell if more - // events get enqueued while processing. - var processingEventQueue = eventQueue; - eventQueue = null; - if (simulated) { - forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated); - } else { - forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel); - } - !!eventQueue ? "development" !== 'production' ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing an event queue. Support for this has not yet been implemented.') : _prodInvariant('95') : void 0; - // This would be a good time to rethrow if any of the event handlers threw. - ReactErrorUtils.rethrowCaughtError(); - }, - - /** - * These are needed for tests only. Do not use! - */ - __purge: function () { - listenerBank = {}; - }, - - __getListenerBank: function () { - return listenerBank; - } - -}; - -module.exports = EventPluginHub; -},{"116":116,"125":125,"140":140,"162":162,"18":18,"19":19,"64":64}],18:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule EventPluginRegistry - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var invariant = _dereq_(162); - -/** - * Injectable ordering of event plugins. - */ -var EventPluginOrder = null; - -/** - * Injectable mapping from names to event plugin modules. - */ -var namesToPlugins = {}; - -/** - * Recomputes the plugin list using the injected plugins and plugin ordering. - * - * @private - */ -function recomputePluginOrdering() { - if (!EventPluginOrder) { - // Wait until an `EventPluginOrder` is injected. - return; - } - for (var pluginName in namesToPlugins) { - var PluginModule = namesToPlugins[pluginName]; - var pluginIndex = EventPluginOrder.indexOf(pluginName); - !(pluginIndex > -1) ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugins that do not exist in the plugin ordering, `%s`.', pluginName) : _prodInvariant('96', pluginName) : void 0; - if (EventPluginRegistry.plugins[pluginIndex]) { - continue; - } - !PluginModule.extractEvents ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Event plugins must implement an `extractEvents` method, but `%s` does not.', pluginName) : _prodInvariant('97', pluginName) : void 0; - EventPluginRegistry.plugins[pluginIndex] = PluginModule; - var publishedEvents = PluginModule.eventTypes; - for (var eventName in publishedEvents) { - !publishEventForPlugin(publishedEvents[eventName], PluginModule, eventName) ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.', eventName, pluginName) : _prodInvariant('98', eventName, pluginName) : void 0; - } - } -} - -/** - * Publishes an event so that it can be dispatched by the supplied plugin. - * - * @param {object} dispatchConfig Dispatch configuration for the event. - * @param {object} PluginModule Plugin publishing the event. - * @return {boolean} True if the event was successfully published. - * @private - */ -function publishEventForPlugin(dispatchConfig, PluginModule, eventName) { - !!EventPluginRegistry.eventNameDispatchConfigs.hasOwnProperty(eventName) ? "development" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same event name, `%s`.', eventName) : _prodInvariant('99', eventName) : void 0; - EventPluginRegistry.eventNameDispatchConfigs[eventName] = dispatchConfig; - - var phasedRegistrationNames = dispatchConfig.phasedRegistrationNames; - if (phasedRegistrationNames) { - for (var phaseName in phasedRegistrationNames) { - if (phasedRegistrationNames.hasOwnProperty(phaseName)) { - var phasedRegistrationName = phasedRegistrationNames[phaseName]; - publishRegistrationName(phasedRegistrationName, PluginModule, eventName); - } - } - return true; - } else if (dispatchConfig.registrationName) { - publishRegistrationName(dispatchConfig.registrationName, PluginModule, eventName); - return true; - } - return false; -} - -/** - * Publishes a registration name that is used to identify dispatched events and - * can be used with `EventPluginHub.putListener` to register listeners. - * - * @param {string} registrationName Registration name to add. - * @param {object} PluginModule Plugin publishing the event. - * @private - */ -function publishRegistrationName(registrationName, PluginModule, eventName) { - !!EventPluginRegistry.registrationNameModules[registrationName] ? "development" !== 'production' ? invariant(false, 'EventPluginHub: More than one plugin attempted to publish the same registration name, `%s`.', registrationName) : _prodInvariant('100', registrationName) : void 0; - EventPluginRegistry.registrationNameModules[registrationName] = PluginModule; - EventPluginRegistry.registrationNameDependencies[registrationName] = PluginModule.eventTypes[eventName].dependencies; - - if ("development" !== 'production') { - var lowerCasedName = registrationName.toLowerCase(); - EventPluginRegistry.possibleRegistrationNames[lowerCasedName] = registrationName; - - if (registrationName === 'onDoubleClick') { - EventPluginRegistry.possibleRegistrationNames.ondblclick = registrationName; - } - } -} - -/** - * Registers plugins so that they can extract and dispatch events. - * - * @see {EventPluginHub} - */ -var EventPluginRegistry = { - - /** - * Ordered list of injected plugins. - */ - plugins: [], - - /** - * Mapping from event name to dispatch config - */ - eventNameDispatchConfigs: {}, - - /** - * Mapping from registration name to plugin module - */ - registrationNameModules: {}, - - /** - * Mapping from registration name to event name - */ - registrationNameDependencies: {}, - - /** - * Mapping from lowercase registration names to the properly cased version, - * used to warn in the case of missing event handlers. Available - * only in __DEV__. - * @type {Object} - */ - possibleRegistrationNames: "development" !== 'production' ? {} : null, - - /** - * Injects an ordering of plugins (by plugin name). This allows the ordering - * to be decoupled from injection of the actual plugins so that ordering is - * always deterministic regardless of packaging, on-the-fly injection, etc. - * - * @param {array} InjectedEventPluginOrder - * @internal - * @see {EventPluginHub.injection.injectEventPluginOrder} - */ - injectEventPluginOrder: function (InjectedEventPluginOrder) { - !!EventPluginOrder ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject event plugin ordering more than once. You are likely trying to load more than one copy of React.') : _prodInvariant('101') : void 0; - // Clone the ordering so it cannot be dynamically mutated. - EventPluginOrder = Array.prototype.slice.call(InjectedEventPluginOrder); - recomputePluginOrdering(); - }, - - /** - * Injects plugins to be used by `EventPluginHub`. The plugin names must be - * in the ordering injected by `injectEventPluginOrder`. - * - * Plugins can be injected as part of page initialization or on-the-fly. - * - * @param {object} injectedNamesToPlugins Map from names to plugin modules. - * @internal - * @see {EventPluginHub.injection.injectEventPluginsByName} - */ - injectEventPluginsByName: function (injectedNamesToPlugins) { - var isOrderingDirty = false; - for (var pluginName in injectedNamesToPlugins) { - if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) { - continue; - } - var PluginModule = injectedNamesToPlugins[pluginName]; - if (!namesToPlugins.hasOwnProperty(pluginName) || namesToPlugins[pluginName] !== PluginModule) { - !!namesToPlugins[pluginName] ? "development" !== 'production' ? invariant(false, 'EventPluginRegistry: Cannot inject two different event plugins using the same name, `%s`.', pluginName) : _prodInvariant('102', pluginName) : void 0; - namesToPlugins[pluginName] = PluginModule; - isOrderingDirty = true; - } - } - if (isOrderingDirty) { - recomputePluginOrdering(); - } - }, - - /** - * Looks up the plugin for the supplied event. - * - * @param {object} event A synthetic event. - * @return {?object} The plugin that created the supplied event. - * @internal - */ - getPluginModuleForEvent: function (event) { - var dispatchConfig = event.dispatchConfig; - if (dispatchConfig.registrationName) { - return EventPluginRegistry.registrationNameModules[dispatchConfig.registrationName] || null; - } - for (var phase in dispatchConfig.phasedRegistrationNames) { - if (!dispatchConfig.phasedRegistrationNames.hasOwnProperty(phase)) { - continue; - } - var PluginModule = EventPluginRegistry.registrationNameModules[dispatchConfig.phasedRegistrationNames[phase]]; - if (PluginModule) { - return PluginModule; - } - } - return null; - }, - - /** - * Exposed for unit testing. - * @private - */ - _resetEventPlugins: function () { - EventPluginOrder = null; - for (var pluginName in namesToPlugins) { - if (namesToPlugins.hasOwnProperty(pluginName)) { - delete namesToPlugins[pluginName]; - } - } - EventPluginRegistry.plugins.length = 0; - - var eventNameDispatchConfigs = EventPluginRegistry.eventNameDispatchConfigs; - for (var eventName in eventNameDispatchConfigs) { - if (eventNameDispatchConfigs.hasOwnProperty(eventName)) { - delete eventNameDispatchConfigs[eventName]; - } - } - - var registrationNameModules = EventPluginRegistry.registrationNameModules; - for (var registrationName in registrationNameModules) { - if (registrationNameModules.hasOwnProperty(registrationName)) { - delete registrationNameModules[registrationName]; - } - } - - if ("development" !== 'production') { - var possibleRegistrationNames = EventPluginRegistry.possibleRegistrationNames; - for (var lowerCasedName in possibleRegistrationNames) { - if (possibleRegistrationNames.hasOwnProperty(lowerCasedName)) { - delete possibleRegistrationNames[lowerCasedName]; - } - } - } - } - -}; - -module.exports = EventPluginRegistry; -},{"140":140,"162":162}],19:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule EventPluginUtils - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var EventConstants = _dereq_(16); -var ReactErrorUtils = _dereq_(64); - -var invariant = _dereq_(162); -var warning = _dereq_(171); - -/** - * Injected dependencies: - */ - -/** - * - `ComponentTree`: [required] Module that can convert between React instances - * and actual node references. - */ -var ComponentTree; -var TreeTraversal; -var injection = { - injectComponentTree: function (Injected) { - ComponentTree = Injected; - if ("development" !== 'production') { - "development" !== 'production' ? warning(Injected && Injected.getNodeFromInstance && Injected.getInstanceFromNode, 'EventPluginUtils.injection.injectComponentTree(...): Injected ' + 'module is missing getNodeFromInstance or getInstanceFromNode.') : void 0; - } - }, - injectTreeTraversal: function (Injected) { - TreeTraversal = Injected; - if ("development" !== 'production') { - "development" !== 'production' ? warning(Injected && Injected.isAncestor && Injected.getLowestCommonAncestor, 'EventPluginUtils.injection.injectTreeTraversal(...): Injected ' + 'module is missing isAncestor or getLowestCommonAncestor.') : void 0; - } - } -}; - -var topLevelTypes = EventConstants.topLevelTypes; - -function isEndish(topLevelType) { - return topLevelType === topLevelTypes.topMouseUp || topLevelType === topLevelTypes.topTouchEnd || topLevelType === topLevelTypes.topTouchCancel; -} - -function isMoveish(topLevelType) { - return topLevelType === topLevelTypes.topMouseMove || topLevelType === topLevelTypes.topTouchMove; -} -function isStartish(topLevelType) { - return topLevelType === topLevelTypes.topMouseDown || topLevelType === topLevelTypes.topTouchStart; -} - -var validateEventDispatches; -if ("development" !== 'production') { - validateEventDispatches = function (event) { - var dispatchListeners = event._dispatchListeners; - var dispatchInstances = event._dispatchInstances; - - var listenersIsArr = Array.isArray(dispatchListeners); - var listenersLen = listenersIsArr ? dispatchListeners.length : dispatchListeners ? 1 : 0; - - var instancesIsArr = Array.isArray(dispatchInstances); - var instancesLen = instancesIsArr ? dispatchInstances.length : dispatchInstances ? 1 : 0; - - "development" !== 'production' ? warning(instancesIsArr === listenersIsArr && instancesLen === listenersLen, 'EventPluginUtils: Invalid `event`.') : void 0; - }; -} - -/** - * Dispatch the event to the listener. - * @param {SyntheticEvent} event SyntheticEvent to handle - * @param {boolean} simulated If the event is simulated (changes exn behavior) - * @param {function} listener Application-level callback - * @param {*} inst Internal component instance - */ -function executeDispatch(event, simulated, listener, inst) { - var type = event.type || 'unknown-event'; - event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); - if (simulated) { - ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); - } else { - ReactErrorUtils.invokeGuardedCallback(type, listener, event); - } - event.currentTarget = null; -} - -/** - * Standard/simple iteration through an event's collected dispatches. - */ -function executeDispatchesInOrder(event, simulated) { - var dispatchListeners = event._dispatchListeners; - var dispatchInstances = event._dispatchInstances; - if ("development" !== 'production') { - validateEventDispatches(event); - } - if (Array.isArray(dispatchListeners)) { - for (var i = 0; i < dispatchListeners.length; i++) { - if (event.isPropagationStopped()) { - break; - } - // Listeners and Instances are two parallel arrays that are always in sync. - executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]); - } - } else if (dispatchListeners) { - executeDispatch(event, simulated, dispatchListeners, dispatchInstances); - } - event._dispatchListeners = null; - event._dispatchInstances = null; -} - -/** - * Standard/simple iteration through an event's collected dispatches, but stops - * at the first dispatch execution returning true, and returns that id. - * - * @return {?string} id of the first dispatch execution who's listener returns - * true, or null if no listener returned true. - */ -function executeDispatchesInOrderStopAtTrueImpl(event) { - var dispatchListeners = event._dispatchListeners; - var dispatchInstances = event._dispatchInstances; - if ("development" !== 'production') { - validateEventDispatches(event); - } - if (Array.isArray(dispatchListeners)) { - for (var i = 0; i < dispatchListeners.length; i++) { - if (event.isPropagationStopped()) { - break; - } - // Listeners and Instances are two parallel arrays that are always in sync. - if (dispatchListeners[i](event, dispatchInstances[i])) { - return dispatchInstances[i]; - } - } - } else if (dispatchListeners) { - if (dispatchListeners(event, dispatchInstances)) { - return dispatchInstances; - } - } - return null; -} - -/** - * @see executeDispatchesInOrderStopAtTrueImpl - */ -function executeDispatchesInOrderStopAtTrue(event) { - var ret = executeDispatchesInOrderStopAtTrueImpl(event); - event._dispatchInstances = null; - event._dispatchListeners = null; - return ret; -} - -/** - * Execution of a "direct" dispatch - there must be at most one dispatch - * accumulated on the event or it is considered an error. It doesn't really make - * sense for an event with multiple dispatches (bubbled) to keep track of the - * return values at each dispatch execution, but it does tend to make sense when - * dealing with "direct" dispatches. - * - * @return {*} The return value of executing the single dispatch. - */ -function executeDirectDispatch(event) { - if ("development" !== 'production') { - validateEventDispatches(event); - } - var dispatchListener = event._dispatchListeners; - var dispatchInstance = event._dispatchInstances; - !!Array.isArray(dispatchListener) ? "development" !== 'production' ? invariant(false, 'executeDirectDispatch(...): Invalid `event`.') : _prodInvariant('103') : void 0; - event.currentTarget = dispatchListener ? EventPluginUtils.getNodeFromInstance(dispatchInstance) : null; - var res = dispatchListener ? dispatchListener(event) : null; - event.currentTarget = null; - event._dispatchListeners = null; - event._dispatchInstances = null; - return res; -} - -/** - * @param {SyntheticEvent} event - * @return {boolean} True iff number of dispatches accumulated is greater than 0. - */ -function hasDispatches(event) { - return !!event._dispatchListeners; -} - -/** - * General utilities that are useful in creating custom Event Plugins. - */ -var EventPluginUtils = { - isEndish: isEndish, - isMoveish: isMoveish, - isStartish: isStartish, - - executeDirectDispatch: executeDirectDispatch, - executeDispatchesInOrder: executeDispatchesInOrder, - executeDispatchesInOrderStopAtTrue: executeDispatchesInOrderStopAtTrue, - hasDispatches: hasDispatches, - - getInstanceFromNode: function (node) { - return ComponentTree.getInstanceFromNode(node); - }, - getNodeFromInstance: function (node) { - return ComponentTree.getNodeFromInstance(node); - }, - isAncestor: function (a, b) { - return TreeTraversal.isAncestor(a, b); - }, - getLowestCommonAncestor: function (a, b) { - return TreeTraversal.getLowestCommonAncestor(a, b); - }, - getParentInstance: function (inst) { - return TreeTraversal.getParentInstance(inst); - }, - traverseTwoPhase: function (target, fn, arg) { - return TreeTraversal.traverseTwoPhase(target, fn, arg); - }, - traverseEnterLeave: function (from, to, fn, argFrom, argTo) { - return TreeTraversal.traverseEnterLeave(from, to, fn, argFrom, argTo); - }, - - injection: injection -}; - -module.exports = EventPluginUtils; -},{"140":140,"16":16,"162":162,"171":171,"64":64}],20:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule EventPropagators - */ - -'use strict'; - -var EventConstants = _dereq_(16); -var EventPluginHub = _dereq_(17); -var EventPluginUtils = _dereq_(19); - -var accumulateInto = _dereq_(116); -var forEachAccumulated = _dereq_(125); -var warning = _dereq_(171); - -var PropagationPhases = EventConstants.PropagationPhases; -var getListener = EventPluginHub.getListener; - -/** - * Some event types have a notion of different registration names for different - * "phases" of propagation. This finds listeners by a given phase. - */ -function listenerAtPhase(inst, event, propagationPhase) { - var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; - return getListener(inst, registrationName); -} - -/** - * Tags a `SyntheticEvent` with dispatched listeners. Creating this function - * here, allows us to not have to bind or create functions for each event. - * Mutating the event's members allows us to not have to create a wrapping - * "dispatch" object that pairs the event with the listener. - */ -function accumulateDirectionalDispatches(inst, upwards, event) { - if ("development" !== 'production') { - "development" !== 'production' ? warning(inst, 'Dispatching inst must not be null') : void 0; - } - var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured; - var listener = listenerAtPhase(inst, event, phase); - if (listener) { - event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); - } -} - -/** - * Collect dispatches (must be entirely collected before dispatching - see unit - * tests). Lazily allocate the array to conserve memory. We must loop through - * each event and perform the traversal for each one. We cannot perform a - * single traversal for the entire collection of events because each event may - * have a different target. - */ -function accumulateTwoPhaseDispatchesSingle(event) { - if (event && event.dispatchConfig.phasedRegistrationNames) { - EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event); - } -} - -/** - * Same as `accumulateTwoPhaseDispatchesSingle`, but skips over the targetID. - */ -function accumulateTwoPhaseDispatchesSingleSkipTarget(event) { - if (event && event.dispatchConfig.phasedRegistrationNames) { - var targetInst = event._targetInst; - var parentInst = targetInst ? EventPluginUtils.getParentInstance(targetInst) : null; - EventPluginUtils.traverseTwoPhase(parentInst, accumulateDirectionalDispatches, event); - } -} - -/** - * Accumulates without regard to direction, does not look for phased - * registration names. Same as `accumulateDirectDispatchesSingle` but without - * requiring that the `dispatchMarker` be the same as the dispatched ID. - */ -function accumulateDispatches(inst, ignoredDirection, event) { - if (event && event.dispatchConfig.registrationName) { - var registrationName = event.dispatchConfig.registrationName; - var listener = getListener(inst, registrationName); - if (listener) { - event._dispatchListeners = accumulateInto(event._dispatchListeners, listener); - event._dispatchInstances = accumulateInto(event._dispatchInstances, inst); - } - } -} - -/** - * Accumulates dispatches on an `SyntheticEvent`, but only for the - * `dispatchMarker`. - * @param {SyntheticEvent} event - */ -function accumulateDirectDispatchesSingle(event) { - if (event && event.dispatchConfig.registrationName) { - accumulateDispatches(event._targetInst, null, event); - } -} - -function accumulateTwoPhaseDispatches(events) { - forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle); -} - -function accumulateTwoPhaseDispatchesSkipTarget(events) { - forEachAccumulated(events, accumulateTwoPhaseDispatchesSingleSkipTarget); -} - -function accumulateEnterLeaveDispatches(leave, enter, from, to) { - EventPluginUtils.traverseEnterLeave(from, to, accumulateDispatches, leave, enter); -} - -function accumulateDirectDispatches(events) { - forEachAccumulated(events, accumulateDirectDispatchesSingle); -} - -/** - * A small set of propagation patterns, each of which will accept a small amount - * of information, and generate a set of "dispatch ready event objects" - which - * are sets of events that have already been annotated with a set of dispatched - * listener functions/ids. The API is designed this way to discourage these - * propagation strategies from actually executing the dispatches, since we - * always want to collect the entire set of dispatches before executing event a - * single one. - * - * @constructor EventPropagators - */ -var EventPropagators = { - accumulateTwoPhaseDispatches: accumulateTwoPhaseDispatches, - accumulateTwoPhaseDispatchesSkipTarget: accumulateTwoPhaseDispatchesSkipTarget, - accumulateDirectDispatches: accumulateDirectDispatches, - accumulateEnterLeaveDispatches: accumulateEnterLeaveDispatches -}; - -module.exports = EventPropagators; -},{"116":116,"125":125,"16":16,"17":17,"171":171,"19":19}],21:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule FallbackCompositionState - */ - -'use strict'; - -var _assign = _dereq_(172); - -var PooledClass = _dereq_(25); - -var getTextContentAccessor = _dereq_(133); - -/** - * This helper class stores information about text content of a target node, - * allowing comparison of content before and after a given event. - * - * Identify the node where selection currently begins, then observe - * both its text content and its current position in the DOM. Since the - * browser may natively replace the target node during composition, we can - * use its position to find its replacement. - * - * @param {DOMEventTarget} root - */ -function FallbackCompositionState(root) { - this._root = root; - this._startText = this.getText(); - this._fallbackText = null; -} - -_assign(FallbackCompositionState.prototype, { - destructor: function () { - this._root = null; - this._startText = null; - this._fallbackText = null; - }, - - /** - * Get current text of input. - * - * @return {string} - */ - getText: function () { - if ('value' in this._root) { - return this._root.value; - } - return this._root[getTextContentAccessor()]; - }, - - /** - * Determine the differing substring between the initially stored - * text content and the current content. - * - * @return {string} - */ - getData: function () { - if (this._fallbackText) { - return this._fallbackText; - } - - var start; - var startValue = this._startText; - var startLength = startValue.length; - var end; - var endValue = this.getText(); - var endLength = endValue.length; - - for (start = 0; start < startLength; start++) { - if (startValue[start] !== endValue[start]) { - break; - } - } - - var minEnd = startLength - start; - for (end = 1; end <= minEnd; end++) { - if (startValue[startLength - end] !== endValue[endLength - end]) { - break; - } - } - - var sliceTail = end > 1 ? 1 - end : undefined; - this._fallbackText = endValue.slice(start, sliceTail); - return this._fallbackText; - } -}); - -PooledClass.addPoolingTo(FallbackCompositionState); - -module.exports = FallbackCompositionState; -},{"133":133,"172":172,"25":25}],22:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule HTMLDOMPropertyConfig - */ - -'use strict'; - -var DOMProperty = _dereq_(10); - -var MUST_USE_PROPERTY = DOMProperty.injection.MUST_USE_PROPERTY; -var HAS_BOOLEAN_VALUE = DOMProperty.injection.HAS_BOOLEAN_VALUE; -var HAS_NUMERIC_VALUE = DOMProperty.injection.HAS_NUMERIC_VALUE; -var HAS_POSITIVE_NUMERIC_VALUE = DOMProperty.injection.HAS_POSITIVE_NUMERIC_VALUE; -var HAS_OVERLOADED_BOOLEAN_VALUE = DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE; - -var HTMLDOMPropertyConfig = { - isCustomAttribute: RegExp.prototype.test.bind(new RegExp('^(data|aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$')), - Properties: { - /** - * Standard Properties - */ - accept: 0, - acceptCharset: 0, - accessKey: 0, - action: 0, - allowFullScreen: HAS_BOOLEAN_VALUE, - allowTransparency: 0, - alt: 0, - // specifies target context for links with `preload` type - as: 0, - async: HAS_BOOLEAN_VALUE, - autoComplete: 0, - // autoFocus is polyfilled/normalized by AutoFocusUtils - // autoFocus: HAS_BOOLEAN_VALUE, - autoPlay: HAS_BOOLEAN_VALUE, - capture: HAS_BOOLEAN_VALUE, - cellPadding: 0, - cellSpacing: 0, - charSet: 0, - challenge: 0, - checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - cite: 0, - classID: 0, - className: 0, - cols: HAS_POSITIVE_NUMERIC_VALUE, - colSpan: 0, - content: 0, - contentEditable: 0, - contextMenu: 0, - controls: HAS_BOOLEAN_VALUE, - coords: 0, - crossOrigin: 0, - data: 0, // For `` acts as `src`. - dateTime: 0, - 'default': HAS_BOOLEAN_VALUE, - defer: HAS_BOOLEAN_VALUE, - dir: 0, - disabled: HAS_BOOLEAN_VALUE, - download: HAS_OVERLOADED_BOOLEAN_VALUE, - draggable: 0, - encType: 0, - form: 0, - formAction: 0, - formEncType: 0, - formMethod: 0, - formNoValidate: HAS_BOOLEAN_VALUE, - formTarget: 0, - frameBorder: 0, - headers: 0, - height: 0, - hidden: HAS_BOOLEAN_VALUE, - high: 0, - href: 0, - hrefLang: 0, - htmlFor: 0, - httpEquiv: 0, - icon: 0, - id: 0, - inputMode: 0, - integrity: 0, - is: 0, - keyParams: 0, - keyType: 0, - kind: 0, - label: 0, - lang: 0, - list: 0, - loop: HAS_BOOLEAN_VALUE, - low: 0, - manifest: 0, - marginHeight: 0, - marginWidth: 0, - max: 0, - maxLength: 0, - media: 0, - mediaGroup: 0, - method: 0, - min: 0, - minLength: 0, - // Caution; `option.selected` is not updated if `select.multiple` is - // disabled with `removeAttribute`. - multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - name: 0, - nonce: 0, - noValidate: HAS_BOOLEAN_VALUE, - open: HAS_BOOLEAN_VALUE, - optimum: 0, - pattern: 0, - placeholder: 0, - playsInline: HAS_BOOLEAN_VALUE, - poster: 0, - preload: 0, - profile: 0, - radioGroup: 0, - readOnly: HAS_BOOLEAN_VALUE, - referrerPolicy: 0, - rel: 0, - required: HAS_BOOLEAN_VALUE, - reversed: HAS_BOOLEAN_VALUE, - role: 0, - rows: HAS_POSITIVE_NUMERIC_VALUE, - rowSpan: HAS_NUMERIC_VALUE, - sandbox: 0, - scope: 0, - scoped: HAS_BOOLEAN_VALUE, - scrolling: 0, - seamless: HAS_BOOLEAN_VALUE, - selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - shape: 0, - size: HAS_POSITIVE_NUMERIC_VALUE, - sizes: 0, - span: HAS_POSITIVE_NUMERIC_VALUE, - spellCheck: 0, - src: 0, - srcDoc: 0, - srcLang: 0, - srcSet: 0, - start: HAS_NUMERIC_VALUE, - step: 0, - style: 0, - summary: 0, - tabIndex: 0, - target: 0, - title: 0, - // Setting .type throws on non- tags - type: 0, - useMap: 0, - value: 0, - width: 0, - wmode: 0, - wrap: 0, - - /** - * RDFa Properties - */ - about: 0, - datatype: 0, - inlist: 0, - prefix: 0, - // property is also supported for OpenGraph in meta tags. - property: 0, - resource: 0, - 'typeof': 0, - vocab: 0, - - /** - * Non-standard Properties - */ - // autoCapitalize and autoCorrect are supported in Mobile Safari for - // keyboard hints. - autoCapitalize: 0, - autoCorrect: 0, - // autoSave allows WebKit/Blink to persist values of input fields on page reloads - autoSave: 0, - // color is for Safari mask-icon link - color: 0, - // itemProp, itemScope, itemType are for - // Microdata support. See http://schema.org/docs/gs.html - itemProp: 0, - itemScope: HAS_BOOLEAN_VALUE, - itemType: 0, - // itemID and itemRef are for Microdata support as well but - // only specified in the WHATWG spec document. See - // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api - itemID: 0, - itemRef: 0, - // results show looking glass icon and recent searches on input - // search fields in WebKit/Blink - results: 0, - // IE-only attribute that specifies security restrictions on an iframe - // as an alternative to the sandbox attribute on IE<10 - security: 0, - // IE-only attribute that controls focus behavior - unselectable: 0 - }, - DOMAttributeNames: { - acceptCharset: 'accept-charset', - className: 'class', - htmlFor: 'for', - httpEquiv: 'http-equiv' - }, - DOMPropertyNames: {} -}; - -module.exports = HTMLDOMPropertyConfig; -},{"10":10}],23:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule KeyEscapeUtils - * - */ - -'use strict'; - -/** - * Escape and wrap key so it is safe to use as a reactid - * - * @param {string} key to be escaped. - * @return {string} the escaped key. - */ - -function escape(key) { - var escapeRegex = /[=:]/g; - var escaperLookup = { - '=': '=0', - ':': '=2' - }; - var escapedString = ('' + key).replace(escapeRegex, function (match) { - return escaperLookup[match]; - }); - - return '$' + escapedString; -} - -/** - * Unescape and unwrap key for human-readable display - * - * @param {string} key to unescape. - * @return {string} the unescaped key. - */ -function unescape(key) { - var unescapeRegex = /(=0|=2)/g; - var unescaperLookup = { - '=0': '=', - '=2': ':' - }; - var keySubstring = key[0] === '.' && key[1] === '$' ? key.substring(2) : key.substring(1); - - return ('' + keySubstring).replace(unescapeRegex, function (match) { - return unescaperLookup[match]; - }); -} - -var KeyEscapeUtils = { - escape: escape, - unescape: unescape -}; - -module.exports = KeyEscapeUtils; -},{}],24:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule LinkedValueUtils - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var ReactPropTypes = _dereq_(84); -var ReactPropTypeLocations = _dereq_(83); -var ReactPropTypesSecret = _dereq_(85); - -var invariant = _dereq_(162); -var warning = _dereq_(171); - -var hasReadOnlyValue = { - 'button': true, - 'checkbox': true, - 'image': true, - 'hidden': true, - 'radio': true, - 'reset': true, - 'submit': true -}; - -function _assertSingleLink(inputProps) { - !(inputProps.checkedLink == null || inputProps.valueLink == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a valueLink. If you want to use checkedLink, you probably don\'t want to use valueLink and vice versa.') : _prodInvariant('87') : void 0; -} -function _assertValueLink(inputProps) { - _assertSingleLink(inputProps); - !(inputProps.value == null && inputProps.onChange == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a valueLink and a value or onChange event. If you want to use value or onChange, you probably don\'t want to use valueLink.') : _prodInvariant('88') : void 0; -} - -function _assertCheckedLink(inputProps) { - _assertSingleLink(inputProps); - !(inputProps.checked == null && inputProps.onChange == null) ? "development" !== 'production' ? invariant(false, 'Cannot provide a checkedLink and a checked property or onChange event. If you want to use checked or onChange, you probably don\'t want to use checkedLink') : _prodInvariant('89') : void 0; -} - -var propTypes = { - value: function (props, propName, componentName) { - if (!props[propName] || hasReadOnlyValue[props.type] || props.onChange || props.readOnly || props.disabled) { - return null; - } - return new Error('You provided a `value` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultValue`. Otherwise, ' + 'set either `onChange` or `readOnly`.'); - }, - checked: function (props, propName, componentName) { - if (!props[propName] || props.onChange || props.readOnly || props.disabled) { - return null; - } - return new Error('You provided a `checked` prop to a form field without an ' + '`onChange` handler. This will render a read-only field. If ' + 'the field should be mutable use `defaultChecked`. Otherwise, ' + 'set either `onChange` or `readOnly`.'); - }, - onChange: ReactPropTypes.func -}; - -var loggedTypeFailures = {}; -function getDeclarationErrorAddendum(owner) { - if (owner) { - var name = owner.getName(); - if (name) { - return ' Check the render method of `' + name + '`.'; - } - } - return ''; -} - -/** - * Provide a linked `value` attribute for controlled forms. You should not use - * this outside of the ReactDOM controlled form components. - */ -var LinkedValueUtils = { - checkPropTypes: function (tagName, props, owner) { - for (var propName in propTypes) { - if (propTypes.hasOwnProperty(propName)) { - var error = propTypes[propName](props, propName, tagName, ReactPropTypeLocations.prop, null, ReactPropTypesSecret); - } - if (error instanceof Error && !(error.message in loggedTypeFailures)) { - // Only monitor this failure once because there tends to be a lot of the - // same error. - loggedTypeFailures[error.message] = true; - - var addendum = getDeclarationErrorAddendum(owner); - "development" !== 'production' ? warning(false, 'Failed form propType: %s%s', error.message, addendum) : void 0; - } - } - }, - - /** - * @param {object} inputProps Props for form component - * @return {*} current value of the input either from value prop or link. - */ - getValue: function (inputProps) { - if (inputProps.valueLink) { - _assertValueLink(inputProps); - return inputProps.valueLink.value; - } - return inputProps.value; - }, - - /** - * @param {object} inputProps Props for form component - * @return {*} current checked status of the input either from checked prop - * or link. - */ - getChecked: function (inputProps) { - if (inputProps.checkedLink) { - _assertCheckedLink(inputProps); - return inputProps.checkedLink.value; - } - return inputProps.checked; - }, - - /** - * @param {object} inputProps Props for form component - * @param {SyntheticEvent} event change event to handle - */ - executeOnChange: function (inputProps, event) { - if (inputProps.valueLink) { - _assertValueLink(inputProps); - return inputProps.valueLink.requestChange(event.target.value); - } else if (inputProps.checkedLink) { - _assertCheckedLink(inputProps); - return inputProps.checkedLink.requestChange(event.target.checked); - } else if (inputProps.onChange) { - return inputProps.onChange.call(undefined, event); - } - } -}; - -module.exports = LinkedValueUtils; -},{"140":140,"162":162,"171":171,"83":83,"84":84,"85":85}],25:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule PooledClass - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var invariant = _dereq_(162); - -/** - * Static poolers. Several custom versions for each potential number of - * arguments. A completely generic pooler is easy to implement, but would - * require accessing the `arguments` object. In each of these, `this` refers to - * the Class itself, not an instance. If any others are needed, simply add them - * here, or in their own files. - */ -var oneArgumentPooler = function (copyFieldsFrom) { - var Klass = this; - if (Klass.instancePool.length) { - var instance = Klass.instancePool.pop(); - Klass.call(instance, copyFieldsFrom); - return instance; - } else { - return new Klass(copyFieldsFrom); - } -}; - -var twoArgumentPooler = function (a1, a2) { - var Klass = this; - if (Klass.instancePool.length) { - var instance = Klass.instancePool.pop(); - Klass.call(instance, a1, a2); - return instance; - } else { - return new Klass(a1, a2); - } -}; - -var threeArgumentPooler = function (a1, a2, a3) { - var Klass = this; - if (Klass.instancePool.length) { - var instance = Klass.instancePool.pop(); - Klass.call(instance, a1, a2, a3); - return instance; - } else { - return new Klass(a1, a2, a3); - } -}; - -var fourArgumentPooler = function (a1, a2, a3, a4) { - var Klass = this; - if (Klass.instancePool.length) { - var instance = Klass.instancePool.pop(); - Klass.call(instance, a1, a2, a3, a4); - return instance; - } else { - return new Klass(a1, a2, a3, a4); - } -}; - -var fiveArgumentPooler = function (a1, a2, a3, a4, a5) { - var Klass = this; - if (Klass.instancePool.length) { - var instance = Klass.instancePool.pop(); - Klass.call(instance, a1, a2, a3, a4, a5); - return instance; - } else { - return new Klass(a1, a2, a3, a4, a5); - } -}; - -var standardReleaser = function (instance) { - var Klass = this; - !(instance instanceof Klass) ? "development" !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : _prodInvariant('25') : void 0; - instance.destructor(); - if (Klass.instancePool.length < Klass.poolSize) { - Klass.instancePool.push(instance); - } -}; - -var DEFAULT_POOL_SIZE = 10; -var DEFAULT_POOLER = oneArgumentPooler; - -/** - * Augments `CopyConstructor` to be a poolable class, augmenting only the class - * itself (statically) not adding any prototypical fields. Any CopyConstructor - * you give this may have a `poolSize` property, and will look for a - * prototypical `destructor` on instances. - * - * @param {Function} CopyConstructor Constructor that can be used to reset. - * @param {Function} pooler Customizable pooler. - */ -var addPoolingTo = function (CopyConstructor, pooler) { - var NewKlass = CopyConstructor; - NewKlass.instancePool = []; - NewKlass.getPooled = pooler || DEFAULT_POOLER; - if (!NewKlass.poolSize) { - NewKlass.poolSize = DEFAULT_POOL_SIZE; - } - NewKlass.release = standardReleaser; - return NewKlass; -}; - -var PooledClass = { - addPoolingTo: addPoolingTo, - oneArgumentPooler: oneArgumentPooler, - twoArgumentPooler: twoArgumentPooler, - threeArgumentPooler: threeArgumentPooler, - fourArgumentPooler: fourArgumentPooler, - fiveArgumentPooler: fiveArgumentPooler -}; - -module.exports = PooledClass; -},{"140":140,"162":162}],26:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule React - */ - -'use strict'; - -var _assign = _dereq_(172); - -var ReactChildren = _dereq_(29); -var ReactComponent = _dereq_(32); -var ReactPureComponent = _dereq_(86); -var ReactClass = _dereq_(31); -var ReactDOMFactories = _dereq_(45); -var ReactElement = _dereq_(61); -var ReactPropTypes = _dereq_(84); -var ReactVersion = _dereq_(97); - -var onlyChild = _dereq_(138); -var warning = _dereq_(171); - -var createElement = ReactElement.createElement; -var createFactory = ReactElement.createFactory; -var cloneElement = ReactElement.cloneElement; - -if ("development" !== 'production') { - var ReactElementValidator = _dereq_(62); - createElement = ReactElementValidator.createElement; - createFactory = ReactElementValidator.createFactory; - cloneElement = ReactElementValidator.cloneElement; -} - -var __spread = _assign; - -if ("development" !== 'production') { - var warned = false; - __spread = function () { - "development" !== 'production' ? warning(warned, 'React.__spread is deprecated and should not be used. Use ' + 'Object.assign directly or another helper function with similar ' + 'semantics. You may be seeing this warning due to your compiler. ' + 'See https://fb.me/react-spread-deprecation for more details.') : void 0; - warned = true; - return _assign.apply(null, arguments); - }; -} - -var React = { - - // Modern - - Children: { - map: ReactChildren.map, - forEach: ReactChildren.forEach, - count: ReactChildren.count, - toArray: ReactChildren.toArray, - only: onlyChild - }, - - Component: ReactComponent, - PureComponent: ReactPureComponent, - - createElement: createElement, - cloneElement: cloneElement, - isValidElement: ReactElement.isValidElement, - - // Classic - - PropTypes: ReactPropTypes, - createClass: ReactClass.createClass, - createFactory: createFactory, - createMixin: function (mixin) { - // Currently a noop. Will be used to validate and trace mixins. - return mixin; - }, - - // This looks DOM specific but these are actually isomorphic helpers - // since they are just generating DOM strings. - DOM: ReactDOMFactories, - - version: ReactVersion, - - // Deprecated hook for JSX spread, don't use this for anything. - __spread: __spread -}; - -module.exports = React; -},{"138":138,"171":171,"172":172,"29":29,"31":31,"32":32,"45":45,"61":61,"62":62,"84":84,"86":86,"97":97}],27:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactBrowserEventEmitter - */ - -'use strict'; - -var _assign = _dereq_(172); - -var EventConstants = _dereq_(16); -var EventPluginRegistry = _dereq_(18); -var ReactEventEmitterMixin = _dereq_(65); -var ViewportMetrics = _dereq_(115); - -var getVendorPrefixedEventName = _dereq_(134); -var isEventSupported = _dereq_(136); - -/** - * Summary of `ReactBrowserEventEmitter` event handling: - * - * - Top-level delegation is used to trap most native browser events. This - * may only occur in the main thread and is the responsibility of - * ReactEventListener, which is injected and can therefore support pluggable - * event sources. This is the only work that occurs in the main thread. - * - * - We normalize and de-duplicate events to account for browser quirks. This - * may be done in the worker thread. - * - * - Forward these native events (with the associated top-level type used to - * trap it) to `EventPluginHub`, which in turn will ask plugins if they want - * to extract any synthetic events. - * - * - The `EventPluginHub` will then process each event by annotating them with - * "dispatches", a sequence of listeners and IDs that care about that event. - * - * - The `EventPluginHub` then dispatches the events. - * - * Overview of React and the event system: - * - * +------------+ . - * | DOM | . - * +------------+ . - * | . - * v . - * +------------+ . - * | ReactEvent | . - * | Listener | . - * +------------+ . +-----------+ - * | . +--------+|SimpleEvent| - * | . | |Plugin | - * +-----|------+ . v +-----------+ - * | | | . +--------------+ +------------+ - * | +-----------.--->|EventPluginHub| | Event | - * | | . | | +-----------+ | Propagators| - * | ReactEvent | . | | |TapEvent | |------------| - * | Emitter | . | |<---+|Plugin | |other plugin| - * | | . | | +-----------+ | utilities | - * | +-----------.--->| | +------------+ - * | | | . +--------------+ - * +-----|------+ . ^ +-----------+ - * | . | |Enter/Leave| - * + . +-------+|Plugin | - * +-------------+ . +-----------+ - * | application | . - * |-------------| . - * | | . - * | | . - * +-------------+ . - * . - * React Core . General Purpose Event Plugin System - */ - -var hasEventPageXY; -var alreadyListeningTo = {}; -var isMonitoringScrollValue = false; -var reactTopListenersCounter = 0; - -// For events like 'submit' which don't consistently bubble (which we trap at a -// lower node than `document`), binding at `document` would cause duplicate -// events so we don't include them here -var topEventMapping = { - topAbort: 'abort', - topAnimationEnd: getVendorPrefixedEventName('animationend') || 'animationend', - topAnimationIteration: getVendorPrefixedEventName('animationiteration') || 'animationiteration', - topAnimationStart: getVendorPrefixedEventName('animationstart') || 'animationstart', - topBlur: 'blur', - topCanPlay: 'canplay', - topCanPlayThrough: 'canplaythrough', - topChange: 'change', - topClick: 'click', - topCompositionEnd: 'compositionend', - topCompositionStart: 'compositionstart', - topCompositionUpdate: 'compositionupdate', - topContextMenu: 'contextmenu', - topCopy: 'copy', - topCut: 'cut', - topDoubleClick: 'dblclick', - topDrag: 'drag', - topDragEnd: 'dragend', - topDragEnter: 'dragenter', - topDragExit: 'dragexit', - topDragLeave: 'dragleave', - topDragOver: 'dragover', - topDragStart: 'dragstart', - topDrop: 'drop', - topDurationChange: 'durationchange', - topEmptied: 'emptied', - topEncrypted: 'encrypted', - topEnded: 'ended', - topError: 'error', - topFocus: 'focus', - topInput: 'input', - topKeyDown: 'keydown', - topKeyPress: 'keypress', - topKeyUp: 'keyup', - topLoadedData: 'loadeddata', - topLoadedMetadata: 'loadedmetadata', - topLoadStart: 'loadstart', - topMouseDown: 'mousedown', - topMouseMove: 'mousemove', - topMouseOut: 'mouseout', - topMouseOver: 'mouseover', - topMouseUp: 'mouseup', - topPaste: 'paste', - topPause: 'pause', - topPlay: 'play', - topPlaying: 'playing', - topProgress: 'progress', - topRateChange: 'ratechange', - topScroll: 'scroll', - topSeeked: 'seeked', - topSeeking: 'seeking', - topSelectionChange: 'selectionchange', - topStalled: 'stalled', - topSuspend: 'suspend', - topTextInput: 'textInput', - topTimeUpdate: 'timeupdate', - topTouchCancel: 'touchcancel', - topTouchEnd: 'touchend', - topTouchMove: 'touchmove', - topTouchStart: 'touchstart', - topTransitionEnd: getVendorPrefixedEventName('transitionend') || 'transitionend', - topVolumeChange: 'volumechange', - topWaiting: 'waiting', - topWheel: 'wheel' -}; - -/** - * To ensure no conflicts with other potential React instances on the page - */ -var topListenersIDKey = '_reactListenersID' + String(Math.random()).slice(2); - -function getListeningForDocument(mountAt) { - // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty` - // directly. - if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) { - mountAt[topListenersIDKey] = reactTopListenersCounter++; - alreadyListeningTo[mountAt[topListenersIDKey]] = {}; - } - return alreadyListeningTo[mountAt[topListenersIDKey]]; -} - -/** - * `ReactBrowserEventEmitter` is used to attach top-level event listeners. For - * example: - * - * EventPluginHub.putListener('myID', 'onClick', myFunction); - * - * This would allocate a "registration" of `('onClick', myFunction)` on 'myID'. - * - * @internal - */ -var ReactBrowserEventEmitter = _assign({}, ReactEventEmitterMixin, { - - /** - * Injectable event backend - */ - ReactEventListener: null, - - injection: { - /** - * @param {object} ReactEventListener - */ - injectReactEventListener: function (ReactEventListener) { - ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel); - ReactBrowserEventEmitter.ReactEventListener = ReactEventListener; - } - }, - - /** - * Sets whether or not any created callbacks should be enabled. - * - * @param {boolean} enabled True if callbacks should be enabled. - */ - setEnabled: function (enabled) { - if (ReactBrowserEventEmitter.ReactEventListener) { - ReactBrowserEventEmitter.ReactEventListener.setEnabled(enabled); - } - }, - - /** - * @return {boolean} True if callbacks are enabled. - */ - isEnabled: function () { - return !!(ReactBrowserEventEmitter.ReactEventListener && ReactBrowserEventEmitter.ReactEventListener.isEnabled()); - }, - - /** - * We listen for bubbled touch events on the document object. - * - * Firefox v8.01 (and possibly others) exhibited strange behavior when - * mounting `onmousemove` events at some node that was not the document - * element. The symptoms were that if your mouse is not moving over something - * contained within that mount point (for example on the background) the - * top-level listeners for `onmousemove` won't be called. However, if you - * register the `mousemove` on the document object, then it will of course - * catch all `mousemove`s. This along with iOS quirks, justifies restricting - * top-level listeners to the document object only, at least for these - * movement types of events and possibly all events. - * - * @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html - * - * Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but - * they bubble to document. - * - * @param {string} registrationName Name of listener (e.g. `onClick`). - * @param {object} contentDocumentHandle Document which owns the container - */ - listenTo: function (registrationName, contentDocumentHandle) { - var mountAt = contentDocumentHandle; - var isListening = getListeningForDocument(mountAt); - var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName]; - - var topLevelTypes = EventConstants.topLevelTypes; - for (var i = 0; i < dependencies.length; i++) { - var dependency = dependencies[i]; - if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) { - if (dependency === topLevelTypes.topWheel) { - if (isEventSupported('wheel')) { - ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt); - } else if (isEventSupported('mousewheel')) { - ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt); - } else { - // Firefox needs to capture a different mouse scroll event. - // @see http://www.quirksmode.org/dom/events/tests/scroll.html - ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt); - } - } else if (dependency === topLevelTypes.topScroll) { - - if (isEventSupported('scroll', true)) { - ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt); - } else { - ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topScroll, 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE); - } - } else if (dependency === topLevelTypes.topFocus || dependency === topLevelTypes.topBlur) { - - if (isEventSupported('focus', true)) { - ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt); - ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt); - } else if (isEventSupported('focusin')) { - // IE has `focusin` and `focusout` events which bubble. - // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html - ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt); - ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt); - } - - // to make sure blur and focus event listeners are only attached once - isListening[topLevelTypes.topBlur] = true; - isListening[topLevelTypes.topFocus] = true; - } else if (topEventMapping.hasOwnProperty(dependency)) { - ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt); - } - - isListening[dependency] = true; - } - } - }, - - trapBubbledEvent: function (topLevelType, handlerBaseName, handle) { - return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle); - }, - - trapCapturedEvent: function (topLevelType, handlerBaseName, handle) { - return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle); - }, - - /** - * Protect against document.createEvent() returning null - * Some popup blocker extensions appear to do this: - * https://github.com/facebook/react/issues/6887 - */ - supportsEventPageXY: function () { - if (!document.createEvent) { - return false; - } - var ev = document.createEvent('MouseEvent'); - return ev != null && 'pageX' in ev; - }, - - /** - * Listens to window scroll and resize events. We cache scroll values so that - * application code can access them without triggering reflows. - * - * ViewportMetrics is only used by SyntheticMouse/TouchEvent and only when - * pageX/pageY isn't supported (legacy browsers). - * - * NOTE: Scroll events do not bubble. - * - * @see http://www.quirksmode.org/dom/events/scroll.html - */ - ensureScrollValueMonitoring: function () { - if (hasEventPageXY === undefined) { - hasEventPageXY = ReactBrowserEventEmitter.supportsEventPageXY(); - } - if (!hasEventPageXY && !isMonitoringScrollValue) { - var refresh = ViewportMetrics.refreshScrollValues; - ReactBrowserEventEmitter.ReactEventListener.monitorScrollValue(refresh); - isMonitoringScrollValue = true; - } - } - -}); - -module.exports = ReactBrowserEventEmitter; -},{"115":115,"134":134,"136":136,"16":16,"172":172,"18":18,"65":65}],28:[function(_dereq_,module,exports){ -(function (process){ -/** - * Copyright 2014-present, 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. - * - * @providesModule ReactChildReconciler - */ - -'use strict'; - -var ReactReconciler = _dereq_(88); - -var instantiateReactComponent = _dereq_(135); -var KeyEscapeUtils = _dereq_(23); -var shouldUpdateReactComponent = _dereq_(144); -var traverseAllChildren = _dereq_(145); -var warning = _dereq_(171); - -var ReactComponentTreeHook; - -if (typeof process !== 'undefined' && process.env && "development" === 'test') { - // Temporary hack. - // Inline requires don't work well with Jest: - // https://github.com/facebook/react/issues/7240 - // Remove the inline requires when we don't need them anymore: - // https://github.com/facebook/react/pull/7178 - ReactComponentTreeHook = _dereq_(35); -} - -function instantiateChild(childInstances, child, name, selfDebugID) { - // We found a component instance. - var keyUnique = childInstances[name] === undefined; - if ("development" !== 'production') { - if (!ReactComponentTreeHook) { - ReactComponentTreeHook = _dereq_(35); - } - if (!keyUnique) { - "development" !== 'production' ? warning(false, 'flattenChildren(...): Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, only ' + 'the first child will be used.%s', KeyEscapeUtils.unescape(name), ReactComponentTreeHook.getStackAddendumByID(selfDebugID)) : void 0; - } - } - if (child != null && keyUnique) { - childInstances[name] = instantiateReactComponent(child, true); - } -} - -/** - * ReactChildReconciler provides helpers for initializing or updating a set of - * children. Its output is suitable for passing it onto ReactMultiChild which - * does diffed reordering and insertion. - */ -var ReactChildReconciler = { - /** - * Generates a "mount image" for each of the supplied children. In the case - * of `ReactDOMComponent`, a mount image is a string of markup. - * - * @param {?object} nestedChildNodes Nested child maps. - * @return {?object} A set of child instances. - * @internal - */ - instantiateChildren: function (nestedChildNodes, transaction, context, selfDebugID // 0 in production and for roots - ) { - if (nestedChildNodes == null) { - return null; - } - var childInstances = {}; - - if ("development" !== 'production') { - traverseAllChildren(nestedChildNodes, function (childInsts, child, name) { - return instantiateChild(childInsts, child, name, selfDebugID); - }, childInstances); - } else { - traverseAllChildren(nestedChildNodes, instantiateChild, childInstances); - } - return childInstances; - }, - - /** - * Updates the rendered children and returns a new set of children. - * - * @param {?object} prevChildren Previously initialized set of children. - * @param {?object} nextChildren Flat child element maps. - * @param {ReactReconcileTransaction} transaction - * @param {object} context - * @return {?object} A new set of child instances. - * @internal - */ - updateChildren: function (prevChildren, nextChildren, mountImages, removedNodes, transaction, hostParent, hostContainerInfo, context, selfDebugID // 0 in production and for roots - ) { - // We currently don't have a way to track moves here but if we use iterators - // instead of for..in we can zip the iterators and check if an item has - // moved. - // TODO: If nothing has changed, return the prevChildren object so that we - // can quickly bailout if nothing has changed. - if (!nextChildren && !prevChildren) { - return; - } - var name; - var prevChild; - for (name in nextChildren) { - if (!nextChildren.hasOwnProperty(name)) { - continue; - } - prevChild = prevChildren && prevChildren[name]; - var prevElement = prevChild && prevChild._currentElement; - var nextElement = nextChildren[name]; - if (prevChild != null && shouldUpdateReactComponent(prevElement, nextElement)) { - ReactReconciler.receiveComponent(prevChild, nextElement, transaction, context); - nextChildren[name] = prevChild; - } else { - if (prevChild) { - removedNodes[name] = ReactReconciler.getHostNode(prevChild); - ReactReconciler.unmountComponent(prevChild, false); - } - // The child must be instantiated before it's mounted. - var nextChildInstance = instantiateReactComponent(nextElement, true); - nextChildren[name] = nextChildInstance; - // Creating mount image now ensures refs are resolved in right order - // (see https://github.com/facebook/react/pull/7101 for explanation). - var nextChildMountImage = ReactReconciler.mountComponent(nextChildInstance, transaction, hostParent, hostContainerInfo, context, selfDebugID); - mountImages.push(nextChildMountImage); - } - } - // Unmount children that are no longer present. - for (name in prevChildren) { - if (prevChildren.hasOwnProperty(name) && !(nextChildren && nextChildren.hasOwnProperty(name))) { - prevChild = prevChildren[name]; - removedNodes[name] = ReactReconciler.getHostNode(prevChild); - ReactReconciler.unmountComponent(prevChild, false); - } - } - }, - - /** - * Unmounts all rendered children. This should be used to clean up children - * when this component is unmounted. - * - * @param {?object} renderedChildren Previously initialized set of children. - * @internal - */ - unmountChildren: function (renderedChildren, safely) { - for (var name in renderedChildren) { - if (renderedChildren.hasOwnProperty(name)) { - var renderedChild = renderedChildren[name]; - ReactReconciler.unmountComponent(renderedChild, safely); - } - } - } - -}; - -module.exports = ReactChildReconciler; -}).call(this,undefined) -},{"135":135,"144":144,"145":145,"171":171,"23":23,"35":35,"88":88}],29:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactChildren - */ - -'use strict'; - -var PooledClass = _dereq_(25); -var ReactElement = _dereq_(61); - -var emptyFunction = _dereq_(154); -var traverseAllChildren = _dereq_(145); - -var twoArgumentPooler = PooledClass.twoArgumentPooler; -var fourArgumentPooler = PooledClass.fourArgumentPooler; - -var userProvidedKeyEscapeRegex = /\/+/g; -function escapeUserProvidedKey(text) { - return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/'); -} - -/** - * PooledClass representing the bookkeeping associated with performing a child - * traversal. Allows avoiding binding callbacks. - * - * @constructor ForEachBookKeeping - * @param {!function} forEachFunction Function to perform traversal with. - * @param {?*} forEachContext Context to perform context with. - */ -function ForEachBookKeeping(forEachFunction, forEachContext) { - this.func = forEachFunction; - this.context = forEachContext; - this.count = 0; -} -ForEachBookKeeping.prototype.destructor = function () { - this.func = null; - this.context = null; - this.count = 0; -}; -PooledClass.addPoolingTo(ForEachBookKeeping, twoArgumentPooler); - -function forEachSingleChild(bookKeeping, child, name) { - var func = bookKeeping.func; - var context = bookKeeping.context; - - func.call(context, child, bookKeeping.count++); -} - -/** - * Iterates through children that are typically specified as `props.children`. - * - * See https://facebook.github.io/react/docs/top-level-api.html#react.children.foreach - * - * The provided forEachFunc(child, index) will be called for each - * leaf child. - * - * @param {?*} children Children tree container. - * @param {function(*, int)} forEachFunc - * @param {*} forEachContext Context for forEachContext. - */ -function forEachChildren(children, forEachFunc, forEachContext) { - if (children == null) { - return children; - } - var traverseContext = ForEachBookKeeping.getPooled(forEachFunc, forEachContext); - traverseAllChildren(children, forEachSingleChild, traverseContext); - ForEachBookKeeping.release(traverseContext); -} - -/** - * PooledClass representing the bookkeeping associated with performing a child - * mapping. Allows avoiding binding callbacks. - * - * @constructor MapBookKeeping - * @param {!*} mapResult Object containing the ordered map of results. - * @param {!function} mapFunction Function to perform mapping with. - * @param {?*} mapContext Context to perform mapping with. - */ -function MapBookKeeping(mapResult, keyPrefix, mapFunction, mapContext) { - this.result = mapResult; - this.keyPrefix = keyPrefix; - this.func = mapFunction; - this.context = mapContext; - this.count = 0; -} -MapBookKeeping.prototype.destructor = function () { - this.result = null; - this.keyPrefix = null; - this.func = null; - this.context = null; - this.count = 0; -}; -PooledClass.addPoolingTo(MapBookKeeping, fourArgumentPooler); - -function mapSingleChildIntoContext(bookKeeping, child, childKey) { - var result = bookKeeping.result; - var keyPrefix = bookKeeping.keyPrefix; - var func = bookKeeping.func; - var context = bookKeeping.context; - - - var mappedChild = func.call(context, child, bookKeeping.count++); - if (Array.isArray(mappedChild)) { - mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, emptyFunction.thatReturnsArgument); - } else if (mappedChild != null) { - if (ReactElement.isValidElement(mappedChild)) { - mappedChild = ReactElement.cloneAndReplaceKey(mappedChild, - // Keep both the (mapped) and old keys if they differ, just as - // traverseAllChildren used to do for objects as children - keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey(mappedChild.key) + '/' : '') + childKey); - } - result.push(mappedChild); - } -} - -function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { - var escapedPrefix = ''; - if (prefix != null) { - escapedPrefix = escapeUserProvidedKey(prefix) + '/'; - } - var traverseContext = MapBookKeeping.getPooled(array, escapedPrefix, func, context); - traverseAllChildren(children, mapSingleChildIntoContext, traverseContext); - MapBookKeeping.release(traverseContext); -} - -/** - * Maps children that are typically specified as `props.children`. - * - * See https://facebook.github.io/react/docs/top-level-api.html#react.children.map - * - * The provided mapFunction(child, key, index) will be called for each - * leaf child. - * - * @param {?*} children Children tree container. - * @param {function(*, int)} func The map function. - * @param {*} context Context for mapFunction. - * @return {object} Object containing the ordered map of results. - */ -function mapChildren(children, func, context) { - if (children == null) { - return children; - } - var result = []; - mapIntoWithKeyPrefixInternal(children, result, null, func, context); - return result; -} - -function forEachSingleChildDummy(traverseContext, child, name) { - return null; -} - -/** - * Count the number of children that are typically specified as - * `props.children`. - * - * See https://facebook.github.io/react/docs/top-level-api.html#react.children.count - * - * @param {?*} children Children tree container. - * @return {number} The number of children. - */ -function countChildren(children, context) { - return traverseAllChildren(children, forEachSingleChildDummy, null); -} - -/** - * Flatten a children object (typically specified as `props.children`) and - * return an array with appropriately re-keyed children. - * - * See https://facebook.github.io/react/docs/top-level-api.html#react.children.toarray - */ -function toArray(children) { - var result = []; - mapIntoWithKeyPrefixInternal(children, result, null, emptyFunction.thatReturnsArgument); - return result; -} - -var ReactChildren = { - forEach: forEachChildren, - map: mapChildren, - mapIntoWithKeyPrefixInternal: mapIntoWithKeyPrefixInternal, - count: countChildren, - toArray: toArray -}; - -module.exports = ReactChildren; -},{"145":145,"154":154,"25":25,"61":61}],30:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactChildrenMutationWarningHook - */ - -'use strict'; - -var ReactComponentTreeHook = _dereq_(35); - -var warning = _dereq_(171); - -function handleElement(debugID, element) { - if (element == null) { - return; - } - if (element._shadowChildren === undefined) { - return; - } - if (element._shadowChildren === element.props.children) { - return; - } - var isMutated = false; - if (Array.isArray(element._shadowChildren)) { - if (element._shadowChildren.length === element.props.children.length) { - for (var i = 0; i < element._shadowChildren.length; i++) { - if (element._shadowChildren[i] !== element.props.children[i]) { - isMutated = true; - } - } - } else { - isMutated = true; - } - } - if (!Array.isArray(element._shadowChildren) || isMutated) { - "development" !== 'production' ? warning(false, 'Component\'s children should not be mutated.%s', ReactComponentTreeHook.getStackAddendumByID(debugID)) : void 0; - } -} - -var ReactChildrenMutationWarningHook = { - onMountComponent: function (debugID) { - handleElement(debugID, ReactComponentTreeHook.getElement(debugID)); - }, - onUpdateComponent: function (debugID) { - handleElement(debugID, ReactComponentTreeHook.getElement(debugID)); - } -}; - -module.exports = ReactChildrenMutationWarningHook; -},{"171":171,"35":35}],31:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactClass - */ - -'use strict'; - -var _prodInvariant = _dereq_(140), - _assign = _dereq_(172); - -var ReactComponent = _dereq_(32); -var ReactElement = _dereq_(61); -var ReactPropTypeLocations = _dereq_(83); -var ReactPropTypeLocationNames = _dereq_(82); -var ReactNoopUpdateQueue = _dereq_(80); - -var emptyObject = _dereq_(155); -var invariant = _dereq_(162); -var keyMirror = _dereq_(165); -var keyOf = _dereq_(166); -var warning = _dereq_(171); - -var MIXINS_KEY = keyOf({ mixins: null }); - -/** - * Policies that describe methods in `ReactClassInterface`. - */ -var SpecPolicy = keyMirror({ - /** - * These methods may be defined only once by the class specification or mixin. - */ - DEFINE_ONCE: null, - /** - * These methods may be defined by both the class specification and mixins. - * Subsequent definitions will be chained. These methods must return void. - */ - DEFINE_MANY: null, - /** - * These methods are overriding the base class. - */ - OVERRIDE_BASE: null, - /** - * These methods are similar to DEFINE_MANY, except we assume they return - * objects. We try to merge the keys of the return values of all the mixed in - * functions. If there is a key conflict we throw. - */ - DEFINE_MANY_MERGED: null -}); - -var injectedMixins = []; - -/** - * Composite components are higher-level components that compose other composite - * or host components. - * - * To create a new type of `ReactClass`, pass a specification of - * your new class to `React.createClass`. The only requirement of your class - * specification is that you implement a `render` method. - * - * var MyComponent = React.createClass({ - * render: function() { - * return
    Hello World
    ; - * } - * }); - * - * The class specification supports a specific protocol of methods that have - * special meaning (e.g. `render`). See `ReactClassInterface` for - * more the comprehensive protocol. Any other properties and methods in the - * class specification will be available on the prototype. - * - * @interface ReactClassInterface - * @internal - */ -var ReactClassInterface = { - - /** - * An array of Mixin objects to include when defining your component. - * - * @type {array} - * @optional - */ - mixins: SpecPolicy.DEFINE_MANY, - - /** - * An object containing properties and methods that should be defined on - * the component's constructor instead of its prototype (static methods). - * - * @type {object} - * @optional - */ - statics: SpecPolicy.DEFINE_MANY, - - /** - * Definition of prop types for this component. - * - * @type {object} - * @optional - */ - propTypes: SpecPolicy.DEFINE_MANY, - - /** - * Definition of context types for this component. - * - * @type {object} - * @optional - */ - contextTypes: SpecPolicy.DEFINE_MANY, - - /** - * Definition of context types this component sets for its children. - * - * @type {object} - * @optional - */ - childContextTypes: SpecPolicy.DEFINE_MANY, - - // ==== Definition methods ==== - - /** - * Invoked when the component is mounted. Values in the mapping will be set on - * `this.props` if that prop is not specified (i.e. using an `in` check). - * - * This method is invoked before `getInitialState` and therefore cannot rely - * on `this.state` or use `this.setState`. - * - * @return {object} - * @optional - */ - getDefaultProps: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * Invoked once before the component is mounted. The return value will be used - * as the initial value of `this.state`. - * - * getInitialState: function() { - * return { - * isOn: false, - * fooBaz: new BazFoo() - * } - * } - * - * @return {object} - * @optional - */ - getInitialState: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * @return {object} - * @optional - */ - getChildContext: SpecPolicy.DEFINE_MANY_MERGED, - - /** - * Uses props from `this.props` and state from `this.state` to render the - * structure of the component. - * - * No guarantees are made about when or how often this method is invoked, so - * it must not have side effects. - * - * render: function() { - * var name = this.props.name; - * return
    Hello, {name}!
    ; - * } - * - * @return {ReactComponent} - * @nosideeffects - * @required - */ - render: SpecPolicy.DEFINE_ONCE, - - // ==== Delegate methods ==== - - /** - * Invoked when the component is initially created and about to be mounted. - * This may have side effects, but any external subscriptions or data created - * by this method must be cleaned up in `componentWillUnmount`. - * - * @optional - */ - componentWillMount: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component has been mounted and has a DOM representation. - * However, there is no guarantee that the DOM node is in the document. - * - * Use this as an opportunity to operate on the DOM when the component has - * been mounted (initialized and rendered) for the first time. - * - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidMount: SpecPolicy.DEFINE_MANY, - - /** - * Invoked before the component receives new props. - * - * Use this as an opportunity to react to a prop transition by updating the - * state using `this.setState`. Current props are accessed via `this.props`. - * - * componentWillReceiveProps: function(nextProps, nextContext) { - * this.setState({ - * likesIncreasing: nextProps.likeCount > this.props.likeCount - * }); - * } - * - * NOTE: There is no equivalent `componentWillReceiveState`. An incoming prop - * transition may cause a state change, but the opposite is not true. If you - * need it, you are probably looking for `componentWillUpdate`. - * - * @param {object} nextProps - * @optional - */ - componentWillReceiveProps: SpecPolicy.DEFINE_MANY, - - /** - * Invoked while deciding if the component should be updated as a result of - * receiving new props, state and/or context. - * - * Use this as an opportunity to `return false` when you're certain that the - * transition to the new props/state/context will not require a component - * update. - * - * shouldComponentUpdate: function(nextProps, nextState, nextContext) { - * return !equal(nextProps, this.props) || - * !equal(nextState, this.state) || - * !equal(nextContext, this.context); - * } - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @return {boolean} True if the component should update. - * @optional - */ - shouldComponentUpdate: SpecPolicy.DEFINE_ONCE, - - /** - * Invoked when the component is about to update due to a transition from - * `this.props`, `this.state` and `this.context` to `nextProps`, `nextState` - * and `nextContext`. - * - * Use this as an opportunity to perform preparation before an update occurs. - * - * NOTE: You **cannot** use `this.setState()` in this method. - * - * @param {object} nextProps - * @param {?object} nextState - * @param {?object} nextContext - * @param {ReactReconcileTransaction} transaction - * @optional - */ - componentWillUpdate: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component's DOM representation has been updated. - * - * Use this as an opportunity to operate on the DOM when the component has - * been updated. - * - * @param {object} prevProps - * @param {?object} prevState - * @param {?object} prevContext - * @param {DOMElement} rootNode DOM element representing the component. - * @optional - */ - componentDidUpdate: SpecPolicy.DEFINE_MANY, - - /** - * Invoked when the component is about to be removed from its parent and have - * its DOM representation destroyed. - * - * Use this as an opportunity to deallocate any external resources. - * - * NOTE: There is no `componentDidUnmount` since your component will have been - * destroyed by that point. - * - * @optional - */ - componentWillUnmount: SpecPolicy.DEFINE_MANY, - - // ==== Advanced methods ==== - - /** - * Updates the component's currently mounted DOM representation. - * - * By default, this implements React's rendering and reconciliation algorithm. - * Sophisticated clients may wish to override this. - * - * @param {ReactReconcileTransaction} transaction - * @internal - * @overridable - */ - updateComponent: SpecPolicy.OVERRIDE_BASE - -}; - -/** - * Mapping from class specification keys to special processing functions. - * - * Although these are declared like instance properties in the specification - * when defining classes using `React.createClass`, they are actually static - * and are accessible on the constructor instead of the prototype. Despite - * being static, they must be defined outside of the "statics" key under - * which all other static methods are defined. - */ -var RESERVED_SPEC_KEYS = { - displayName: function (Constructor, displayName) { - Constructor.displayName = displayName; - }, - mixins: function (Constructor, mixins) { - if (mixins) { - for (var i = 0; i < mixins.length; i++) { - mixSpecIntoComponent(Constructor, mixins[i]); - } - } - }, - childContextTypes: function (Constructor, childContextTypes) { - if ("development" !== 'production') { - validateTypeDef(Constructor, childContextTypes, ReactPropTypeLocations.childContext); - } - Constructor.childContextTypes = _assign({}, Constructor.childContextTypes, childContextTypes); - }, - contextTypes: function (Constructor, contextTypes) { - if ("development" !== 'production') { - validateTypeDef(Constructor, contextTypes, ReactPropTypeLocations.context); - } - Constructor.contextTypes = _assign({}, Constructor.contextTypes, contextTypes); - }, - /** - * Special case getDefaultProps which should move into statics but requires - * automatic merging. - */ - getDefaultProps: function (Constructor, getDefaultProps) { - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps = createMergedResultFunction(Constructor.getDefaultProps, getDefaultProps); - } else { - Constructor.getDefaultProps = getDefaultProps; - } - }, - propTypes: function (Constructor, propTypes) { - if ("development" !== 'production') { - validateTypeDef(Constructor, propTypes, ReactPropTypeLocations.prop); - } - Constructor.propTypes = _assign({}, Constructor.propTypes, propTypes); - }, - statics: function (Constructor, statics) { - mixStaticSpecIntoComponent(Constructor, statics); - }, - autobind: function () {} }; - -// noop -function validateTypeDef(Constructor, typeDef, location) { - for (var propName in typeDef) { - if (typeDef.hasOwnProperty(propName)) { - // use a warning instead of an invariant so components - // don't show up in prod but only in __DEV__ - "development" !== 'production' ? warning(typeof typeDef[propName] === 'function', '%s: %s type `%s` is invalid; it must be a function, usually from ' + 'React.PropTypes.', Constructor.displayName || 'ReactClass', ReactPropTypeLocationNames[location], propName) : void 0; - } - } -} - -function validateMethodOverride(isAlreadyDefined, name) { - var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null; - - // Disallow overriding of base class methods unless explicitly allowed. - if (ReactClassMixin.hasOwnProperty(name)) { - !(specPolicy === SpecPolicy.OVERRIDE_BASE) ? "development" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods.', name) : _prodInvariant('73', name) : void 0; - } - - // Disallow defining methods more than once unless explicitly allowed. - if (isAlreadyDefined) { - !(specPolicy === SpecPolicy.DEFINE_MANY || specPolicy === SpecPolicy.DEFINE_MANY_MERGED) ? "development" !== 'production' ? invariant(false, 'ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.', name) : _prodInvariant('74', name) : void 0; - } -} - -/** - * Mixin helper which handles policy validation and reserved - * specification keys when building React classes. - */ -function mixSpecIntoComponent(Constructor, spec) { - if (!spec) { - if ("development" !== 'production') { - var typeofSpec = typeof spec; - var isMixinValid = typeofSpec === 'object' && spec !== null; - - "development" !== 'production' ? warning(isMixinValid, '%s: You\'re attempting to include a mixin that is either null ' + 'or not an object. Check the mixins included by the component, ' + 'as well as any mixins they include themselves. ' + 'Expected object but got %s.', Constructor.displayName || 'ReactClass', spec === null ? null : typeofSpec) : void 0; - } - - return; - } - - !(typeof spec !== 'function') ? "development" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to use a component class or function as a mixin. Instead, just use a regular object.') : _prodInvariant('75') : void 0; - !!ReactElement.isValidElement(spec) ? "development" !== 'production' ? invariant(false, 'ReactClass: You\'re attempting to use a component as a mixin. Instead, just use a regular object.') : _prodInvariant('76') : void 0; - - var proto = Constructor.prototype; - var autoBindPairs = proto.__reactAutoBindPairs; - - // By handling mixins before any other properties, we ensure the same - // chaining order is applied to methods with DEFINE_MANY policy, whether - // mixins are listed before or after these methods in the spec. - if (spec.hasOwnProperty(MIXINS_KEY)) { - RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins); - } - - for (var name in spec) { - if (!spec.hasOwnProperty(name)) { - continue; - } - - if (name === MIXINS_KEY) { - // We have already handled mixins in a special case above. - continue; - } - - var property = spec[name]; - var isAlreadyDefined = proto.hasOwnProperty(name); - validateMethodOverride(isAlreadyDefined, name); - - if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { - RESERVED_SPEC_KEYS[name](Constructor, property); - } else { - // Setup methods on prototype: - // The following member methods should not be automatically bound: - // 1. Expected ReactClass methods (in the "interface"). - // 2. Overridden methods (that were mixed in). - var isReactClassMethod = ReactClassInterface.hasOwnProperty(name); - var isFunction = typeof property === 'function'; - var shouldAutoBind = isFunction && !isReactClassMethod && !isAlreadyDefined && spec.autobind !== false; - - if (shouldAutoBind) { - autoBindPairs.push(name, property); - proto[name] = property; - } else { - if (isAlreadyDefined) { - var specPolicy = ReactClassInterface[name]; - - // These cases should already be caught by validateMethodOverride. - !(isReactClassMethod && (specPolicy === SpecPolicy.DEFINE_MANY_MERGED || specPolicy === SpecPolicy.DEFINE_MANY)) ? "development" !== 'production' ? invariant(false, 'ReactClass: Unexpected spec policy %s for key %s when mixing in component specs.', specPolicy, name) : _prodInvariant('77', specPolicy, name) : void 0; - - // For methods which are defined more than once, call the existing - // methods before calling the new property, merging if appropriate. - if (specPolicy === SpecPolicy.DEFINE_MANY_MERGED) { - proto[name] = createMergedResultFunction(proto[name], property); - } else if (specPolicy === SpecPolicy.DEFINE_MANY) { - proto[name] = createChainedFunction(proto[name], property); - } - } else { - proto[name] = property; - if ("development" !== 'production') { - // Add verbose displayName to the function, which helps when looking - // at profiling tools. - if (typeof property === 'function' && spec.displayName) { - proto[name].displayName = spec.displayName + '_' + name; - } - } - } - } - } - } -} - -function mixStaticSpecIntoComponent(Constructor, statics) { - if (!statics) { - return; - } - for (var name in statics) { - var property = statics[name]; - if (!statics.hasOwnProperty(name)) { - continue; - } - - var isReserved = name in RESERVED_SPEC_KEYS; - !!isReserved ? "development" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define a reserved property, `%s`, that shouldn\'t be on the "statics" key. Define it as an instance property instead; it will still be accessible on the constructor.', name) : _prodInvariant('78', name) : void 0; - - var isInherited = name in Constructor; - !!isInherited ? "development" !== 'production' ? invariant(false, 'ReactClass: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin.', name) : _prodInvariant('79', name) : void 0; - Constructor[name] = property; - } -} - -/** - * Merge two objects, but throw if both contain the same key. - * - * @param {object} one The first object, which is mutated. - * @param {object} two The second object - * @return {object} one after it has been mutated to contain everything in two. - */ -function mergeIntoWithNoDuplicateKeys(one, two) { - !(one && two && typeof one === 'object' && typeof two === 'object') ? "development" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects.') : _prodInvariant('80') : void 0; - - for (var key in two) { - if (two.hasOwnProperty(key)) { - !(one[key] === undefined) ? "development" !== 'production' ? invariant(false, 'mergeIntoWithNoDuplicateKeys(): Tried to merge two objects with the same key: `%s`. This conflict may be due to a mixin; in particular, this may be caused by two getInitialState() or getDefaultProps() methods returning objects with clashing keys.', key) : _prodInvariant('81', key) : void 0; - one[key] = two[key]; - } - } - return one; -} - -/** - * Creates a function that invokes two functions and merges their return values. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createMergedResultFunction(one, two) { - return function mergedResult() { - var a = one.apply(this, arguments); - var b = two.apply(this, arguments); - if (a == null) { - return b; - } else if (b == null) { - return a; - } - var c = {}; - mergeIntoWithNoDuplicateKeys(c, a); - mergeIntoWithNoDuplicateKeys(c, b); - return c; - }; -} - -/** - * Creates a function that invokes two functions and ignores their return vales. - * - * @param {function} one Function to invoke first. - * @param {function} two Function to invoke second. - * @return {function} Function that invokes the two argument functions. - * @private - */ -function createChainedFunction(one, two) { - return function chainedFunction() { - one.apply(this, arguments); - two.apply(this, arguments); - }; -} - -/** - * Binds a method to the component. - * - * @param {object} component Component whose method is going to be bound. - * @param {function} method Method to be bound. - * @return {function} The bound method. - */ -function bindAutoBindMethod(component, method) { - var boundMethod = method.bind(component); - if ("development" !== 'production') { - boundMethod.__reactBoundContext = component; - boundMethod.__reactBoundMethod = method; - boundMethod.__reactBoundArguments = null; - var componentName = component.constructor.displayName; - var _bind = boundMethod.bind; - boundMethod.bind = function (newThis) { - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - - // User is trying to bind() an autobound method; we effectively will - // ignore the value of "this" that the user is trying to use, so - // let's warn. - if (newThis !== component && newThis !== null) { - "development" !== 'production' ? warning(false, 'bind(): React component methods may only be bound to the ' + 'component instance. See %s', componentName) : void 0; - } else if (!args.length) { - "development" !== 'production' ? warning(false, 'bind(): You are binding a component method to the component. ' + 'React does this for you automatically in a high-performance ' + 'way, so you can safely remove this call. See %s', componentName) : void 0; - return boundMethod; - } - var reboundMethod = _bind.apply(boundMethod, arguments); - reboundMethod.__reactBoundContext = component; - reboundMethod.__reactBoundMethod = method; - reboundMethod.__reactBoundArguments = args; - return reboundMethod; - }; - } - return boundMethod; -} - -/** - * Binds all auto-bound methods in a component. - * - * @param {object} component Component whose method is going to be bound. - */ -function bindAutoBindMethods(component) { - var pairs = component.__reactAutoBindPairs; - for (var i = 0; i < pairs.length; i += 2) { - var autoBindKey = pairs[i]; - var method = pairs[i + 1]; - component[autoBindKey] = bindAutoBindMethod(component, method); - } -} - -/** - * Add more to the ReactClass base class. These are all legacy features and - * therefore not already part of the modern ReactComponent. - */ -var ReactClassMixin = { - - /** - * TODO: This will be deprecated because state should always keep a consistent - * type signature and the only use case for this, is to avoid that. - */ - replaceState: function (newState, callback) { - this.updater.enqueueReplaceState(this, newState); - if (callback) { - this.updater.enqueueCallback(this, callback, 'replaceState'); - } - }, - - /** - * Checks whether or not this composite component is mounted. - * @return {boolean} True if mounted, false otherwise. - * @protected - * @final - */ - isMounted: function () { - return this.updater.isMounted(this); - } -}; - -var ReactClassComponent = function () {}; -_assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin); - -/** - * Module for creating composite components. - * - * @class ReactClass - */ -var ReactClass = { - - /** - * Creates a composite component class given a class specification. - * See https://facebook.github.io/react/docs/top-level-api.html#react.createclass - * - * @param {object} spec Class specification (which must define `render`). - * @return {function} Component constructor function. - * @public - */ - createClass: function (spec) { - var Constructor = function (props, context, updater) { - // This constructor gets overridden by mocks. The argument is used - // by mocks to assert on what gets mounted. - - if ("development" !== 'production') { - "development" !== 'production' ? warning(this instanceof Constructor, 'Something is calling a React component directly. Use a factory or ' + 'JSX instead. See: https://fb.me/react-legacyfactory') : void 0; - } - - // Wire up auto-binding - if (this.__reactAutoBindPairs.length) { - bindAutoBindMethods(this); - } - - this.props = props; - this.context = context; - this.refs = emptyObject; - this.updater = updater || ReactNoopUpdateQueue; - - this.state = null; - - // ReactClasses doesn't have constructors. Instead, they use the - // getInitialState and componentWillMount methods for initialization. - - var initialState = this.getInitialState ? this.getInitialState() : null; - if ("development" !== 'production') { - // We allow auto-mocks to proceed as if they're returning null. - if (initialState === undefined && this.getInitialState._isMockFunction) { - // This is probably bad practice. Consider warning here and - // deprecating this convenience. - initialState = null; - } - } - !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "development" !== 'production' ? invariant(false, '%s.getInitialState(): must return an object or null', Constructor.displayName || 'ReactCompositeComponent') : _prodInvariant('82', Constructor.displayName || 'ReactCompositeComponent') : void 0; - - this.state = initialState; - }; - Constructor.prototype = new ReactClassComponent(); - Constructor.prototype.constructor = Constructor; - Constructor.prototype.__reactAutoBindPairs = []; - - injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor)); - - mixSpecIntoComponent(Constructor, spec); - - // Initialize the defaultProps property after all mixins have been merged. - if (Constructor.getDefaultProps) { - Constructor.defaultProps = Constructor.getDefaultProps(); - } - - if ("development" !== 'production') { - // This is a tag to indicate that the use of these method names is ok, - // since it's used with createClass. If it's not, then it's likely a - // mistake so we'll warn you to use the static property, property - // initializer or constructor respectively. - if (Constructor.getDefaultProps) { - Constructor.getDefaultProps.isReactClassApproved = {}; - } - if (Constructor.prototype.getInitialState) { - Constructor.prototype.getInitialState.isReactClassApproved = {}; - } - } - - !Constructor.prototype.render ? "development" !== 'production' ? invariant(false, 'createClass(...): Class specification must implement a `render` method.') : _prodInvariant('83') : void 0; - - if ("development" !== 'production') { - "development" !== 'production' ? warning(!Constructor.prototype.componentShouldUpdate, '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', spec.displayName || 'A component') : void 0; - "development" !== 'production' ? warning(!Constructor.prototype.componentWillRecieveProps, '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', spec.displayName || 'A component') : void 0; - } - - // Reduce time spent doing lookups by setting these on the prototype. - for (var methodName in ReactClassInterface) { - if (!Constructor.prototype[methodName]) { - Constructor.prototype[methodName] = null; - } - } - - return Constructor; - }, - - injection: { - injectMixin: function (mixin) { - injectedMixins.push(mixin); - } - } - -}; - -module.exports = ReactClass; -},{"140":140,"155":155,"162":162,"165":165,"166":166,"171":171,"172":172,"32":32,"61":61,"80":80,"82":82,"83":83}],32:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactComponent - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var ReactNoopUpdateQueue = _dereq_(80); - -var canDefineProperty = _dereq_(118); -var emptyObject = _dereq_(155); -var invariant = _dereq_(162); -var warning = _dereq_(171); - -/** - * Base class helpers for the updating state of a component. - */ -function ReactComponent(props, context, updater) { - this.props = props; - this.context = context; - this.refs = emptyObject; - // We initialize the default updater but the real one gets injected by the - // renderer. - this.updater = updater || ReactNoopUpdateQueue; -} - -ReactComponent.prototype.isReactComponent = {}; - -/** - * Sets a subset of the state. Always use this to mutate - * state. You should treat `this.state` as immutable. - * - * There is no guarantee that `this.state` will be immediately updated, so - * accessing `this.state` after calling this method may return the old value. - * - * There is no guarantee that calls to `setState` will run synchronously, - * as they may eventually be batched together. You can provide an optional - * callback that will be executed when the call to setState is actually - * completed. - * - * When a function is provided to setState, it will be called at some point in - * the future (not synchronously). It will be called with the up to date - * component arguments (state, props, context). These values can be different - * from this.* because your function may be called after receiveProps but before - * shouldComponentUpdate, and this new state, props, and context will not yet be - * assigned to this. - * - * @param {object|function} partialState Next partial state or function to - * produce next partial state to be merged with current state. - * @param {?function} callback Called after state is updated. - * @final - * @protected - */ -ReactComponent.prototype.setState = function (partialState, callback) { - !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0; - this.updater.enqueueSetState(this, partialState); - if (callback) { - this.updater.enqueueCallback(this, callback, 'setState'); - } -}; - -/** - * Forces an update. This should only be invoked when it is known with - * certainty that we are **not** in a DOM transaction. - * - * You may want to call this when you know that some deeper aspect of the - * component's state has changed but `setState` was not called. - * - * This will not invoke `shouldComponentUpdate`, but it will invoke - * `componentWillUpdate` and `componentDidUpdate`. - * - * @param {?function} callback Called after update is complete. - * @final - * @protected - */ -ReactComponent.prototype.forceUpdate = function (callback) { - this.updater.enqueueForceUpdate(this); - if (callback) { - this.updater.enqueueCallback(this, callback, 'forceUpdate'); - } -}; - -/** - * Deprecated APIs. These APIs used to exist on classic React classes but since - * we would like to deprecate them, we're not going to move them over to this - * modern base class. Instead, we define a getter that warns if it's accessed. - */ -if ("development" !== 'production') { - var deprecatedAPIs = { - isMounted: ['isMounted', 'Instead, make sure to clean up subscriptions and pending requests in ' + 'componentWillUnmount to prevent memory leaks.'], - replaceState: ['replaceState', 'Refactor your code to use setState instead (see ' + 'https://github.com/facebook/react/issues/3236).'] - }; - var defineDeprecationWarning = function (methodName, info) { - if (canDefineProperty) { - Object.defineProperty(ReactComponent.prototype, methodName, { - get: function () { - "development" !== 'production' ? warning(false, '%s(...) is deprecated in plain JavaScript React classes. %s', info[0], info[1]) : void 0; - return undefined; - } - }); - } - }; - for (var fnName in deprecatedAPIs) { - if (deprecatedAPIs.hasOwnProperty(fnName)) { - defineDeprecationWarning(fnName, deprecatedAPIs[fnName]); - } - } -} - -module.exports = ReactComponent; -},{"118":118,"140":140,"155":155,"162":162,"171":171,"80":80}],33:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactComponentBrowserEnvironment - */ - -'use strict'; - -var DOMChildrenOperations = _dereq_(7); -var ReactDOMIDOperations = _dereq_(47); - -/** - * Abstracts away all functionality of the reconciler that requires knowledge of - * the browser context. TODO: These callers should be refactored to avoid the - * need for this injection. - */ -var ReactComponentBrowserEnvironment = { - - processChildrenUpdates: ReactDOMIDOperations.dangerouslyProcessChildrenUpdates, - - replaceNodeWithMarkup: DOMChildrenOperations.dangerouslyReplaceNodeWithMarkup - -}; - -module.exports = ReactComponentBrowserEnvironment; -},{"47":47,"7":7}],34:[function(_dereq_,module,exports){ -/** - * Copyright 2014-present, 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. - * - * @providesModule ReactComponentEnvironment - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var invariant = _dereq_(162); - -var injected = false; - -var ReactComponentEnvironment = { - - /** - * Optionally injectable hook for swapping out mount images in the middle of - * the tree. - */ - replaceNodeWithMarkup: null, - - /** - * Optionally injectable hook for processing a queue of child updates. Will - * later move into MultiChildComponents. - */ - processChildrenUpdates: null, - - injection: { - injectEnvironment: function (environment) { - !!injected ? "development" !== 'production' ? invariant(false, 'ReactCompositeComponent: injectEnvironment() can only be called once.') : _prodInvariant('104') : void 0; - ReactComponentEnvironment.replaceNodeWithMarkup = environment.replaceNodeWithMarkup; - ReactComponentEnvironment.processChildrenUpdates = environment.processChildrenUpdates; - injected = true; - } - } - -}; - -module.exports = ReactComponentEnvironment; -},{"140":140,"162":162}],35:[function(_dereq_,module,exports){ -/** - * Copyright 2016-present, 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. - * - * @providesModule ReactComponentTreeHook - */ - -'use strict'; - -var _prodInvariant = _dereq_(140); - -var ReactCurrentOwner = _dereq_(37); - -var invariant = _dereq_(162); -var warning = _dereq_(171); - -function isNative(fn) { - // Based on isNative() from Lodash - var funcToString = Function.prototype.toString; - var hasOwnProperty = Object.prototype.hasOwnProperty; - var reIsNative = RegExp('^' + funcToString - // Take an example native function source for comparison - .call(hasOwnProperty) - // Strip regex characters so we can use it for regex - .replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') - // Remove hasOwnProperty from the template to make it generic - .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'); - try { - var source = funcToString.call(fn); - return reIsNative.test(source); - } catch (err) { - return false; - } -} - -var canUseCollections = -// Array.from -typeof Array.from === 'function' && -// Map -typeof Map === 'function' && isNative(Map) && -// Map.prototype.keys -Map.prototype != null && typeof Map.prototype.keys === 'function' && isNative(Map.prototype.keys) && -// Set -typeof Set === 'function' && isNative(Set) && -// Set.prototype.keys -Set.prototype != null && typeof Set.prototype.keys === 'function' && isNative(Set.prototype.keys); - -var itemMap; -var rootIDSet; - -var itemByKey; -var rootByKey; - -if (canUseCollections) { - itemMap = new Map(); - rootIDSet = new Set(); -} else { - itemByKey = {}; - rootByKey = {}; -} - -var unmountedIDs = []; - -// Use non-numeric keys to prevent V8 performance issues: -// https://github.com/facebook/react/pull/7232 -function getKeyFromID(id) { - return '.' + id; -} -function getIDFromKey(key) { - return parseInt(key.substr(1), 10); -} - -function get(id) { - if (canUseCollections) { - return itemMap.get(id); - } else { - var key = getKeyFromID(id); - return itemByKey[key]; - } -} - -function remove(id) { - if (canUseCollections) { - itemMap['delete'](id); - } else { - var key = getKeyFromID(id); - delete itemByKey[key]; - } -} - -function create(id, element, parentID) { - var item = { - element: element, - parentID: parentID, - text: null, - childIDs: [], - isMounted: false, - updateCount: 0 - }; - - if (canUseCollections) { - itemMap.set(id, item); - } else { - var key = getKeyFromID(id); - itemByKey[key] = item; - } -} - -function addRoot(id) { - if (canUseCollections) { - rootIDSet.add(id); - } else { - var key = getKeyFromID(id); - rootByKey[key] = true; - } -} - -function removeRoot(id) { - if (canUseCollections) { - rootIDSet['delete'](id); - } else { - var key = getKeyFromID(id); - delete rootByKey[key]; - } -} - -function getRegisteredIDs() { - if (canUseCollections) { - return Array.from(itemMap.keys()); - } else { - return Object.keys(itemByKey).map(getIDFromKey); - } -} - -function getRootIDs() { - if (canUseCollections) { - return Array.from(rootIDSet.keys()); - } else { - return Object.keys(rootByKey).map(getIDFromKey); - } -} - -function purgeDeep(id) { - var item = get(id); - if (item) { - var childIDs = item.childIDs; - - remove(id); - childIDs.forEach(purgeDeep); - } -} - -function describeComponentFrame(name, source, ownerName) { - return '\n in ' + name + (source ? ' (at ' + source.fileName.replace(/^.*[\\\/]/, '') + ':' + source.lineNumber + ')' : ownerName ? ' (created by ' + ownerName + ')' : ''); -} - -function getDisplayName(element) { - if (element == null) { - return '#empty'; - } else if (typeof element === 'string' || typeof element === 'number') { - return '#text'; - } else if (typeof element.type === 'string') { - return element.type; - } else { - return element.type.displayName || element.type.name || 'Unknown'; - } -} - -function describeID(id) { - var name = ReactComponentTreeHook.getDisplayName(id); - var element = ReactComponentTreeHook.getElement(id); - var ownerID = ReactComponentTreeHook.getOwnerID(id); - var ownerName; - if (ownerID) { - ownerName = ReactComponentTreeHook.getDisplayName(ownerID); - } - "development" !== 'production' ? warning(element, 'ReactComponentTreeHook: Missing React element for debugID %s when ' + 'building stack', id) : void 0; - return describeComponentFrame(name, element && element._source, ownerName); -} - -var ReactComponentTreeHook = { - onSetChildren: function (id, nextChildIDs) { - var item = get(id); - item.childIDs = nextChildIDs; - - for (var i = 0; i < nextChildIDs.length; i++) { - var nextChildID = nextChildIDs[i]; - var nextChild = get(nextChildID); - !nextChild ? "development" !== 'production' ? invariant(false, 'Expected hook events to fire for the child before its parent includes it in onSetChildren().') : _prodInvariant('140') : void 0; - !(nextChild.childIDs != null || typeof nextChild.element !== 'object' || nextChild.element == null) ? "development" !== 'production' ? invariant(false, 'Expected onSetChildren() to fire for a container child before its parent includes it in onSetChildren().') : _prodInvariant('141') : void 0; - !nextChild.isMounted ? "development" !== 'production' ? invariant(false, 'Expected onMountComponent() to fire for the child before its parent includes it in onSetChildren().') : _prodInvariant('71') : void 0; - if (nextChild.parentID == null) { - nextChild.parentID = id; - // TODO: This shouldn't be necessary but mounting a new root during in - // componentWillMount currently causes not-yet-mounted components to - // be purged from our tree data so their parent ID is missing. - } - !(nextChild.parentID === id) ? "development" !== 'production' ? invariant(false, 'Expected onBeforeMountComponent() parent and onSetChildren() to be consistent (%s has parents %s and %s).', nextChildID, nextChild.parentID, id) : _prodInvariant('142', nextChildID, nextChild.parentID, id) : void 0; - } - }, - onBeforeMountComponent: function (id, element, parentID) { - create(id, element, parentID); - }, - onBeforeUpdateComponent: function (id, element) { - var item = get(id); - if (!item || !item.isMounted) { - // We may end up here as a result of setState() in componentWillUnmount(). - // In this case, ignore the element. - return; - } - item.element = element; - }, - onMountComponent: function (id) { - var item = get(id); - item.isMounted = true; - var isRoot = item.parentID === 0; - if (isRoot) { - addRoot(id); - } - }, - onUpdateComponent: function (id) { - var item = get(id); - if (!item || !item.isMounted) { - // We may end up here as a result of setState() in componentWillUnmount(). - // In this case, ignore the element. - return; - } - item.updateCount++; - }, - onUnmountComponent: function (id) { - var item = get(id); - if (item) { - // We need to check if it exists. - // `item` might not exist if it is inside an error boundary, and a sibling - // error boundary child threw while mounting. Then this instance never - // got a chance to mount, but it still gets an unmounting event during - // the error boundary cleanup. - item.isMounted = false; - var isRoot = item.parentID === 0; - if (isRoot) { - removeRoot(id); - } - } - unmountedIDs.push(id); - }, - purgeUnmountedComponents: function () { - if (ReactComponentTreeHook._preventPurging) { - // Should only be used for testing. - return; - } - - for (var i = 0; i < unmountedIDs.length; i++) { - var id = unmountedIDs[i]; - purgeDeep(id); - } - unmountedIDs.length = 0; - }, - isMounted: function (id) { - var item = get(id); - return item ? item.isMounted : false; - }, - getCurrentStackAddendum: function (topElement) { - var info = ''; - if (topElement) { - var type = topElement.type; - var name = typeof type === 'function' ? type.displayName || type.name : type; - var owner = topElement._owner; - info += describeComponentFrame(name || 'Unknown', topElement._source, owner && owner.getName()); - } - - var currentOwner = ReactCurrentOwner.current; - var id = currentOwner && currentOwner._debugID; - - info += ReactComponentTreeHook.getStackAddendumByID(id); - return info; - }, - getStackAddendumByID: function (id) { - var info = ''; - while (id) { - info += describeID(id); - id = ReactComponentTreeHook.getParentID(id); - } - return info; - }, - getChildIDs: function (id) { - var item = get(id); - return item ? item.childIDs : []; - }, - getDisplayName: function (id) { - var element = ReactComponentTreeHook.getElement(id); - if (!element) { - return null; - } - return getDisplayName(element); - }, - getElement: function (id) { - var item = get(id); - return item ? item.element : null; - }, - getOwnerID: function (id) { - var element = ReactComponentTreeHook.getElement(id); - if (!element || !element._owner) { - return null; - } - return element._owner._debugID; - }, - getParentID: function (id) { - var item = get(id); - return item ? item.parentID : null; - }, - getSource: function (id) { - var item = get(id); - var element = item ? item.element : null; - var source = element != null ? element._source : null; - return source; - }, - getText: function (id) { - var element = ReactComponentTreeHook.getElement(id); - if (typeof element === 'string') { - return element; - } else if (typeof element === 'number') { - return '' + element; - } else { - return null; - } - }, - getUpdateCount: function (id) { - var item = get(id); - return item ? item.updateCount : 0; - }, - - - getRegisteredIDs: getRegisteredIDs, - - getRootIDs: getRootIDs -}; - -module.exports = ReactComponentTreeHook; -},{"140":140,"162":162,"171":171,"37":37}],36:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactCompositeComponent - */ - -'use strict'; - -var _prodInvariant = _dereq_(140), - _assign = _dereq_(172); - -var ReactComponentEnvironment = _dereq_(34); -var ReactCurrentOwner = _dereq_(37); -var ReactElement = _dereq_(61); -var ReactErrorUtils = _dereq_(64); -var ReactInstanceMap = _dereq_(72); -var ReactInstrumentation = _dereq_(73); -var ReactNodeTypes = _dereq_(79); -var ReactPropTypeLocations = _dereq_(83); -var ReactReconciler = _dereq_(88); - -var checkReactTypeSpec = _dereq_(119); -var emptyObject = _dereq_(155); -var invariant = _dereq_(162); -var shallowEqual = _dereq_(170); -var shouldUpdateReactComponent = _dereq_(144); -var warning = _dereq_(171); - -var CompositeTypes = { - ImpureClass: 0, - PureClass: 1, - StatelessFunctional: 2 -}; - -function StatelessComponent(Component) {} -StatelessComponent.prototype.render = function () { - var Component = ReactInstanceMap.get(this)._currentElement.type; - var element = Component(this.props, this.context, this.updater); - warnIfInvalidElement(Component, element); - return element; -}; - -function warnIfInvalidElement(Component, element) { - if ("development" !== 'production') { - "development" !== 'production' ? warning(element === null || element === false || ReactElement.isValidElement(element), '%s(...): A valid React element (or null) must be returned. You may have ' + 'returned undefined, an array or some other invalid object.', Component.displayName || Component.name || 'Component') : void 0; - "development" !== 'production' ? warning(!Component.childContextTypes, '%s(...): childContextTypes cannot be defined on a functional component.', Component.displayName || Component.name || 'Component') : void 0; - } -} - -function shouldConstruct(Component) { - return !!(Component.prototype && Component.prototype.isReactComponent); -} - -function isPureComponent(Component) { - return !!(Component.prototype && Component.prototype.isPureReactComponent); -} - -// Separated into a function to contain deoptimizations caused by try/finally. -function measureLifeCyclePerf(fn, debugID, timerType) { - if (debugID === 0) { - // Top-level wrappers (see ReactMount) and empty components (see - // ReactDOMEmptyComponent) are invisible to hooks and devtools. - // Both are implementation details that should go away in the future. - return fn(); - } - - ReactInstrumentation.debugTool.onBeginLifeCycleTimer(debugID, timerType); - try { - return fn(); - } finally { - ReactInstrumentation.debugTool.onEndLifeCycleTimer(debugID, timerType); - } -} - -/** - * ------------------ The Life-Cycle of a Composite Component ------------------ - * - * - constructor: Initialization of state. The instance is now retained. - * - componentWillMount - * - render - * - [children's constructors] - * - [children's componentWillMount and render] - * - [children's componentDidMount] - * - componentDidMount - * - * Update Phases: - * - componentWillReceiveProps (only called if parent updated) - * - shouldComponentUpdate - * - componentWillUpdate - * - render - * - [children's constructors or receive props phases] - * - componentDidUpdate - * - * - componentWillUnmount - * - [children's componentWillUnmount] - * - [children destroyed] - * - (destroyed): The instance is now blank, released by React and ready for GC. - * - * ----------------------------------------------------------------------------- - */ - -/** - * An incrementing ID assigned to each component when it is mounted. This is - * used to enforce the order in which `ReactUpdates` updates dirty components. - * - * @private - */ -var nextMountID = 1; - -/** - * @lends {ReactCompositeComponent.prototype} - */ -var ReactCompositeComponentMixin = { - - /** - * Base constructor for all composite component. - * - * @param {ReactElement} element - * @final - * @internal - */ - construct: function (element) { - this._currentElement = element; - this._rootNodeID = 0; - this._compositeType = null; - this._instance = null; - this._hostParent = null; - this._hostContainerInfo = null; - - // See ReactUpdateQueue - this._updateBatchNumber = null; - this._pendingElement = null; - this._pendingStateQueue = null; - this._pendingReplaceState = false; - this._pendingForceUpdate = false; - - this._renderedNodeType = null; - this._renderedComponent = null; - this._context = null; - this._mountOrder = 0; - this._topLevelWrapper = null; - - // See ReactUpdates and ReactUpdateQueue. - this._pendingCallbacks = null; - - // ComponentWillUnmount shall only be called once - this._calledComponentWillUnmount = false; - - if ("development" !== 'production') { - this._warnedAboutRefsInRender = false; - } - }, - - /** - * Initializes the component, renders markup, and registers event listeners. - * - * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction - * @param {?object} hostParent - * @param {?object} hostContainerInfo - * @param {?object} context - * @return {?string} Rendered markup to be inserted into the DOM. - * @final - * @internal - */ - mountComponent: function (transaction, hostParent, hostContainerInfo, context) { - var _this = this; - - this._context = context; - this._mountOrder = nextMountID++; - this._hostParent = hostParent; - this._hostContainerInfo = hostContainerInfo; - - var publicProps = this._currentElement.props; - var publicContext = this._processContext(context); - - var Component = this._currentElement.type; - - var updateQueue = transaction.getUpdateQueue(); - - // Initialize the public class - var doConstruct = shouldConstruct(Component); - var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue); - var renderedElement; - - // Support functional components - if (!doConstruct && (inst == null || inst.render == null)) { - renderedElement = inst; - warnIfInvalidElement(Component, renderedElement); - !(inst === null || inst === false || ReactElement.isValidElement(inst)) ? "development" !== 'production' ? invariant(false, '%s(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.', Component.displayName || Component.name || 'Component') : _prodInvariant('105', Component.displayName || Component.name || 'Component') : void 0; - inst = new StatelessComponent(Component); - this._compositeType = CompositeTypes.StatelessFunctional; - } else { - if (isPureComponent(Component)) { - this._compositeType = CompositeTypes.PureClass; - } else { - this._compositeType = CompositeTypes.ImpureClass; - } - } - - if ("development" !== 'production') { - // This will throw later in _renderValidatedComponent, but add an early - // warning now to help debugging - if (inst.render == null) { - "development" !== 'production' ? warning(false, '%s(...): No `render` method found on the returned component ' + 'instance: you may have forgotten to define `render`.', Component.displayName || Component.name || 'Component') : void 0; - } - - var propsMutated = inst.props !== publicProps; - var componentName = Component.displayName || Component.name || 'Component'; - - "development" !== 'production' ? warning(inst.props === undefined || !propsMutated, '%s(...): When calling super() in `%s`, make sure to pass ' + 'up the same props that your component\'s constructor was passed.', componentName, componentName) : void 0; - } - - // These should be set up in the constructor, but as a convenience for - // simpler class abstractions, we set them up after the fact. - inst.props = publicProps; - inst.context = publicContext; - inst.refs = emptyObject; - inst.updater = updateQueue; - - this._instance = inst; - - // Store a reference from the instance back to the internal representation - ReactInstanceMap.set(inst, this); - - if ("development" !== 'production') { - // Since plain JS classes are defined without any special initialization - // logic, we can not catch common errors early. Therefore, we have to - // catch them here, at initialization time, instead. - "development" !== 'production' ? warning(!inst.getInitialState || inst.getInitialState.isReactClassApproved, 'getInitialState was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Did you mean to define a state property instead?', this.getName() || 'a component') : void 0; - "development" !== 'production' ? warning(!inst.getDefaultProps || inst.getDefaultProps.isReactClassApproved, 'getDefaultProps was defined on %s, a plain JavaScript class. ' + 'This is only supported for classes created using React.createClass. ' + 'Use a static property to define defaultProps instead.', this.getName() || 'a component') : void 0; - "development" !== 'production' ? warning(!inst.propTypes, 'propTypes was defined as an instance property on %s. Use a static ' + 'property to define propTypes instead.', this.getName() || 'a component') : void 0; - "development" !== 'production' ? warning(!inst.contextTypes, 'contextTypes was defined as an instance property on %s. Use a ' + 'static property to define contextTypes instead.', this.getName() || 'a component') : void 0; - "development" !== 'production' ? warning(typeof inst.componentShouldUpdate !== 'function', '%s has a method called ' + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + 'The name is phrased as a question because the function is ' + 'expected to return a value.', this.getName() || 'A component') : void 0; - "development" !== 'production' ? warning(typeof inst.componentDidUnmount !== 'function', '%s has a method called ' + 'componentDidUnmount(). But there is no such lifecycle method. ' + 'Did you mean componentWillUnmount()?', this.getName() || 'A component') : void 0; - "development" !== 'production' ? warning(typeof inst.componentWillRecieveProps !== 'function', '%s has a method called ' + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', this.getName() || 'A component') : void 0; - } - - var initialState = inst.state; - if (initialState === undefined) { - inst.state = initialState = null; - } - !(typeof initialState === 'object' && !Array.isArray(initialState)) ? "development" !== 'production' ? invariant(false, '%s.state: must be set to an object or null', this.getName() || 'ReactCompositeComponent') : _prodInvariant('106', this.getName() || 'ReactCompositeComponent') : void 0; - - this._pendingStateQueue = null; - this._pendingReplaceState = false; - this._pendingForceUpdate = false; - - var markup; - if (inst.unstable_handleError) { - markup = this.performInitialMountWithErrorHandling(renderedElement, hostParent, hostContainerInfo, transaction, context); - } else { - markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context); - } - - if (inst.componentDidMount) { - if ("development" !== 'production') { - transaction.getReactMountReady().enqueue(function () { - measureLifeCyclePerf(function () { - return inst.componentDidMount(); - }, _this._debugID, 'componentDidMount'); - }); - } else { - transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); - } - } - - return markup; - }, - - _constructComponent: function (doConstruct, publicProps, publicContext, updateQueue) { - if ("development" !== 'production') { - ReactCurrentOwner.current = this; - try { - return this._constructComponentWithoutOwner(doConstruct, publicProps, publicContext, updateQueue); - } finally { - ReactCurrentOwner.current = null; - } - } else { - return this._constructComponentWithoutOwner(doConstruct, publicProps, publicContext, updateQueue); - } - }, - - _constructComponentWithoutOwner: function (doConstruct, publicProps, publicContext, updateQueue) { - var Component = this._currentElement.type; - - if (doConstruct) { - if ("development" !== 'production') { - return measureLifeCyclePerf(function () { - return new Component(publicProps, publicContext, updateQueue); - }, this._debugID, 'ctor'); - } else { - return new Component(publicProps, publicContext, updateQueue); - } - } - - // This can still be an instance in case of factory components - // but we'll count this as time spent rendering as the more common case. - if ("development" !== 'production') { - return measureLifeCyclePerf(function () { - return Component(publicProps, publicContext, updateQueue); - }, this._debugID, 'render'); - } else { - return Component(publicProps, publicContext, updateQueue); - } - }, - - performInitialMountWithErrorHandling: function (renderedElement, hostParent, hostContainerInfo, transaction, context) { - var markup; - var checkpoint = transaction.checkpoint(); - try { - markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context); - } catch (e) { - // Roll back to checkpoint, handle error (which may add items to the transaction), and take a new checkpoint - transaction.rollback(checkpoint); - this._instance.unstable_handleError(e); - if (this._pendingStateQueue) { - this._instance.state = this._processPendingState(this._instance.props, this._instance.context); - } - checkpoint = transaction.checkpoint(); - - this._renderedComponent.unmountComponent(true); - transaction.rollback(checkpoint); - - // Try again - we've informed the component about the error, so they can render an error message this time. - // If this throws again, the error will bubble up (and can be caught by a higher error boundary). - markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context); - } - return markup; - }, - - performInitialMount: function (renderedElement, hostParent, hostContainerInfo, transaction, context) { - var inst = this._instance; - - var debugID = 0; - if ("development" !== 'production') { - debugID = this._debugID; - } - - if (inst.componentWillMount) { - if ("development" !== 'production') { - measureLifeCyclePerf(function () { - return inst.componentWillMount(); - }, debugID, 'componentWillMount'); - } else { - inst.componentWillMount(); - } - // When mounting, calls to `setState` by `componentWillMount` will set - // `this._pendingStateQueue` without triggering a re-render. - if (this._pendingStateQueue) { - inst.state = this._processPendingState(inst.props, inst.context); - } - } - - // If not a stateless component, we now render - if (renderedElement === undefined) { - renderedElement = this._renderValidatedComponent(); - } - - var nodeType = ReactNodeTypes.getType(renderedElement); - this._renderedNodeType = nodeType; - var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */ - ); - this._renderedComponent = child; - - var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID); - - if ("development" !== 'production') { - if (debugID !== 0) { - var childDebugIDs = child._debugID !== 0 ? [child._debugID] : []; - ReactInstrumentation.debugTool.onSetChildren(debugID, childDebugIDs); - } - } - - return markup; - }, - - getHostNode: function () { - return ReactReconciler.getHostNode(this._renderedComponent); - }, - - /** - * Releases any resources allocated by `mountComponent`. - * - * @final - * @internal - */ - unmountComponent: function (safely) { - if (!this._renderedComponent) { - return; - } - - var inst = this._instance; - - if (inst.componentWillUnmount && !inst._calledComponentWillUnmount) { - inst._calledComponentWillUnmount = true; - - if (safely) { - var name = this.getName() + '.componentWillUnmount()'; - ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst)); - } else { - if ("development" !== 'production') { - measureLifeCyclePerf(function () { - return inst.componentWillUnmount(); - }, this._debugID, 'componentWillUnmount'); - } else { - inst.componentWillUnmount(); - } - } - } - - if (this._renderedComponent) { - ReactReconciler.unmountComponent(this._renderedComponent, safely); - this._renderedNodeType = null; - this._renderedComponent = null; - this._instance = null; - } - - // Reset pending fields - // Even if this component is scheduled for another update in ReactUpdates, - // it would still be ignored because these fields are reset. - this._pendingStateQueue = null; - this._pendingReplaceState = false; - this._pendingForceUpdate = false; - this._pendingCallbacks = null; - this._pendingElement = null; - - // These fields do not really need to be reset since this object is no - // longer accessible. - this._context = null; - this._rootNodeID = 0; - this._topLevelWrapper = null; - - // Delete the reference from the instance to this internal representation - // which allow the internals to be properly cleaned up even if the user - // leaks a reference to the public instance. - ReactInstanceMap.remove(inst); - - // Some existing components rely on inst.props even after they've been - // destroyed (in event handlers). - // TODO: inst.props = null; - // TODO: inst.state = null; - // TODO: inst.context = null; - }, - - /** - * Filters the context object to only contain keys specified in - * `contextTypes` - * - * @param {object} context - * @return {?object} - * @private - */ - _maskContext: function (context) { - var Component = this._currentElement.type; - var contextTypes = Component.contextTypes; - if (!contextTypes) { - return emptyObject; - } - var maskedContext = {}; - for (var contextName in contextTypes) { - maskedContext[contextName] = context[contextName]; - } - return maskedContext; - }, - - /** - * Filters the context object to only contain keys specified in - * `contextTypes`, and asserts that they are valid. - * - * @param {object} context - * @return {?object} - * @private - */ - _processContext: function (context) { - var maskedContext = this._maskContext(context); - if ("development" !== 'production') { - var Component = this._currentElement.type; - if (Component.contextTypes) { - this._checkContextTypes(Component.contextTypes, maskedContext, ReactPropTypeLocations.context); - } - } - return maskedContext; - }, - - /** - * @param {object} currentContext - * @return {object} - * @private - */ - _processChildContext: function (currentContext) { - var Component = this._currentElement.type; - var inst = this._instance; - var childContext; - - if (inst.getChildContext) { - if ("development" !== 'production') { - ReactInstrumentation.debugTool.onBeginProcessingChildContext(); - try { - childContext = inst.getChildContext(); - } finally { - ReactInstrumentation.debugTool.onEndProcessingChildContext(); - } - } else { - childContext = inst.getChildContext(); - } - } - - if (childContext) { - !(typeof Component.childContextTypes === 'object') ? "development" !== 'production' ? invariant(false, '%s.getChildContext(): childContextTypes must be defined in order to use getChildContext().', this.getName() || 'ReactCompositeComponent') : _prodInvariant('107', this.getName() || 'ReactCompositeComponent') : void 0; - if ("development" !== 'production') { - this._checkContextTypes(Component.childContextTypes, childContext, ReactPropTypeLocations.childContext); - } - for (var name in childContext) { - !(name in Component.childContextTypes) ? "development" !== 'production' ? invariant(false, '%s.getChildContext(): key "%s" is not defined in childContextTypes.', this.getName() || 'ReactCompositeComponent', name) : _prodInvariant('108', this.getName() || 'ReactCompositeComponent', name) : void 0; - } - return _assign({}, currentContext, childContext); - } - return currentContext; - }, - - /** - * Assert that the context types are valid - * - * @param {object} typeSpecs Map of context field to a ReactPropType - * @param {object} values Runtime values that need to be type-checked - * @param {string} location e.g. "prop", "context", "child context" - * @private - */ - _checkContextTypes: function (typeSpecs, values, location) { - checkReactTypeSpec(typeSpecs, values, location, this.getName(), null, this._debugID); - }, - - receiveComponent: function (nextElement, transaction, nextContext) { - var prevElement = this._currentElement; - var prevContext = this._context; - - this._pendingElement = null; - - this.updateComponent(transaction, prevElement, nextElement, prevContext, nextContext); - }, - - /** - * If any of `_pendingElement`, `_pendingStateQueue`, or `_pendingForceUpdate` - * is set, update the component. - * - * @param {ReactReconcileTransaction} transaction - * @internal - */ - performUpdateIfNecessary: function (transaction) { - if (this._pendingElement != null) { - ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context); - } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) { - this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context); - } else { - this._updateBatchNumber = null; - } - }, - - /** - * Perform an update to a mounted component. The componentWillReceiveProps and - * shouldComponentUpdate methods are called, then (assuming the update isn't - * skipped) the remaining update lifecycle methods are called and the DOM - * representation is updated. - * - * By default, this implements React's rendering and reconciliation algorithm. - * Sophisticated clients may wish to override this. - * - * @param {ReactReconcileTransaction} transaction - * @param {ReactElement} prevParentElement - * @param {ReactElement} nextParentElement - * @internal - * @overridable - */ - updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) { - var inst = this._instance; - !(inst != null) ? "development" !== 'production' ? invariant(false, 'Attempted to update component `%s` that has already been unmounted (or failed to mount).', this.getName() || 'ReactCompositeComponent') : _prodInvariant('136', this.getName() || 'ReactCompositeComponent') : void 0; - - var willReceive = false; - var nextContext; - - // Determine if the context has changed or not - if (this._context === nextUnmaskedContext) { - nextContext = inst.context; - } else { - nextContext = this._processContext(nextUnmaskedContext); - willReceive = true; - } - - var prevProps = prevParentElement.props; - var nextProps = nextParentElement.props; - - // Not a simple state update but a props update - if (prevParentElement !== nextParentElement) { - willReceive = true; - } - - // An update here will schedule an update but immediately set - // _pendingStateQueue which will ensure that any state updates gets - // immediately reconciled instead of waiting for the next batch. - if (willReceive && inst.componentWillReceiveProps) { - if ("development" !== 'production') { - measureLifeCyclePerf(function () { - return inst.componentWillReceiveProps(nextProps, nextContext); - }, this._debugID, 'componentWillReceiveProps'); - } else { - inst.componentWillReceiveProps(nextProps, nextContext); - } - } - - var nextState = this._processPendingState(nextProps, nextContext); - var shouldUpdate = true; - - if (!this._pendingForceUpdate) { - if (inst.shouldComponentUpdate) { - if ("development" !== 'production') { - shouldUpdate = measureLifeCyclePerf(function () { - return inst.shouldComponentUpdate(nextProps, nextState, nextContext); - }, this._debugID, 'shouldComponentUpdate'); - } else { - shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); - } - } else { - if (this._compositeType === CompositeTypes.PureClass) { - shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); - } - } - } - - if ("development" !== 'production') { - "development" !== 'production' ? warning(shouldUpdate !== undefined, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', this.getName() || 'ReactCompositeComponent') : void 0; - } - - this._updateBatchNumber = null; - if (shouldUpdate) { - this._pendingForceUpdate = false; - // Will set `this.props`, `this.state` and `this.context`. - this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext); - } else { - // If it's determined that a component should not update, we still want - // to set props and state but we shortcut the rest of the update. - this._currentElement = nextParentElement; - this._context = nextUnmaskedContext; - inst.props = nextProps; - inst.state = nextState; - inst.context = nextContext; - } - }, - - _processPendingState: function (props, context) { - var inst = this._instance; - var queue = this._pendingStateQueue; - var replace = this._pendingReplaceState; - this._pendingReplaceState = false; - this._pendingStateQueue = null; - - if (!queue) { - return inst.state; - } - - if (replace && queue.length === 1) { - return queue[0]; - } - - var nextState = _assign({}, replace ? queue[0] : inst.state); - for (var i = replace ? 1 : 0; i < queue.length; i++) { - var partial = queue[i]; - _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial); - } - - return nextState; - }, - - /** - * Merges new props and state, notifies delegate methods of update and - * performs update. - * - * @param {ReactElement} nextElement Next element - * @param {object} nextProps Next public object to set as properties. - * @param {?object} nextState Next object to set as state. - * @param {?object} nextContext Next public object to set as context. - * @param {ReactReconcileTransaction} transaction - * @param {?object} unmaskedContext - * @private - */ - _performComponentUpdate: function (nextElement, nextProps, nextState, nextContext, transaction, unmaskedContext) { - var _this2 = this; - - var inst = this._instance; - - var hasComponentDidUpdate = Boolean(inst.componentDidUpdate); - var prevProps; - var prevState; - var prevContext; - if (hasComponentDidUpdate) { - prevProps = inst.props; - prevState = inst.state; - prevContext = inst.context; - } - - if (inst.componentWillUpdate) { - if ("development" !== 'production') { - measureLifeCyclePerf(function () { - return inst.componentWillUpdate(nextProps, nextState, nextContext); - }, this._debugID, 'componentWillUpdate'); - } else { - inst.componentWillUpdate(nextProps, nextState, nextContext); - } - } - - this._currentElement = nextElement; - this._context = unmaskedContext; - inst.props = nextProps; - inst.state = nextState; - inst.context = nextContext; - - this._updateRenderedComponent(transaction, unmaskedContext); - - if (hasComponentDidUpdate) { - if ("development" !== 'production') { - transaction.getReactMountReady().enqueue(function () { - measureLifeCyclePerf(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), _this2._debugID, 'componentDidUpdate'); - }); - } else { - transaction.getReactMountReady().enqueue(inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext), inst); - } - } - }, - - /** - * Call the component's `render` method and update the DOM accordingly. - * - * @param {ReactReconcileTransaction} transaction - * @internal - */ - _updateRenderedComponent: function (transaction, context) { - var prevComponentInstance = this._renderedComponent; - var prevRenderedElement = prevComponentInstance._currentElement; - var nextRenderedElement = this._renderValidatedComponent(); - - var debugID = 0; - if ("development" !== 'production') { - debugID = this._debugID; - } - - if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { - ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context)); - } else { - var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance); - ReactReconciler.unmountComponent(prevComponentInstance, false); - - var nodeType = ReactNodeTypes.getType(nextRenderedElement); - this._renderedNodeType = nodeType; - var child = this._instantiateReactComponent(nextRenderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */ - ); - this._renderedComponent = child; - - var nextMarkup = ReactReconciler.mountComponent(child, transaction, this._hostParent, this._hostContainerInfo, this._processChildContext(context), debugID); - - if ("development" !== 'production') { - if (debugID !== 0) { - var childDebugIDs = child._debugID !== 0 ? [child._debugID] : []; - ReactInstrumentation.debugTool.onSetChildren(debugID, childDebugIDs); - } - } - - this._replaceNodeWithMarkup(oldHostNode, nextMarkup, prevComponentInstance); - } - }, - - /** - * Overridden in shallow rendering. - * - * @protected - */ - _replaceNodeWithMarkup: function (oldHostNode, nextMarkup, prevInstance) { - ReactComponentEnvironment.replaceNodeWithMarkup(oldHostNode, nextMarkup, prevInstance); - }, - - /** - * @protected - */ - _renderValidatedComponentWithoutOwnerOrContext: function () { - var inst = this._instance; - var renderedComponent; - - if ("development" !== 'production') { - renderedComponent = measureLifeCyclePerf(function () { - return inst.render(); - }, this._debugID, 'render'); - } else { - renderedComponent = inst.render(); - } - - if ("development" !== 'production') { - // We allow auto-mocks to proceed as if they're returning null. - if (renderedComponent === undefined && inst.render._isMockFunction) { - // This is probably bad practice. Consider warning here and - // deprecating this convenience. - renderedComponent = null; - } - } - - return renderedComponent; - }, - - /** - * @private - */ - _renderValidatedComponent: function () { - var renderedComponent; - if ("development" !== 'production' || this._compositeType !== CompositeTypes.StatelessFunctional) { - ReactCurrentOwner.current = this; - try { - renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext(); - } finally { - ReactCurrentOwner.current = null; - } - } else { - renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext(); - } - !( - // TODO: An `isValidNode` function would probably be more appropriate - renderedComponent === null || renderedComponent === false || ReactElement.isValidElement(renderedComponent)) ? "development" !== 'production' ? invariant(false, '%s.render(): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.', this.getName() || 'ReactCompositeComponent') : _prodInvariant('109', this.getName() || 'ReactCompositeComponent') : void 0; - - return renderedComponent; - }, - - /** - * Lazily allocates the refs object and stores `component` as `ref`. - * - * @param {string} ref Reference name. - * @param {component} component Component to store as `ref`. - * @final - * @private - */ - attachRef: function (ref, component) { - var inst = this.getPublicInstance(); - !(inst != null) ? "development" !== 'production' ? invariant(false, 'Stateless function components cannot have refs.') : _prodInvariant('110') : void 0; - var publicComponentInstance = component.getPublicInstance(); - if ("development" !== 'production') { - var componentName = component && component.getName ? component.getName() : 'a component'; - "development" !== 'production' ? warning(publicComponentInstance != null || component._compositeType !== CompositeTypes.StatelessFunctional, 'Stateless function components cannot be given refs ' + '(See ref "%s" in %s created by %s). ' + 'Attempts to access this ref will fail.', ref, componentName, this.getName()) : void 0; - } - var refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs; - refs[ref] = publicComponentInstance; - }, - - /** - * Detaches a reference name. - * - * @param {string} ref Name to dereference. - * @final - * @private - */ - detachRef: function (ref) { - var refs = this.getPublicInstance().refs; - delete refs[ref]; - }, - - /** - * Get a text description of the component that can be used to identify it - * in error messages. - * @return {string} The name or null. - * @internal - */ - getName: function () { - var type = this._currentElement.type; - var constructor = this._instance && this._instance.constructor; - return type.displayName || constructor && constructor.displayName || type.name || constructor && constructor.name || null; - }, - - /** - * Get the publicly accessible representation of this component - i.e. what - * is exposed by refs and returned by render. Can be null for stateless - * components. - * - * @return {ReactComponent} the public component instance. - * @internal - */ - getPublicInstance: function () { - var inst = this._instance; - if (this._compositeType === CompositeTypes.StatelessFunctional) { - return null; - } - return inst; - }, - - // Stub - _instantiateReactComponent: null - -}; - -var ReactCompositeComponent = { - - Mixin: ReactCompositeComponentMixin - -}; - -module.exports = ReactCompositeComponent; -},{"119":119,"140":140,"144":144,"155":155,"162":162,"170":170,"171":171,"172":172,"34":34,"37":37,"61":61,"64":64,"72":72,"73":73,"79":79,"83":83,"88":88}],37:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactCurrentOwner - */ - -'use strict'; - -/** - * Keeps track of the current owner. - * - * The current owner is the component who should own any components that are - * currently being constructed. - */ - -var ReactCurrentOwner = { - - /** - * @internal - * @type {ReactComponent} - */ - current: null - -}; - -module.exports = ReactCurrentOwner; -},{}],38:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactDOM - */ - -/* globals __REACT_DEVTOOLS_GLOBAL_HOOK__*/ - -'use strict'; - -var ReactDOMComponentTree = _dereq_(42); -var ReactDefaultInjection = _dereq_(60); -var ReactMount = _dereq_(76); -var ReactReconciler = _dereq_(88); -var ReactUpdates = _dereq_(96); -var ReactVersion = _dereq_(97); - -var findDOMNode = _dereq_(123); -var getHostComponentFromComposite = _dereq_(130); -var renderSubtreeIntoContainer = _dereq_(141); -var warning = _dereq_(171); - -ReactDefaultInjection.inject(); - -var ReactDOM = { - findDOMNode: findDOMNode, - render: ReactMount.render, - unmountComponentAtNode: ReactMount.unmountComponentAtNode, - version: ReactVersion, - - /* eslint-disable camelcase */ - unstable_batchedUpdates: ReactUpdates.batchedUpdates, - unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer -}; - -// Inject the runtime into a devtools global hook regardless of browser. -// Allows for debugging when the hook is injected on the page. -/* eslint-enable camelcase */ -if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined' && typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.inject === 'function') { - __REACT_DEVTOOLS_GLOBAL_HOOK__.inject({ - ComponentTree: { - getClosestInstanceFromNode: ReactDOMComponentTree.getClosestInstanceFromNode, - getNodeFromInstance: function (inst) { - // inst is an internal instance (but could be a composite) - if (inst._renderedComponent) { - inst = getHostComponentFromComposite(inst); - } - if (inst) { - return ReactDOMComponentTree.getNodeFromInstance(inst); - } else { - return null; - } - } - }, - Mount: ReactMount, - Reconciler: ReactReconciler - }); -} - -if ("development" !== 'production') { - var ExecutionEnvironment = _dereq_(148); - if (ExecutionEnvironment.canUseDOM && window.top === window.self) { - - // First check if devtools is not installed - if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') { - // If we're in Chrome or Firefox, provide a download link if not installed. - if (navigator.userAgent.indexOf('Chrome') > -1 && navigator.userAgent.indexOf('Edge') === -1 || navigator.userAgent.indexOf('Firefox') > -1) { - // Firefox does not have the issue with devtools loaded over file:// - var showFileUrlMessage = window.location.protocol.indexOf('http') === -1 && navigator.userAgent.indexOf('Firefox') === -1; - console.debug('Download the React DevTools ' + (showFileUrlMessage ? 'and use an HTTP server (instead of a file: URL) ' : '') + 'for a better development experience: ' + 'https://fb.me/react-devtools'); - } - } - - var testFunc = function testFn() {}; - "development" !== 'production' ? warning((testFunc.name || testFunc.toString()).indexOf('testFn') !== -1, 'It looks like you\'re using a minified copy of the development build ' + 'of React. When deploying React apps to production, make sure to use ' + 'the production build which skips development warnings and is faster. ' + 'See https://fb.me/react-minification for more details.') : void 0; - - // If we're in IE8, check to see if we are in compatibility mode and provide - // information on preventing compatibility mode - var ieCompatibilityMode = document.documentMode && document.documentMode < 8; - - "development" !== 'production' ? warning(!ieCompatibilityMode, 'Internet Explorer is running in compatibility mode; please add the ' + 'following tag to your HTML to prevent this from happening: ' + '') : void 0; - - var expectedFeatures = [ - // shims - Array.isArray, Array.prototype.every, Array.prototype.forEach, Array.prototype.indexOf, Array.prototype.map, Date.now, Function.prototype.bind, Object.keys, String.prototype.split, String.prototype.trim]; - - for (var i = 0; i < expectedFeatures.length; i++) { - if (!expectedFeatures[i]) { - "development" !== 'production' ? warning(false, 'One or more ES5 shims expected by React are not available: ' + 'https://fb.me/react-warning-polyfills') : void 0; - break; - } - } - } -} - -if ("development" !== 'production') { - var ReactInstrumentation = _dereq_(73); - var ReactDOMUnknownPropertyHook = _dereq_(57); - var ReactDOMNullInputValuePropHook = _dereq_(49); - - ReactInstrumentation.debugTool.addHook(ReactDOMUnknownPropertyHook); - ReactInstrumentation.debugTool.addHook(ReactDOMNullInputValuePropHook); -} - -module.exports = ReactDOM; -},{"123":123,"130":130,"141":141,"148":148,"171":171,"42":42,"49":49,"57":57,"60":60,"73":73,"76":76,"88":88,"96":96,"97":97}],39:[function(_dereq_,module,exports){ -/** - * Copyright 2013-present, 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. - * - * @providesModule ReactDOMButton - */ - -'use strict'; - -var DisabledInputUtils = _dereq_(14); - -/** - * Implements a