You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 56a2346
Browse filesBrowse the repository at this point in the historyBrowse files
fix(plugin-editor): serve Monaco from the Console's own build instead of cdn.jsdelivr.net (#25993)
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9Ag3f4YQR9fBKKRFtXe8QcZ)_Fixes#25899
Clause-②: no
Metadata-protocol: none
## What changed
The Console's code editors loaded Monaco's core from `cdn.jsdelivr.net`
at run time, because nothing in `ui/` ever called `loader.config` and
`@monaco-editor/loader` 1.7.0 defaults `paths.vs` to the jsdelivr
`monaco-editor@0.55.1` AMD build. Under the Console's CSP (`script-src
'self' 'nonce-…'`, no CDN, no `worker-src`) that script is refused, so
every surface fell back to a textarea and the `code-editor` page
component stayed on "Loading..." for good.
- **`@object-ui/plugin-editor`**
- `monaco-editor@^0.55.1` is now a runtime dependency (the maintainer
approved it on 2026-10-25, option A, self-host).
- New `bundledMonaco.ts` with `configureBundledMonaco(loader)`. It
imports `monaco-editor` as a lazy ESM chunk and calls `loader.config({
monaco })` before the first `loader.init()`. A loader that already holds
an instance resolves `init()` with it and injects no script.
- `self.MonacoEnvironment.getWorker` imports each language worker
through Vite's `?worker` the first time Monaco asks for that label
(json, css/scss/less, html/handlebars/razor, typescript/javascript, and
the editor worker for the rest). Each worker starts from a same-origin
file under `assets/`.
- `MonacoImpl` mounts `<Editor>` only once that config has landed. When
the editor chunk cannot be fetched, it edits the same text in a plain
textarea (read-only kept) and warns once, instead of loading forever.
- A failed import clears the cached promise, so the next caller tries
again.
- **`@object-ui/app-shell`**
- `useMonacoFallback`'s one loader probe per page now awaits
`configureBundledMonaco` (through a dynamic
`import('@object-ui/plugin-editor')`, because the probe sits in the
eager closure) before `loader.init()`.
- The JSON source tab, the html / react page source editor and the
metadata form's `CodeWidget` all mount through that probe, so all of
them get the bundled Monaco. The plain-textarea fallback stays for a
chunk that cannot be fetched, and the report now names the chunk URL the
browser could not fetch.
- Comments that said "CDN" now describe the lazy chunk.
- No change to `ui/apps/console/vite.config.ts`: Vite's default
`?worker` output already emits same-origin worker files, so no worker
output config was needed.
## Bundle size (console `vite build`, before = `d5f4c2a2`, after = this
branch)
| Measurement | Before | After |
|---|---|---|
| `du -sh dist/assets` | 54M (45,554,410 B) | 73M (65,018,497 B) |
| All JS, raw | 30,547,408 B | 44,096,511 B (+13,549,103) |
| First-paint closure (`eager-closure.json`) | 85 chunks, 6,739,969 B
raw / 1,976,242 B gz | 85 chunks, 6,740,072 B raw (+103) / 1,976,272 B
gz (+30) |
| Entry chunk | 310,652 B | 310,652 B (unchanged) |
No Monaco, editor or worker chunk is in the first-paint closure. The
added bytes are lazy files loaded only on a screen that opens an editor:
| File | Raw | Gzip |
|---|---|---|
| editor.api2 (Monaco core) | 3,622,381 B | 913,125 B |
| ts.worker | 6,904,380 B | 1,477,433 B |
| css.worker | 1,054,840 B | 238,629 B |
| html.worker | about 720 KB | |
| json.worker | about 410 KB | |
| editor.worker | 280,410 B | 85,786 B |
`codicon.ttf` is also added.
## Browser smoke under the Console CSP
Setup:
- `os` showcase backend serving this branch's console build, with
`OS_PAGE_REACT_ENABLED=false`.
- The response carried the strict CSP: no `unsafe-eval`, no CDN in
`script-src`, no `worker-src`.
- Playwright Chromium; 827 requests in total.
| Surface | Route | Result |
|---|---|---|
| `CodeWidget` (metadata form code field) | hook
`showcase_normalize_task_title` | Monaco mounted |
| `JsonSourceEditor` (Source tab) | object `showcase_task` | Monaco, 34
lines rendered |
| `SourcePageEditor` | html page `showcase_start_here` | 2 Monaco
editors, 54 lines |
- **CSP:** 0 `securitypolicyviolation` events (script-src or
worker-src).
- **CDN:** 0 requests to `cdn.jsdelivr.net`.
- **Failures:** 0 failed requests.
- **Workers:** `ts.worker-*.js` and `editor.worker-*.js` loaded from
`/_console/assets/` (same origin).
- **Not separately smoked:** the `code-editor` page component. It
renders the same `MonacoImpl` that the tests below cover.
## Tests
**New tests in `ui/packages/plugin-editor`:**
- `bundledMonaco.noCdn.test.tsx`. Uses the real `@monaco-editor/loader`
and the real `MonacoImpl`.
- `<Editor>` mounts only after the loader holds the bundled Monaco.
- `loader.init()` answers with it, and no jsdelivr or `vs/loader.js`
script is appended.
- `getWorker` maps each label to its worker module.
- `MonacoImpl.chunkFailure.test.tsx`. Importing `monaco-editor` rejects,
which covers card bullet 3.
- An editable textarea holds the value and `onChange` fires.
- Read-only is kept.
- Exactly one warning.
**Reverse verification**, run with `scripts/ablation-replace.mjs`. Each
mutation landed as an on-disk blob change and was restored to an empty
`git diff HEAD`:
- Dropping `loader.config(...)` from `configureBundledMonaco` turns
noCdn red: "the loader held the bundled Monaco when Editor mounted:
expected false to be true".
- Disabling the `unavailable` branch in `MonacoImpl` turns both
chunkFailure tests red: "Unable to find a label with the text of:
javascript source".
**app-shell suites:**
- 16 existing app-shell suites that mock `@monaco-editor/react` now also
stub `configureBundledMonaco`.
- `useMonacoFallback.loaderRejection-11800` and
`CodeWidget.loaderFallback-11858` now fail the chunk import itself. They
assert that the report names the chunk URL and that no loader script is
ever appended.
**Results at `32c20ded`:**
- Monaco-related set (21 files): 907/907 passed.
- Full `packages/app-shell/` + `packages/plugin-editor/` run from the ui
root: 1724 test files passed, 1 skipped (1725); 15880 tests passed, 9
skipped.
- plugin-editor build and type-check, app-shell type-check: pass.
- eslint on the changed files: 0 errors.
**Gates:** `node scripts/pm/dispatch-gates.mjs --ran` reports 53
derived, 53 run, 0 NOT-MEASURED, 0 UNRUN. `check:dts-closure` and
`check:dual-build-cjs-loads` first stopped on missing build output: the
console `plugin.d.ts`, and seven backend packages with no `dist/`. Both
passed after building those outputs.
## Acceptance notes
- **Outside the claimed file surface.** Three suites under
`ui/packages/app-shell/src/views/studio-design/` got the same one-line
`configureBundledMonaco` stub: `InterfacesPillar.sourcePage-12522`,
`StudioDesignSurface.interfacesCreatePage-11823` and
`StudioDesignSurface.interfacesReadOnly-11136`.
- They render the same Monaco surfaces and broke the same way as the
claimed metadata-admin suites.
- This is a bounded in-place fix of the same defect class, with no new
verification surface.
- **Out of scope: the `ui/apps/site` playground**
(`apps/site/app/playground/page.tsx`) still loads Monaco from the CDN.
- **Stale comment, not edited:** the CSP doc comment in
`packages/cli/src/utils/console.ts`, near
`consoleContentSecurityPolicy`. It still says Monaco loads its core from
jsdelivr and shows its plain-text fallback. That file is outside this
card's surface.
- **jsdelivr string still shipped:** the URL survives in
`@monaco-editor/loader`'s default config inside an eager chunk. It is
never requested once `loader.config({ monaco })` has run, and the smoke
above shows zero requests to it.
- **React page tier:** the smoke ran with the react page tier off. With
it on, the CSP adds only `'unsafe-eval'`, which does not affect where
scripts or workers may load from.
- **Other bundlers:** a host bundling `@object-ui/plugin-editor` with
something other than Vite must resolve the `?worker` imports itself
(noted in the changeset).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_0144tqLAVSHy6hGKUSH4LhVL
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(plugin-editor): the Console's code editor loads Monaco from its own build, never from cdn.jsdelivr.net
8
+
9
+
Clause-②: no
10
+
Metadata-protocol: none
11
+
12
+
The Studio's code editors (the JSON source tab, the html / react page source editor, the metadata form's code fields) and the `code-editor` page component fetched Monaco's core from `cdn.jsdelivr.net` at run time, through `@monaco-editor/loader`'s default config. An install with no route to that CDN, and every Console served under its content-security-policy (which lists no CDN in `script-src`), got a plain textarea instead, and the `code-editor` component stayed on "Loading..." for good.
13
+
14
+
-**`@object-ui/plugin-editor`:**`monaco-editor` is now a dependency, bundled by the host. The new `configureBundledMonaco(loader)` imports its ESM core as a lazy chunk and hands it to the loader with `loader.config({ monaco })` before the first `loader.init()`, so the loader injects no CDN script. Each language worker is imported through Vite's `?worker` when Monaco first asks for it, and starts from a same-origin file under the build's assets. The `code-editor` component mounts the editor only once that config has landed, and when the editor chunk cannot be fetched it edits the same text in a plain textarea instead of loading forever.
15
+
-**`@object-ui/app-shell`:** the code editors' one loader probe per page awaits `configureBundledMonaco` before `loader.init()`. The plain-textarea fallback stays for a chunk that cannot be fetched.
16
+
-**`@objectstack/console`:** the built Console carries Monaco as lazy chunks and worker files under `assets/` (about 13.5 MB of uncompressed JavaScript, loaded only on a screen that opens an editor); the first-paint closure is unchanged.
17
+
18
+
What to do: nothing. A host that bundles `@object-ui/plugin-editor` with a bundler other than Vite must handle the `?worker` imports itself.
0 commit comments