Skip to content

Commit 56a2346

Browse 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>
1 parent 416e96f commit 56a2346

28 files changed

Lines changed: 524 additions & 89 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/plugin-editor': patch
3+
'@object-ui/app-shell': patch
4+
'@objectstack/console': patch
5+
---
6+
7+
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.

‎pnpm-lock.yaml‎

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ui/packages/app-shell/src/views/metadata-admin/CodeWidget.expressionEnvelope-10963.test.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
* The editor mounts only once the page's Monaco loader probe has resolved
4545
* (objectui#11858), so the loader is stubbed to resolve, and the sink paints
4646
* one `.view-line` row as a loaded Monaco does: this file is the
47-
* reachable-CDN page. The failing page is
47+
* reachable-loader page. The failing page is
4848
* `CodeWidget.loaderFallback-11858.test.tsx`.
4949
*/
5050

@@ -55,6 +55,8 @@ import { render, screen, fireEvent, cleanup, act } from '@testing-library/react'
5555
import { FieldSchema, hookForm, objectForm } from '@objectstack/spec/data';
5656

5757
vi.mock('@object-ui/plugin-editor', () => ({
58+
// The bundled Monaco loads: the probe goes on to `loader.init()`.
59+
configureBundledMonaco: () => Promise.resolve(),
5860
CodeEditorRenderer: ({
5961
value,
6062
onChange,
@@ -76,7 +78,7 @@ vi.mock('@object-ui/plugin-editor', () => ({
7678
),
7779
}));
7880

79-
// The CDN is reachable: the page's one `loader.init()` resolves.
81+
// The loader is reachable: the page's one `loader.init()` resolves.
8082
vi.mock('@monaco-editor/react', () => {
8183
const Editor = () => null;
8284
return { Editor, default: Editor, loader: { init: () => Promise.resolve({}) } };

‎ui/packages/app-shell/src/views/metadata-admin/CodeWidget.loaderFallback-11858.test.tsx‎

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* objectui#11858 — when Monaco's CDN loader script cannot be fetched (no
5-
* egress, a strict CSP), SchemaForm's `code` widget renders an editable
6-
* textarea that saves, the failure is reported once per page, and nothing
7-
* escapes as an uncaught error.
4+
* objectui#11858 — when Monaco cannot be loaded (its bundled chunk cannot be
5+
* fetched), SchemaForm's `code` widget renders an editable textarea that
6+
* saves, the failure is reported once per page, and nothing escapes as an
7+
* uncaught error.
88
*
99
* ## The defect this file keeps fixed
1010
*
@@ -28,18 +28,18 @@
2828
* formula's ADR-0089 envelope parsed by the spec: the envelope write the
2929
* editor makes (objectui#10963) is the one the textarea makes.
3030
*
31-
* ## Nothing about Monaco is mocked
31+
* ## Only the chunk is stood in for
3232
*
3333
* As in `useMonacoFallback.loaderRejection-11800.test.tsx`, this drives the REAL
34-
* `@monaco-editor/loader` and the real plugin-editor `<Editor>`: the leak lives
35-
* in the loader, and a stubbed `loader.init` would hand back a promise the
36-
* caller CAN catch. Two pieces of browser behaviour are modelled, and only
37-
* these two, the same way that file models them:
34+
* `@monaco-editor/loader` and the real plugin-editor `<Editor>`; only
35+
* plugin-editor's `configureBundledMonaco`, which hands the bundled Monaco to
36+
* the loader before the page's one `loader.init()`, stands for the chunk. Two
37+
* pieces of browser behaviour are modelled, and only these two, the same way
38+
* that file models them:
3839
*
39-
* - **The network.** The loader appends a `vs/loader.js` script and waits for
40-
* its `error` event. The script is held back from the DOM and its `error`
41-
* event fired by the test, after both lazy editor modules have loaded, so
42-
* an editor that mounted before the loader answered would have mounted.
40+
* - **The network.** `configureBundledMonaco` waits until the test lets the
41+
* chunk fail, after both lazy editor modules have loaded, so an editor that
42+
* mounted before the chunk answered would have mounted.
4343
* - **Unhandled rejections.** Each Node report is replayed as a cancelable
4444
* `unhandledrejection` event on `window`, and the ones left uncancelled are
4545
* counted. Vitest's own listeners are put back afterwards.
@@ -69,6 +69,26 @@ import { CodeWidget } from './widgets';
6969
import type { FormFieldSpec, FormViewSpec } from './form-spec';
7070

7171
const LOADER_SCRIPT = /\/vs\/loader\.js$/;
72+
const CHUNK_URL = 'http://localhost/_console/assets/editor.api2-hash.js';
73+
74+
const chunk = vi.hoisted(() => {
75+
let fail!: () => void;
76+
const answered = new Promise<void>((resolve) => {
77+
fail = resolve;
78+
});
79+
return { answered, fail: () => fail() };
80+
});
81+
82+
// The bundled Monaco chunk cannot be fetched: plugin-editor's
83+
// `configureBundledMonaco` rejects, as its import of the chunk does, once the
84+
// test says the network answered. The rest of plugin-editor is real.
85+
vi.mock('@object-ui/plugin-editor', async (importOriginal) => ({
86+
...(await importOriginal<typeof import('@object-ui/plugin-editor')>()),
87+
configureBundledMonaco: async () => {
88+
await chunk.answered;
89+
throw new TypeError(`Failed to fetch dynamically imported module: ${CHUNK_URL}`);
90+
},
91+
}));
7292

7393
let uncaught: unknown[] = [];
7494
let vitestListeners: NodeJS.UnhandledRejectionListener[] = [];
@@ -152,7 +172,7 @@ const STORED_EXPRESSION = FieldSchema.parse({
152172
expression: { dialect: 'cel', source: 'record.amount * 2', meta: META },
153173
}).expression;
154174

155-
describe('CodeWidget — a failing Monaco loader falls back to an editable textarea (objectui#11858)', () => {
175+
describe('CodeWidget — a Monaco chunk that cannot be fetched falls back to an editable textarea (objectui#11858)', () => {
156176
it('the rows are what they claim: spec code rows, and a spec-parsed envelope', () => {
157177
// Guards the premises, so a spec that renames a row or changes its type
158178
// fails here instead of turning the pins below into tests of nothing.
@@ -185,15 +205,13 @@ describe('CodeWidget — a failing Monaco loader falls back to an editable texta
185205
);
186206
await settle();
187207

188-
expect(heldLoaderScripts, 'the loader appends its script once per page').toHaveLength(1);
189-
const [loaderScript] = heldLoaderScripts;
190208
// Before the network answers, no Monaco editor is mounted: it would show
191209
// its own "Loading..." and call `loader.init()` itself.
192-
expect.soft(screen.queryAllByText('Loading...'), 'Monaco editors mounted before the loader answered').toHaveLength(0);
210+
expect.soft(screen.queryAllByText('Loading...'), 'Monaco editors mounted before the chunk answered').toHaveLength(0);
193211

194-
// The network answers: the loader script failed to load.
195-
act(() => {
196-
loaderScript.dispatchEvent(new Event('error'));
212+
// The network answers: the chunk could not be fetched.
213+
await act(async () => {
214+
chunk.fail();
197215
});
198216

199217
const hookBody = (await screen.findByRole('textbox', { name: /^source/i }, { timeout: 2000 })) as HTMLTextAreaElement;
@@ -216,14 +234,15 @@ describe('CodeWidget — a failing Monaco loader falls back to an editable texta
216234

217235
await settle();
218236
// Soft, so a regression reports every reading at once, not just the first.
219-
expect.soft(uncaught, 'uncaught rejections after the loader failed').toHaveLength(0);
237+
expect.soft(uncaught, 'uncaught rejections after the chunk failed').toHaveLength(0);
220238
const editorInitErrors = consoleError.mock.calls.filter(
221239
([first]) => typeof first === 'string' && first.startsWith('Monaco initialization'),
222240
);
223241
expect.soft(editorInitErrors, "<Editor>'s own init error lines").toHaveLength(0);
224242
const reports = monacoReports(consoleWarn.mock.calls);
225243
expect.soft(reports, 'console lines reporting the Monaco failure').toHaveLength(1);
226-
expect.soft(reports.join('\n'), 'the report names the loader script URL').toContain(loaderScript.src);
244+
expect.soft(reports.join('\n'), 'the report names the chunk URL').toContain(CHUNK_URL);
245+
expect.soft(heldLoaderScripts, "loader scripts appended (the loader's CDN default)").toHaveLength(0);
227246
expect.soft(screen.queryAllByText('Loading...'), 'Monaco editors mounted after the failure').toHaveLength(0);
228247

229248
// Reopen: a code row mounted after the failure is known renders the
@@ -248,7 +267,7 @@ describe('CodeWidget — a failing Monaco loader falls back to an editable texta
248267
await settle();
249268

250269
expect.soft(uncaught, 'uncaught rejections after a reopen').toHaveLength(0);
251-
expect.soft(heldLoaderScripts, 'loader scripts after a reopen').toHaveLength(1);
270+
expect.soft(heldLoaderScripts, 'loader scripts after a reopen').toHaveLength(0);
252271
expect.soft(monacoReports(consoleWarn.mock.calls), 'reports after a reopen').toHaveLength(1);
253272
});
254273
});

‎ui/packages/app-shell/src/views/metadata-admin/JsonSourceEditor.fallback.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ vi.mock('@monaco-editor/react', () => {
1919
return { Editor, default: Editor, loader: { init: () => Promise.resolve({}) } };
2020
});
2121

22+
// The bundled Monaco loads: the page's probe awaits plugin-editor's
23+
// `configureBundledMonaco` before `loader.init()` (its own suite drives it).
24+
vi.mock('@object-ui/plugin-editor', () => ({ configureBundledMonaco: () => Promise.resolve() }));
25+
2226
import { JsonSourceEditor } from './JsonSourceEditor';
2327

2428
describe('JsonSourceEditor — F5 textarea fallback', () => {

‎ui/packages/app-shell/src/views/metadata-admin/JsonSourceEditor.monaco-export.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ vi.mock('@monaco-editor/react', () => {
4747
};
4848
});
4949

50+
// The bundled Monaco loads: the page's probe awaits plugin-editor's
51+
// `configureBundledMonaco` before `loader.init()` (its own suite drives it).
52+
vi.mock('@object-ui/plugin-editor', () => ({ configureBundledMonaco: () => Promise.resolve() }));
53+
5054
import { JsonSourceEditor } from './JsonSourceEditor';
5155

5256
describe('JsonSourceEditor — lazy Monaco export shape', () => {

‎ui/packages/app-shell/src/views/metadata-admin/JsonSourceEditor.tsx‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,12 @@ export function JsonSourceEditor({
9999
const editorRef = React.useRef<any>(null);
100100
const monacoRef = React.useRef<any>(null);
101101

102-
// Monaco's core is fetched lazily and, by default, from a public CDN, and it
103-
// also spins up web workers. When any of that is blocked — offline /
104-
// air-gapped / CSP-restricted installs — the editor mounts an empty shell
105-
// with no error and the Source tab looks blank. `useMonacoFallback` fast-fails
106-
// to a plain textarea the moment the CDN loader rejects, and also backstops
102+
// Monaco's core is a lazy chunk of the host's own build (plugin-editor's
103+
// `configureBundledMonaco`, never a CDN), and it also spins up web workers.
104+
// When either is blocked — a chunk that cannot be fetched, a worker refused —
105+
// the editor mounts an empty shell with no error and the Source tab looks
106+
// blank. `useMonacoFallback` fast-fails to a plain textarea the moment the
107+
// chunk or the loader rejects, and also backstops
107108
// the "resolved but painted nothing" case via a `.view-line` DOM poll, so the
108109
// source is always readable and editable. The editor mounts only once the
109110
// loader has resolved: mounted earlier, its own `loader.init()` on a failing

‎ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.nameSurvivesLabel-12680.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ vi.mock('@monaco-editor/react', () => {
8282
const Editor = () => null;
8383
return { Editor, default: Editor, loader: { init: () => Promise.reject(new Error('offline')) } };
8484
});
85+
86+
// The bundled Monaco loads: the page's probe awaits plugin-editor's
87+
// `configureBundledMonaco` before `loader.init()` (its own suite drives it).
88+
vi.mock('@object-ui/plugin-editor', () => ({ configureBundledMonaco: () => Promise.resolve() }));
8589
// The page's own side reads (the AI agents roster and the like) go through the
8690
// global `fetch`; one double answers "absent" for the whole file, so no read
8791
// can reach a socket after a test body returns.

‎ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,10 @@ vi.mock('@monaco-editor/react', () => {
208208
return { Editor, default: Editor, loader: { init: () => Promise.reject(new Error('offline')) } };
209209
});
210210

211+
// The bundled Monaco loads: the page's probe awaits plugin-editor's
212+
// `configureBundledMonaco` before `loader.init()` (its own suite drives it).
213+
vi.mock('@object-ui/plugin-editor', () => ({ configureBundledMonaco: () => Promise.resolve() }));
214+
211215
// The `page` resource config under test is registered here, at load, exactly
212216
// as the package entry registers it.
213217
import '../../services/builtinComponents.js';

‎ui/packages/app-shell/src/views/metadata-admin/SchemaForm.nonRegistryLabelling.test.tsx‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ vi.mock('@monaco-editor/react', () => {
7777
const Editor = () => null;
7878
return { Editor, default: Editor, loader: { init: () => Promise.reject(new Error('offline')) } };
7979
});
80+
81+
// The bundled Monaco loads: the page's probe awaits plugin-editor's
82+
// `configureBundledMonaco` before `loader.init()` (its own suite drives it).
83+
vi.mock('@object-ui/plugin-editor', () => ({ configureBundledMonaco: () => Promise.resolve() }));
8084
import {
8185
SchemaForm,
8286
resolveFieldFace,

0 commit comments

Comments
 (0)