Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,30 @@ jobs:
- run: yarn lint
- run: yarn test
- run: yarn build

# The example consumes the built package via `file:../`, so it is the only
# check that exercises the published entry points and the monaco-editor peer
# dependency the way a real consumer would. The publish workflow builds it to
# deploy GitHub Pages, and without this job a break there is only discovered
# during a release.
example:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24.x
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn build

- name: install example dependencies
working-directory: ./example
run: yarn install --frozen-lockfile

# react-scripts runs eslint as part of the production build, so this
# covers both linting and compilation of the example.
- name: build example site
working-directory: ./example
run: yarn build
9 changes: 7 additions & 2 deletions example/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# @popsql/monaco-sql-languages example

This folder holds an example of how you could integrate the `@popsql/monaco-sql-languages` module
with your `react-monaco-editor` setup. It was bootstrapped with
with `monaco-editor`. It was bootstrapped with
[Create React App](https://github.com/facebook/create-react-app).

The editor is mounted directly via `monaco.editor.create` rather than through a React wrapper
library. Note that monaco-editor 0.56 serves its ESM entry points through the package `exports`
map, so the import specifier is `monaco-editor/editor/editor.api` — the older
`monaco-editor/esm/vs/editor/editor.api` path no longer resolves.

## Getting Started

Before using this example, you will need to build the outer project:
Expand All @@ -17,7 +22,7 @@ yarn build
Then, you can install the necessary dependencies in this folder:

```bash
yarn build
yarn
```

## Running the example
Expand Down
1 change: 0 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"monaco-editor": "^0.56.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-monaco-editor": "^0.59.0",
"react-scripts": "5.0.1"
},
"scripts": {
Expand Down
38 changes: 0 additions & 38 deletions example/src/App.css

This file was deleted.

128 changes: 86 additions & 42 deletions example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,104 @@ import {
snowflakeLanguageDefinition,
timescaleLanguageDefinition,
} from '@popsql/monaco-sql-languages';
import React, { useCallback } from 'react';
import MonacoEditor from 'react-monaco-editor';
// monaco-editor 0.56 exposes its ESM entry points through the package `exports`
// map, so `monaco-editor/editor/editor.api` is the supported specifier. The
// pre-0.56 `monaco-editor/esm/vs/editor/editor.api` path now resolves to
// ./esm/vs/esm/vs/... and no longer exists.
import * as monaco from 'monaco-editor/editor/editor.api';
import React, { useEffect, useRef, useState } from 'react';

const languageDefinitions = [
bigqueryLanguageDefinition,
clickhouseLanguageDefinition,
pgsqlLanguageDefinition,
prestoLanguageDefinition,
snowflakeLanguageDefinition,
timescaleLanguageDefinition,
];

const INITIAL_SQL = 'SELECT * FROM table';

// Registration mutates global monaco state, so it must happen exactly once
// rather than on every mount/re-render.
let haveLanguagesBeenRegistered = false;

const registerSqlLanguagesOnce = () => {
if (haveLanguagesBeenRegistered) {
return;
}
haveLanguagesBeenRegistered = true;

languageDefinitions.forEach((languageDefinition) => {
monaco.languages.register(languageDefinition);
monaco.languages.onLanguage(languageDefinition.id, async () => {
const { conf, language } = await languageDefinition.loader();
monaco.languages.setMonarchTokensProvider(
languageDefinition.id,
language,
);
monaco.languages.setLanguageConfiguration(languageDefinition.id, conf);
});
});
};

const sortedLanguageIds = languageDefinitions
.map(({ id }) => id)
.sort((a, b) => a.localeCompare(b));

const DEFAULT_LANGUAGE_ID = sortedLanguageIds[0];

const App = () => {
const [code, setCode] = React.useState('SELECT * FROM table');
const [language, setLanguage] = React.useState('bigquery');
const [languages, setLanguages] = React.useState([]);
const editorContainerRef = useRef(null);
const editorRef = useRef(null);
const [selectedLanguageId, setSelectedLanguageId] =
useState(DEFAULT_LANGUAGE_ID);

// Create the editor once; language changes are applied to the existing model
// below so that switching dialects preserves the user's text.
useEffect(() => {
registerSqlLanguagesOnce();

const editorWillMount = useCallback((monaco) => {
const newLanguages = [];
[
bigqueryLanguageDefinition,
clickhouseLanguageDefinition,
pgsqlLanguageDefinition,
prestoLanguageDefinition,
snowflakeLanguageDefinition,
timescaleLanguageDefinition,
].forEach((monacoLanguage) => {
newLanguages.push(monacoLanguage.id);
monaco.languages.register(monacoLanguage);
monaco.languages.onLanguage(monacoLanguage.id, () => {
monacoLanguage.loader().then((mod) => {
monaco.languages.setMonarchTokensProvider(
monacoLanguage.id,
mod.language,
);
monaco.languages.setLanguageConfiguration(
monacoLanguage.id,
mod.conf,
);
});
});
setLanguages(newLanguages.sort());
editorRef.current = monaco.editor.create(editorContainerRef.current, {
automaticLayout: true,
language: DEFAULT_LANGUAGE_ID,
minimap: { enabled: false },
theme: 'vs',
value: INITIAL_SQL,
});

return () => {
editorRef.current?.getModel()?.dispose();
editorRef.current?.dispose();
editorRef.current = null;
};
}, []);

useEffect(() => {
const model = editorRef.current?.getModel();
if (model) {
monaco.editor.setModelLanguage(model, selectedLanguageId);
}
}, [selectedLanguageId]);

return (
<div>
<h1>@popsql/monaco-sql-languages</h1>
<div style={{ marginBottom: 10 }}>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
{languages.map((lang) => (
<option key={lang}>{lang}</option>
<div style={{ marginBottom: 10, marginLeft: 20 }}>
<select
value={selectedLanguageId}
onChange={(event) => setSelectedLanguageId(event.target.value)}
>
{sortedLanguageIds.map((languageId) => (
<option key={languageId} value={languageId}>
{languageId}
</option>
))}
</select>
</div>
<MonacoEditor
width="1000"
height="600"
language={language}
theme="vs-light"
value={code}
editorWillMount={editorWillMount}
onChange={(newValue) => setCode(newValue)}
<div
ref={editorContainerRef}
style={{ border: '1px solid #ccc', height: 600, marginLeft: 20 }}
/>
</div>
);
Expand Down
11 changes: 11 additions & 0 deletions example/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,16 @@ import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';

// monaco spawns its editor web worker itself, and without MonacoEnvironment it
// falls back to loading one from a CDN-style absolute path that does not exist
// here. webpack 5 (via react-scripts 5) bundles the worker from this URL.
window.MonacoEnvironment = {
getWorker: () =>
new Worker(
new URL('monaco-editor/editor/editor.worker.js', import.meta.url),
{ type: 'module' },
),
};

const root = createRoot(document.getElementById('root'));
root.render(<App />);
5 changes: 0 additions & 5 deletions example/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7325,11 +7325,6 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==

react-monaco-editor@^0.59.0:
version "0.59.0"
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.59.0.tgz#a3cdef4a47fd0cb899f412c9d66b365c51a76096"
integrity sha512-SggqfZCdUauNk7GI0388bk5n25zYsQ1ai1i+VhxAgwbCH+MTGl7L1fBNTJ6V+oXeUApf+bpzikprHJEZm9J/zA==

react-refresh@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046"
Expand Down