Skip to content

Commit

Permalink
fix: monaco-graphql exports, validate on load (#3384)
Browse files Browse the repository at this point in the history
- validates both graphql and json variables on load
- reduces bundle size further, allowing users to specify which features they want, instead of importing all of them
- adds a typescript mode to the vite/next.js demos to ensure our implementation doesn't break `typescript` language mode & worker
- fix variables json validation by pointing to a meta schema
- add `exports` aliases that are backwards compatible, but allow `monaco-graphql/initializeMode` and `monaco-graphql/graphql.worker` to be imported directly
- add a `monaco-graphql/lite` import for minimal features
- import the json language mode only if variables json validation is used
  • Loading branch information
acao committed Aug 12, 2023
1 parent 46aa56e commit 31ded5e
Show file tree
Hide file tree
Showing 21 changed files with 254 additions and 87 deletions.
15 changes: 15 additions & 0 deletions .changeset/fluffy-pens-lick.md
@@ -0,0 +1,15 @@
---
'monaco-graphql': patch
---

import only `editor.api` & basic features, add `monaco-graphql/lite`

- switch from exporting `edcore.js` to `editor.api.js` as recommended, and minimal features to get the editor working
- `edcore` imports `editor.all` which contains many monaco-editor features we don't use
- dynamic import of `json` language mode only if the user supplies configuration for json validation
- update monaco examples to show minimal `typescript` implementation alongside `graphql`
- add new simplified `exports` with backwards compatibility:
- `monaco-graphql/initializeMode`
- `monaco-graphql/graphql.worker`
- `monaco-graphql/monaco-editor`
- introduce `monaco-graphql/lite` for users who want the most minimum version possible, and to only import the features they need
4 changes: 0 additions & 4 deletions .github/workflows/pr.yml
Expand Up @@ -11,8 +11,6 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 16
Expand All @@ -38,8 +36,6 @@ jobs:

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 16
Expand Down
1 change: 1 addition & 0 deletions custom-words.txt
Expand Up @@ -130,6 +130,7 @@ matchingbracket
middlewares
modulemap
newhope
nextjs
nocheck
nocursor
nonmatchingbracket
Expand Down
13 changes: 12 additions & 1 deletion examples/monaco-graphql-nextjs/next.config.js
Expand Up @@ -19,7 +19,8 @@ const nextConfig = {
// 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',
// this import has moved a bit, so let's make it absolute from the module path
'monaco-editor/esm/vs/common/marked/marked.js': 'marked',
};
if (!options.isServer) {
config.plugins.push(
Expand All @@ -40,6 +41,16 @@ const nextConfig = {
entry: 'monaco-graphql/esm/graphql.worker.js',
},
},
// TOD: webpack monaco editor plugin breaks on languages: ['typescript']
// so this was necessary
// see: https://github.com/microsoft/monaco-editor/issues/2738
{
label: 'typescript',
worker: {
id: 'typescript',
entry: 'monaco-editor/esm/vs/language/typescript/ts.worker.js',
},
},
],
}),
);
Expand Down
57 changes: 45 additions & 12 deletions examples/monaco-graphql-nextjs/src/constants.ts
@@ -1,7 +1,8 @@
import { editor, Uri } from 'monaco-graphql/esm/monaco-editor';
import { initializeMode } from 'monaco-graphql/esm/initializeMode';
import { parse, print } from 'graphql';

type ModelType = 'operations' | 'variables' | 'response';
type ModelType = 'operations' | 'variables' | 'response' | 'typescript';

export const GRAPHQL_URL = 'https://countries.trevorblades.com';

Expand All @@ -18,35 +19,59 @@ export const STORAGE_KEY = {
variables: 'variables',
};

export const DEFAULT_VALUE: Record<ModelType, string> = {
operations:
localStorage.getItem(STORAGE_KEY.operations) ??
`# CMD/CTRL + Return/Enter will execute the operation,
// allow a typo to test validation on schema load
/* cSpell:disable */
const 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!) {
query Example($code: ID!, $filter: LanguageFilterInput!) {
country(code: $code) {
awsRegion
native
phone
emoj
}
}`,
languages(filter: $filter) {
name
}
}`;
/* cSpell:enable */

let prettyOp = '';
const makeOpTemplate = (op: string) => {
try {
prettyOp = print(parse(op));
return `
const graphql = (arg: TemplateStringsArray): string => arg[0]
const op = graphql\`\n${prettyOp}\n\``;
} catch {
return prettyOp;
}
};

export const DEFAULT_VALUE: Record<ModelType, string> = {
operations,
variables:
localStorage.getItem(STORAGE_KEY.variables) ??
`{
"code": "UA"
}`,
response: '',
typescript: makeOpTemplate(operations),
};

