From 2eec73faa32bfceb7a72cc4cd75055ab03c7e8bd Mon Sep 17 00:00:00 2001 From: Justin Murray Date: Tue, 4 Aug 2026 13:57:02 -0400 Subject: [PATCH] fix(example): mount monaco directly instead of react-monaco-editor react-monaco-editor imports `monaco-editor/esm/vs/editor/editor.api`, but monaco-editor 0.56 added an `exports` map ("./*": "./esm/vs/*.js") that rewrites that specifier to ./esm/vs/esm/vs/editor/editor.api.js, which the package does not ship. This broke `yarn build` in ./example, failing the publish workflow's GitHub Pages deploy. Its latest release (0.59.0) still peer-depends on monaco-editor ^0.52.0 and has not adapted. Drop the wrapper and create the editor with monaco.editor.create, importing through the supported `monaco-editor/editor/editor.api` specifier. Switching dialects now uses setModelLanguage so the editor's contents are preserved instead of being remounted. Also add an `example` job to CI. The example consumes the package via `file:../`, so it is the only check that exercises the built entry points and the monaco-editor peer dependency as a real consumer would; previously a break there was only discovered mid-release. react-scripts runs eslint during a production build, so the job covers lint and compilation. Remove the unreferenced CRA boilerplate App.css. --- .github/workflows/ci.yml | 27 +++++++++ example/README.md | 9 ++- example/package.json | 1 - example/src/App.css | 38 ------------ example/src/App.js | 128 ++++++++++++++++++++++++++------------- example/src/index.js | 11 ++++ example/yarn.lock | 5 -- 7 files changed, 131 insertions(+), 88 deletions(-) delete mode 100644 example/src/App.css 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 (

@popsql/monaco-sql-languages

-
- setSelectedLanguageId(event.target.value)} + > + {sortedLanguageIds.map((languageId) => ( + ))}
- setCode(newValue)} +
); diff --git a/example/src/index.js b/example/src/index.js index 16b2ec7..b5e8db1 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -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(); diff --git a/example/yarn.lock b/example/yarn.lock index ce4d776..97a327c 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -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"