Skip to content

Commit be64c51

Browse files
committed
fix(FR-2606): restore Monaco self-hosting in Vite dev server (#7104)
Resolves #6809(FR-2606) ## Summary Dev-server regression noticed after the Vite cutover: pages that mount a Monaco editor break at mount time because the `/resources/monaco/vs/*` URL prefix no longer resolves on the dev server. `@monaco-editor/react` loads the Monaco AMD runtime at runtime via: ```ts loader.config({ paths: { vs: '/resources/monaco/vs' } }); ``` (see `react/src/helper/monacoEditor.ts`). The self-hosted prefix exists so the editor works in offline / air-gapped deployments where jsDelivr is unreachable. Pre-Vite, the dev-server side of this contract was the `static` directory list in `react/craco.config.cjs` (lines 44–60 of the now-deleted file): it served `react/node_modules/monaco-editor/min/vs/*` at the `/resources/monaco/vs` URL prefix. Production builds copy the same tree to `build/web/resources/monaco/vs/` via the root `copymonaco` script (`package.json:32`). After #6876 (FR-2611) dropped `craco.config.cjs`, the production `copymonaco` step was preserved but the dev-server-side mapping was not ported to `vite.config.ts`. As a result, in `pnpm run dev`, requests to `/resources/monaco/vs/loader.js` 404 and any page that mounts a Monaco editor breaks. **Fix**: add `monacoStaticPlugin()` to `react/vite.config.ts` mirroring the deleted craco `static` entry. - Serves `/resources/monaco/vs/*` from `react/node_modules/monaco-editor/min/vs/*`. - `apply: 'serve'` — dev-only; production is unchanged because `copymonaco` already populates `build/web/resources/monaco/vs/`. - Path-traversal guard via `filePath.startsWith(monacoRoot)` after `join` normalization. - Registered in the `plugins` array before `projectRootStaticPlugin()` so the narrower Monaco prefix is matched first. ## Verification - [x] `bash scripts/verify.sh` — Relay / Lint / Format / TypeScript all PASS - [ ] `pnpm run dev`, navigate to a Monaco-mounting page (manifest editor on Environments page) — editor renders without console errors, no `cdn.jsdelivr.net` fetches - [ ] DevTools → Network: `/resources/monaco/vs/loader.js`, `/resources/monaco/vs/editor/editor.main.js` return 200 from the dev server ## Stack Top of the Vite migration follow-up stack. Sits on top of #7083 (FR-2748).
1 parent 9fa887b commit be64c51

1 file changed

Lines changed: 51 additions & 1 deletion

File tree

react/vite.config.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,53 @@ function projectRootStaticPlugin(): Plugin {
231231
};
232232
}
233233

234+
/**
235+
* Serve the Monaco AMD runtime at `/resources/monaco/vs/*` from
236+
* `react/node_modules/monaco-editor/min/vs/*` during dev. Mirrors the
237+
* `static` directory entry that lived in the deleted `react/craco.config.cjs`.
238+
*
239+
* `@monaco-editor/react` resolves the URL prefix via
240+
* loader.config({ paths: { vs: '/resources/monaco/vs' } })
241+
* (see `react/src/helper/monacoEditor.ts`). Self-hosting keeps Monaco
242+
* working in offline / air-gapped deployments where jsDelivr is unreachable.
243+
*
244+
* Production is unchanged: the root `copymonaco` script (`package.json:32`)
245+
* copies the same tree into `build/web/resources/monaco/vs/`.
246+
*
247+
* `min/vs` is Monaco's prebuilt AMD bundle — used here rather than the ESM
248+
* tree because `@monaco-editor/react` consumes the AMD form to keep the
249+
* Monaco worker chunks intact for runtime lazy-loading.
250+
*/
251+
function monacoStaticPlugin(): Plugin {
252+
const monacoRoot = resolve(__dirname, 'node_modules/monaco-editor/min/vs');
253+
const URL_PREFIX = '/resources/monaco/vs/';
254+
255+
return {
256+
name: 'bai-monaco-static',
257+
apply: 'serve',
258+
configureServer(server) {
259+
server.middlewares.use((req, res, next) => {
260+
if (!req.url) return next();
261+
const url = req.url.split('?')[0];
262+
if (!url.startsWith(URL_PREFIX)) return next();
263+
264+
const rel = url.slice(URL_PREFIX.length);
265+
const filePath = join(monacoRoot, rel);
266+
// Path-traversal guard: `join` normalizes `..`, so prefix comparison
267+
// catches any URL that escapes `monacoRoot`.
268+
if (!filePath.startsWith(monacoRoot)) return next();
269+
if (!existsSync(filePath) || !statSync(filePath).isFile()) {
270+
return next();
271+
}
272+
const mime =
273+
MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
274+
res.setHeader('Content-Type', mime);
275+
createReadStream(filePath).pipe(res);
276+
});
277+
},
278+
};
279+
}
280+
234281
export default defineConfig(({ command, mode }) => {
235282
const env = loadEnv(mode, projectRoot, '');
236283
Object.assign(process.env, env);
@@ -405,7 +452,10 @@ export default defineConfig(({ command, mode }) => {
405452

406453
plugins: [
407454
// Must run before @vitejs/plugin-react so we own the HTML transform
408-
// and the index.html resolution.
455+
// and the index.html resolution. Monaco's narrower prefix is matched
456+
// first so the more general projectRoot middleware never has to
457+
// reach into the filesystem for a `/resources/monaco/vs/*` request.
458+
monacoStaticPlugin(),
409459
projectRootStaticPlugin(),
410460
devAssetsReloadPlugin(),
411461

0 commit comments

Comments
 (0)