Skip to content

fix(plugin-editor): serve Monaco from the Console's own build instead of cdn.jsdelivr.net - #25993

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-d433a3
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-d433a3

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

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.ai/code/session_0144tqLAVSHy6hGKUSH4LhVL

…elivr

WIP: shared bundledMonaco loader (ESM core + ?worker language workers),
MonacoImpl textarea fallback, app-shell probe configures before init.

Claude-Session: https://claude.ai/code/session_0144tqLAVSHy6hGKUSH4LhVL
Co-authored-by: Claude <noreply@anthropic.com>
The bundled Monaco loader imports only the core up front; each language
worker is imported when Monaco first asks for it. MonacoImpl gets its
chunk-failure and no-CDN tests; the app-shell Monaco tests stub
configureBundledMonaco, and the two real-loader tests now model a chunk
that cannot be fetched instead of a CDN script.

Claude-Session: https://claude.ai/code/session_0144tqLAVSHy6hGKUSH4LhVL
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0144tqLAVSHy6hGKUSH4LhVL
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-26T08:39Z

Head 32c20dedd8e0 (read from the PR object).

  • Shape. Draft on main; Fixes #25899; Clause-②: no, Metadata-protocol: none; changeset .changeset/25899-monaco-self-host.md in the root .changeset/ (patch, plugin-editor / app-shell / console).
  • Checks pinned to the head. 62 named check runs, latest per name: 57 success, 5 skipped, 0 failing, 0 pending. Lint & Repo Gates, TypeScript Type Check, Test UI 16/16, Type Check UI, Build & E2E UI all success.
  • Governed surface. check-governed-merges.mjs --pr 25993: 0 of 28 paths governed, +524/-89.
  • New runtime dependency. monaco-editor@^0.55.1 in @object-ui/plugin-editor (lockfile +3, through pnpm install). The maintainer already made this call on 2026-10-25 (option A, self-host), as triage records on the card (comment 6409510137). The version is the one @monaco-editor/loader 1.7.0 was fetching from the CDN, so the editor itself does not change.
  • Surface. Within the claim, plus the three studio-design test stubs on the claim amendment and the other metadata-admin test files that mock the same probe (a one-line stub each, named in the PR body).
  • Changeset against the diff. configureBundledMonaco(loader) imports the ESM core lazily and calls loader.config({ monaco }) before the first init(); workers come from ?worker on first request; a failed import resets and the callers fall back to a textarea; useMonacoFallback awaits the config before probing. Each sentence matches the code. The cost is +13.5 MB of lazy JS under assets/, and the first-paint closure is unchanged (+103 B, measured by the dev).
  • Out-of-scope notes. The stale jsdelivr comment in packages/cli/src/utils/console.ts and the ui/apps/site playground still on the CDN go to Acceptance notes. Neither is a defect a user meets in the Console.

Landing: ready + auto-merge through the relay.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 08:43
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 08:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 56a2346 Oct 26, 2026
63 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-d433a3 branch October 26, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console code editor: serve Monaco from the Console's own assets instead of a public CDN

2 participants