diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c024839..8987d3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/example/README.md b/example/README.md index 9ef6eda..bc1d54c 100644 --- a/example/README.md +++ b/example/README.md @@ -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: @@ -17,7 +22,7 @@ yarn build Then, you can install the necessary dependencies in this folder: ```bash -yarn build +yarn ``` ## Running the example diff --git a/example/package.json b/example/package.json index 00304a7..89d6aaf 100644 --- a/example/package.json +++ b/example/package.json @@ -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": { diff --git a/example/src/App.css b/example/src/App.css deleted file mode 100644 index 74b5e05..0000000 --- a/example/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/example/src/App.js b/example/src/App.js index c88464d..fa3b4bf 100644 --- a/example/src/App.js +++ b/example/src/App.js @@ -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 (