export const FILE_SYSTEM_PATH: Record<
ModelType,
`${string}.${'graphql' | 'json'}`
`${string}.${'graphql' | 'json' | 'ts'}`
> = {
operations: 'operations.graphql',
variables: 'variables.json',
response: 'response.json',
typescript: 'typescript.ts',
};

export const MONACO_GRAPHQL_API = initializeMode({
Expand All @@ -70,14 +95,22 @@ export const MODEL: Record<ModelType, editor.ITextModel> = {
operations: getOrCreateModel('operations'),
variables: getOrCreateModel('variables'),
response: getOrCreateModel('response'),
typescript: getOrCreateModel('typescript'),
};

function getOrCreateModel(
type: 'operations' | 'variables' | 'response',
): editor.ITextModel {
MODEL.operations.onDidChangeContent(() => {
const value = MODEL.operations.getValue();
MODEL.typescript.setValue(makeOpTemplate(value));
});

function getOrCreateModel(type: ModelType): editor.ITextModel {
const uri = Uri.file(FILE_SYSTEM_PATH[type]);
const defaultValue = DEFAULT_VALUE[type];
const language = uri.path.split('.').pop();
let language = uri.path.split('.').pop();
console.log({ language });
if (language === 'ts') {
language = 'typescript';
}
return (
editor.getModel(uri) ?? editor.createModel(defaultValue, language, uri)
);
Expand Down
27 changes: 26 additions & 1 deletion examples/monaco-graphql-nextjs/src/editor.tsx
Expand Up @@ -6,6 +6,13 @@ import {
KeyCode,
languages,
} from 'monaco-graphql/esm/monaco-editor';

// to get typescript mode working
import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution';
import 'monaco-editor/esm/vs/editor/contrib/peekView/browser/peekView';
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/browser/parameterHints';
import 'monaco-editor/esm/vs/language/typescript/monaco.contribution';

import { createGraphiQLFetcher } from '@graphiql/toolkit';
import * as JSONC from 'jsonc-parser';
import {
Expand Down Expand Up @@ -78,12 +85,15 @@ export default function Editor(): ReactElement {
const operationsRef = useRef<HTMLDivElement>(null);
const variablesRef = useRef<HTMLDivElement>(null);
const responseRef = useRef<HTMLDivElement>(null);
const typescriptRef = useRef<HTMLDivElement>(null);
const [operationsEditor, setOperationsEditor] =
useState<editor.IStandaloneCodeEditor>();
const [variablesEditor, setVariablesEditor] =
useState<editor.IStandaloneCodeEditor>();
const [responseEditor, setResponseEditor] =
useState<editor.IStandaloneCodeEditor>();
const [typescriptEditor, setTypescriptEditor] =
useState<editor.IStandaloneCodeEditor>();
const [schema, setSchema] = useState<IntrospectionQuery>();
const [loading, setLoading] = useState(false);
/**
Expand Down Expand Up @@ -132,6 +142,18 @@ export default function Editor(): ReactElement {
}),
);
}
if (!typescriptEditor) {
setTypescriptEditor(
editor.create(typescriptRef.current!, {
model: MODEL.typescript,
...DEFAULT_EDITOR_OPTIONS,
smoothScrolling: true,
readOnly: false,
'semanticHighlighting.enabled': true,
language: 'typescript',
}),
);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- only run once on mount
/**
* Handle the initial schema load
Expand All @@ -156,7 +178,10 @@ export default function Editor(): ReactElement {
<div ref={operationsRef} className="left-editor" />
<div ref={variablesRef} className="left-editor" />
</div>
<div ref={responseRef} className="pane" />
<div className="pane">
<div ref={responseRef} className="left-editor" />
<div ref={typescriptRef} className="left-editor" />
</div>
</>
);
}
3 changes: 2 additions & 1 deletion examples/monaco-graphql-react-vite/vite.config.ts
Expand Up @@ -10,7 +10,8 @@ export default defineConfig({
react(),
monacoEditorPlugin({
publicPath: 'workers',
languageWorkers: ['json', 'editorWorkerService'],
// note that this only loads the worker, not the full main process language support
languageWorkers: ['json', 'typescript', 'editorWorkerService'],
customWorkers: [
{
label: 'graphql',
Expand Down
Expand Up @@ -351,6 +351,10 @@ export function getVariablesJSONSchema(
options?: JSONSchemaOptions,
): JSONSchema6 {
const jsonSchema: PropertiedJSON6 = {
// this gets monaco-json validation working again
// otherwise it shows an error for newer schema draft versions
// variables and graphql types are simple and compatible with all versions of json schema
// since draft 4. package.json and many other schemas still use draft 4
$schema: 'http://json-schema.org/draft-04/schema',
type: 'object',
properties: {},
Expand Down
49 changes: 42 additions & 7 deletions packages/monaco-graphql/README.md
Expand Up @@ -53,7 +53,7 @@ yarn add monaco-graphql
```ts
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';

import { initializeMode } from 'monaco-graphql/esm/initializeMode';
import { initializeMode } from 'monaco-graphql/initializeMode'; // `monaco-graphql/esm/initializeMode` still works

// you can also configure these using the webpack or vite plugins for `monaco-editor`
import GraphQLWorker from 'worker-loader!monaco-graphql/esm/graphql.worker';
Expand Down Expand Up @@ -93,7 +93,8 @@ monaco.editor.create(document.getElementById('someElementId'), {

## Lazy Example

The existing API works as before in terms of instantiating the schema
The existing API works as before in terms of instantiating the schema.
To avoid manually calling getWorker(), you can use the monaco editor plugins for webpack or vite (see examples, and below)

```ts
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
Expand Down Expand Up @@ -151,7 +152,7 @@ any given set of operations
```ts
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';

import { initializeMode } from 'monaco-graphql/esm/initializeMode';
import { initializeMode } from 'monaco-graphql/initializeMode';

import GraphQLWorker from 'worker-loader!monaco-graphql/esm/graphql.worker';

Expand Down Expand Up @@ -235,15 +236,41 @@ MonacoGraphQL.setCompletionSettings({
});
```

You can also experiment with the built-in I think `jsonc`? (MSFT json5-ish
syntax, for `tsconfig.json` etc.) and the 3rd party `monaco-yaml` language modes
You can also experiment with the built-in `jsonc`? (JSON
syntax that allows comments and trailing commas, for `tsconfig.json`, etc.) and the 3rd party `monaco-yaml` language modes
for completion of other types of variable input. you can also experiment with
editor methods to parse detected input into different formats, etc (`yaml`
pastes as `json`, etc.)

You could of course prefer to generate a `jsonschema` form for variables input
using a framework of your choice, instead of an editor. Enjoy!

## `monaco-graphql/lite`

You can also import a "lite" version, and manually enable only the monaco features you want!

Warning: by default, completion and other features will not work, only highlighting and validation.

```ts
import { initializeMode } from 'monaco-graphql/lite';

// enable completion
import 'monaco-editor/esm/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution';

const api = initializeMode({
schemas: [
{
// anything that monaco.URI.from() is compatible with
uri: 'schema.graphql',
// match the monaco file uris for this schema.
// accepts specific filenames and anything `picomatch` supports.
fileMatch: ['operation.graphql'],
schema: myGraphqlSchema as GraphQLSchema,
},
],
});
```

## `MonacoGraphQLAPI` ([typedoc](https://graphiql-test.netlify.app/typedoc/classes/monaco_graphql.monacoMonacoGraphQLAPI.html))

If you call any of these API methods to modify the language service
Expand Down Expand Up @@ -271,7 +298,7 @@ const { api } = languages.graphql;
Otherwise, you can, like in the sync demo above:

```ts
import { initializeMode } from 'monaco-graphql/esm/initializeMode';
import { initializeMode } from 'monaco-graphql/initializeMode';

const api = initializeMode(config);
```
Expand Down Expand Up @@ -299,7 +326,7 @@ monaco.languages.graphql.api.setSchemaConfig([
or you can load the language features only when you have your schema

```ts
import { initializeMode } from 'monaco-graphql/esm/initializeMode';
import { initializeMode } from 'monaco-graphql/initializeMode';

const schemas = [
{
Expand Down Expand Up @@ -402,12 +429,20 @@ you'll can refer to the webpack configuration in the
how it works with webpack and the official `monaco-editor-webpack-plugin`. there
is probably an easier way to configure webpack `worker-loader` for this.

**Notes:**

- for additional features, please specify them in `features` for the webpack plugin, or import them directly
- if you are trying to add `typescript` as a language, there is an outstanding [bug with the webpack plugin](https://github.com/microsoft/monaco-editor/issues/2738), see our [next.js example](../../examples/monaco-graphql-nextjs/next.config.js) for the workaround. do not specify `languages: ['typescript']` or `javascript`

### Vite

You can configure vite to load `monaco-editor` json mode and even the language
editor worker using
[the example for our mode](https://github.com/vdesjs/vite-plugin-monaco-editor#options)

Be sure to import additional editor features and language modes manually, as the vite plugin only allows you to specify `languageWorkers`.
See the vite example to see how to add typescript support

## Web Frameworks

the plain javascript
Expand Down

0 comments on commit 31ded5e

Please sign in to comment.