@checkstack/ui 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +145 -0
- package/package.json +20 -15
- package/src/components/Accordion.tsx +17 -9
- package/src/components/ActionCard.tsx +4 -4
- package/src/components/BrandIcon.tsx +57 -0
- package/src/components/CodeEditor/CodeEditor.tsx +71 -7
- package/src/components/CodeEditor/TypefoxEditor.tsx +266 -53
- package/src/components/CodeEditor/editorTheme.test.ts +41 -0
- package/src/components/CodeEditor/editorTheme.ts +26 -0
- package/src/components/CodeEditor/index.ts +3 -1
- package/src/components/CodeEditor/monacoGuard.ts +76 -0
- package/src/components/CodeEditor/monacoTsService.ts +5 -37
- package/src/components/CodeEditor/scriptContext.test.ts +15 -7
- package/src/components/CodeEditor/scriptContext.ts +12 -18
- package/src/components/CodeEditor/types.ts +20 -0
- package/src/components/CodeEditor/validateScripts.ts +53 -13
- package/src/components/CodeEditor/vscodeServicesSignal.ts +72 -0
- package/src/components/ConfirmationModal.tsx +7 -1
- package/src/components/DynamicForm/DynamicForm.tsx +101 -53
- package/src/components/DynamicForm/DynamicOptionsField.tsx +19 -14
- package/src/components/DynamicForm/FormField.tsx +84 -24
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +11 -0
- package/src/components/DynamicForm/index.ts +14 -0
- package/src/components/DynamicForm/types.ts +63 -1
- package/src/components/DynamicForm/utils.test.ts +38 -0
- package/src/components/DynamicForm/utils.ts +22 -0
- package/src/components/DynamicForm/validation.logic.test.ts +255 -0
- package/src/components/DynamicForm/validation.logic.ts +210 -0
- package/src/components/DynamicIcon.tsx +39 -17
- package/src/components/Markdown.tsx +68 -2
- package/src/components/Spinner.tsx +56 -0
- package/src/components/StatusBadge.tsx +78 -0
- package/src/components/StrategyConfigCard.tsx +3 -3
- package/src/components/Tabs.tsx +7 -1
- package/src/components/UserMenu.logic.test.ts +37 -0
- package/src/components/UserMenu.logic.ts +30 -0
- package/src/components/UserMenu.tsx +40 -12
- package/src/components/iconRegistry.tsx +27 -0
- package/src/index.ts +3 -0
- package/stories/Introduction.mdx +1 -1
- package/stories/Markdown.stories.tsx +56 -0
- package/stories/Spinner.stories.tsx +90 -0
- package/tsconfig.json +3 -0
|
@@ -26,8 +26,14 @@ import {
|
|
|
26
26
|
typescriptDefaults,
|
|
27
27
|
javascriptDefaults,
|
|
28
28
|
ensureStandaloneWorkerFactory,
|
|
29
|
-
markVscodeServicesReady,
|
|
30
29
|
} from "./monacoTsService";
|
|
30
|
+
import {
|
|
31
|
+
areVscodeServicesReady,
|
|
32
|
+
claimColdInit,
|
|
33
|
+
markVscodeServicesReady,
|
|
34
|
+
onVscodeServicesReady,
|
|
35
|
+
releaseColdInit,
|
|
36
|
+
} from "./vscodeServicesSignal";
|
|
31
37
|
// Named import also triggers the side-effect registration of the REAL VS Code
|
|
32
38
|
// JSON language service (proper highlighting + completion + folding), replacing
|
|
33
39
|
// the hand-rolled `json-template` Monarch grammar. We turn its built-in
|
|
@@ -41,8 +47,24 @@ import { jsonDefaults } from "@codingame/monaco-vscode-standalone-json-language-
|
|
|
41
47
|
import getLanguagesServiceOverride from "@codingame/monaco-vscode-languages-service-override";
|
|
42
48
|
|
|
43
49
|
|
|
44
|
-
import {
|
|
45
|
-
|
|
50
|
+
import {
|
|
51
|
+
type CSSProperties,
|
|
52
|
+
useEffect,
|
|
53
|
+
useId,
|
|
54
|
+
useLayoutEffect,
|
|
55
|
+
useRef,
|
|
56
|
+
useState,
|
|
57
|
+
} from "react";
|
|
58
|
+
// Types come from the package directly (type-only, so it pulls no runtime code
|
|
59
|
+
// and the lint rule allows it). RUNTIME monaco access goes through the guarded
|
|
60
|
+
// accessor (`monacoRuntime`, see monacoGuard.ts): in dev it throws if a
|
|
61
|
+
// `monaco.editor.*` / `monaco.languages.*` function runs before the services
|
|
62
|
+
// are initialized, preventing the "Services are already initialized" regression
|
|
63
|
+
// class. `no-restricted-imports` forbids importing the raw editor-api value.
|
|
64
|
+
import type * as monaco from "@codingame/monaco-vscode-editor-api";
|
|
65
|
+
import { monaco as monacoRuntime } from "./monacoGuard";
|
|
66
|
+
import { useTheme } from "../ThemeProvider";
|
|
67
|
+
import { MONACO_THEME_MAP, VARIABLE_TOKEN_COLOR } from "./editorTheme";
|
|
46
68
|
import { MonacoEditorReactComp } from "@typefox/monaco-editor-react";
|
|
47
69
|
import { extractBracketKeyGroups } from "./bracketKeyGroups";
|
|
48
70
|
import { validateJsonTemplate } from "./validateJsonTemplate";
|
|
@@ -129,6 +151,44 @@ const registerAcquiredFiles = (
|
|
|
129
151
|
}
|
|
130
152
|
};
|
|
131
153
|
|
|
154
|
+
// ─── @checkstack/sdk editor-type injection ──────────────────────────────────
|
|
155
|
+
//
|
|
156
|
+
// The running release's SDK editor bundle (ambient `.d.ts` for the script
|
|
157
|
+
// helpers + typed client) is mounted ONCE into the shared TS/JS services,
|
|
158
|
+
// keyed by release version. A deployment upgrade changes the key, so the libs
|
|
159
|
+
// reset and the editor never serves stale SDK types (plan §6.2).
|
|
160
|
+
const sdkMountedPaths = new Set<string>();
|
|
161
|
+
let currentSdkResetKey: string | undefined;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Mount the SDK bundle files, resetting on a release-version change. addExtraLib
|
|
165
|
+
* overwrites by path, so a version bump re-mounts the same virtual paths with
|
|
166
|
+
* fresh content; the mounted-path set just dedupes within a version so two
|
|
167
|
+
* editors don't double-register.
|
|
168
|
+
*/
|
|
169
|
+
const mountSdkTypes = ({
|
|
170
|
+
files,
|
|
171
|
+
resetKey,
|
|
172
|
+
}: {
|
|
173
|
+
files: ReadonlyArray<{ path: string; content: string }>;
|
|
174
|
+
resetKey: string | undefined;
|
|
175
|
+
}): void => {
|
|
176
|
+
if (resetKey !== currentSdkResetKey) {
|
|
177
|
+
currentSdkResetKey = resetKey;
|
|
178
|
+
sdkMountedPaths.clear();
|
|
179
|
+
}
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
const uri = `file:///${file.path}`;
|
|
182
|
+
// On a fresh version we re-mount (overwrite) even if the path was seen
|
|
183
|
+
// under a prior key; within a version, skip an already-mounted path.
|
|
184
|
+
if (sdkMountedPaths.has(uri)) continue;
|
|
185
|
+
sdkMountedPaths.add(uri);
|
|
186
|
+
for (const defaults of [typescriptDefaults, javascriptDefaults]) {
|
|
187
|
+
defaults.addExtraLib(file.content, uri);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
132
192
|
/**
|
|
133
193
|
* Acquire types for every NEW bare specifier in `source`, against the given
|
|
134
194
|
* resolver. Pure planning (`parseBareImportSpecifiers` / `planAcquisitions`)
|
|
@@ -212,20 +272,31 @@ const TEMPLATE_VALIDATORS: Partial<
|
|
|
212
272
|
// Variable-like tokens (`{{ template }}` expressions, shell `$env` refs) are
|
|
213
273
|
// highlighted via inline decorations rather than per-language grammars: this
|
|
214
274
|
// works for any language (yaml / xml / markdown have no template grammar; shell
|
|
215
|
-
// doesn't color `$VAR` inside strings) and keeps the color consistent.
|
|
216
|
-
// is
|
|
217
|
-
//
|
|
275
|
+
// doesn't color `$VAR` inside strings) and keeps the color consistent.
|
|
276
|
+
// The style element is injected once and updated whenever the resolved theme
|
|
277
|
+
// changes so the decoration tracks the editor theme.
|
|
218
278
|
const VARIABLE_TOKEN_CLASS = "checkstack-editor-variable";
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
279
|
+
const VARIABLE_TOKEN_STYLE_ID = "checkstack-editor-variable-style";
|
|
280
|
+
|
|
281
|
+
const ensureVariableTokenStyle = ({
|
|
282
|
+
resolvedTheme,
|
|
283
|
+
}: {
|
|
284
|
+
resolvedTheme: "light" | "dark";
|
|
285
|
+
}): void => {
|
|
286
|
+
const color = VARIABLE_TOKEN_COLOR[resolvedTheme];
|
|
287
|
+
const existing = document.querySelector<HTMLStyleElement>(
|
|
288
|
+
`#${VARIABLE_TOKEN_STYLE_ID}`,
|
|
289
|
+
);
|
|
290
|
+
if (existing !== null) {
|
|
291
|
+
// Update in place so a theme toggle refreshes the color.
|
|
292
|
+
existing.textContent = `.${VARIABLE_TOKEN_CLASS}{color:${color} !important;}`;
|
|
222
293
|
return;
|
|
223
294
|
}
|
|
224
|
-
variableTokenStyleInjected = true;
|
|
225
295
|
const style = document.createElement("style");
|
|
296
|
+
style.id = VARIABLE_TOKEN_STYLE_ID;
|
|
226
297
|
// `!important` so the decoration color always wins over the underlying token
|
|
227
298
|
// color (.mtkN), making `{{ }}` look identical inside and outside strings.
|
|
228
|
-
style.textContent = `.${VARIABLE_TOKEN_CLASS}{color
|
|
299
|
+
style.textContent = `.${VARIABLE_TOKEN_CLASS}{color:${color} !important;}`;
|
|
229
300
|
document.head.append(style);
|
|
230
301
|
};
|
|
231
302
|
|
|
@@ -241,7 +312,6 @@ const installRegexDecorations = ({
|
|
|
241
312
|
pattern: RegExp;
|
|
242
313
|
className: string;
|
|
243
314
|
}): monaco.IDisposable => {
|
|
244
|
-
ensureVariableTokenStyle();
|
|
245
315
|
const compute = (): monaco.editor.IModelDeltaDecoration[] => {
|
|
246
316
|
const text = model.getValue();
|
|
247
317
|
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
|
|
@@ -251,7 +321,7 @@ const installRegexDecorations = ({
|
|
|
251
321
|
const start = model.getPositionAt(match.index);
|
|
252
322
|
const end = model.getPositionAt(match.index + match[0].length);
|
|
253
323
|
decorations.push({
|
|
254
|
-
range: new
|
|
324
|
+
range: new monacoRuntime.Range(
|
|
255
325
|
start.lineNumber,
|
|
256
326
|
start.column,
|
|
257
327
|
end.lineNumber,
|
|
@@ -282,7 +352,7 @@ const installRegexDecorations = ({
|
|
|
282
352
|
const findModelById = (
|
|
283
353
|
modelId: string,
|
|
284
354
|
): monaco.editor.ITextModel | undefined =>
|
|
285
|
-
|
|
355
|
+
monacoRuntime.editor
|
|
286
356
|
.getModels()
|
|
287
357
|
.find((candidate) => candidate.uri.toString().includes(modelId));
|
|
288
358
|
|
|
@@ -346,6 +416,20 @@ export type TypefoxEditorProps = {
|
|
|
346
416
|
* resets so types refresh against the new install.
|
|
347
417
|
*/
|
|
348
418
|
acquireResetKey?: string;
|
|
419
|
+
/**
|
|
420
|
+
* The running release's `@checkstack/sdk` editor bundle, as virtual `.d.ts`
|
|
421
|
+
* files to mount (TS/JS editors). Makes `import { defineHealthCheck } from
|
|
422
|
+
* "@checkstack/sdk/healthcheck"` resolve with real, version-matched types.
|
|
423
|
+
* Each file mounts at `file:///<path>` via `addExtraLib`. Fetched live by the
|
|
424
|
+
* consumer (so this component stays network-agnostic + DOM-test-free).
|
|
425
|
+
*/
|
|
426
|
+
sdkTypes?: ReadonlyArray<{ path: string; content: string }>;
|
|
427
|
+
/**
|
|
428
|
+
* Release-version reset key for `sdkTypes`. When it changes, the previously
|
|
429
|
+
* mounted SDK libs are reset so the editor never serves stale SDK types after
|
|
430
|
+
* a deployment upgrade.
|
|
431
|
+
*/
|
|
432
|
+
sdkTypesResetKey?: string;
|
|
349
433
|
/**
|
|
350
434
|
* Importable installed package NAMES (TS/JS editors). When provided, the
|
|
351
435
|
* editor suggests these as completions while the cursor is inside an import
|
|
@@ -355,11 +439,20 @@ export type TypefoxEditorProps = {
|
|
|
355
439
|
* Injected by the consumer so this component stays plugin-agnostic.
|
|
356
440
|
*/
|
|
357
441
|
importablePackages?: string[];
|
|
442
|
+
/**
|
|
443
|
+
* When `true`, this editor never CLAIMS the one-time global cold init - it
|
|
444
|
+
* always waits for another (visible) editor to bring the monaco-vscode
|
|
445
|
+
* services up, then mounts. Set this for OFFSCREEN/hidden editors (the
|
|
446
|
+
* automation `ScriptServicesBooter`): a hidden editor's init may never
|
|
447
|
+
* complete, so it must not be the sole initializer. Defaults to `false`.
|
|
448
|
+
*/
|
|
449
|
+
deferInit?: boolean;
|
|
358
450
|
};
|
|
359
451
|
|
|
360
452
|
/**
|
|
361
453
|
* Isolated editor used to validate the Typefox/monaco-vscode stack in the
|
|
362
|
-
* browser.
|
|
454
|
+
* browser. Theme follows `useTheme().resolvedTheme` (`vs` in light mode,
|
|
455
|
+
* `vs-dark` in dark mode), no minimap, automatic layout, word-based suggestions
|
|
363
456
|
* disabled. For `typescript`/`javascript` it injects the `context` types +
|
|
364
457
|
* bracket completions; for markup/text languages it offers `{{ }}` template
|
|
365
458
|
* completions.
|
|
@@ -381,6 +474,41 @@ const languageStatusServiceOverride: monaco.editor.IEditorOverrideServices =
|
|
|
381
474
|
: {};
|
|
382
475
|
})();
|
|
383
476
|
|
|
477
|
+
// The monaco-vscode global API config. Hoisted to module scope (it has no
|
|
478
|
+
// per-editor state) so it can drive a single, app-lifetime global init below.
|
|
479
|
+
const vscodeApiConfig: MonacoVscodeApiConfig = {
|
|
480
|
+
// 'classic' is the standalone axis (no extension host); 'extended' is the
|
|
481
|
+
// extension-host axis we deliberately avoid in this migration.
|
|
482
|
+
$type: "classic",
|
|
483
|
+
// Register the missing ILanguageStatusService (see the override above) so
|
|
484
|
+
// focusing a JSON editor doesn't throw "addStatus is not supported".
|
|
485
|
+
serviceOverrides: { ...languageStatusServiceOverride },
|
|
486
|
+
// Plain editor, no workbench views.
|
|
487
|
+
viewsConfig: { $type: "EditorService" },
|
|
488
|
+
monacoWorkerFactory: ensureStandaloneWorkerFactory,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
// ─── How the global monaco-vscode init is serialized ────────────────────────
|
|
492
|
+
//
|
|
493
|
+
// `@codingame/monaco-vscode-api`'s global `initialize()` is ONE-SHOT and can
|
|
494
|
+
// never be torn down or re-run. `@typefox/monaco-editor-react` performs that
|
|
495
|
+
// init itself when a `MonacoEditorReactComp` is given a `vscodeApiConfig`, and
|
|
496
|
+
// that is StrictMode-safe FOR A SINGLE EDITOR (upstream tests cover exactly
|
|
497
|
+
// that). The breakage is two editors on one page (e.g. an open script-action
|
|
498
|
+
// editor PLUS the hidden `ScriptServicesBooter`): both wrappers race the init
|
|
499
|
+
// guard (only set inside an async `start()`), both call `initialize()`, the
|
|
500
|
+
// second throws "Services are already initialized", and the global state is
|
|
501
|
+
// corrupted so NO editor starts. StrictMode (dev only) makes the race
|
|
502
|
+
// deterministic via mount -> unmount -> remount; production works because
|
|
503
|
+
// StrictMode is a no-op there.
|
|
504
|
+
//
|
|
505
|
+
// Fix (per the maintainers' single-editor-init guidance): exactly ONE editor
|
|
506
|
+
// claims the cold init and mounts WITH `vscodeApiConfig` (the proven path);
|
|
507
|
+
// every other editor waits for `areVscodeServicesReady()` and only then mounts
|
|
508
|
+
// (it still passes `vscodeApiConfig`, but @typefox no-ops since the services
|
|
509
|
+
// are already initialized). The hidden booter sets `deferInit` so it never
|
|
510
|
+
// claims - the claimer must be a real, visible editor whose init can complete.
|
|
511
|
+
|
|
384
512
|
// Always-available runtime built-in import specifiers (Node + Bun), derived at
|
|
385
513
|
// build time from the bundled stdlib types. These are importable in the script
|
|
386
514
|
// sandbox regardless of the installed-package allowlist (the sandbox is a Bun
|
|
@@ -389,6 +517,13 @@ const languageStatusServiceOverride: monaco.editor.IEditorOverrideServices =
|
|
|
389
517
|
// needs no lazy acquisition. The JSON is a plain `string[]`.
|
|
390
518
|
const BUILTIN_MODULE_SPECIFIERS: readonly string[] = builtinModulesJson;
|
|
391
519
|
|
|
520
|
+
// Passed as the wrapper's `onError` so a wrapper failure is surfaced here
|
|
521
|
+
// instead of becoming an uncaught promise rejection (and so @typefox doesn't
|
|
522
|
+
// reset its internal run-queue lock + re-throw).
|
|
523
|
+
const handleEditorError = (error: Error): void => {
|
|
524
|
+
console.error("[CodeEditor] monaco editor error:", error);
|
|
525
|
+
};
|
|
526
|
+
|
|
392
527
|
export const TypefoxEditor = ({
|
|
393
528
|
id,
|
|
394
529
|
value,
|
|
@@ -404,8 +539,16 @@ export const TypefoxEditor = ({
|
|
|
404
539
|
placeholder,
|
|
405
540
|
acquireTypes,
|
|
406
541
|
acquireResetKey,
|
|
542
|
+
sdkTypes,
|
|
543
|
+
sdkTypesResetKey,
|
|
407
544
|
importablePackages,
|
|
545
|
+
deferInit = false,
|
|
408
546
|
}: TypefoxEditorProps) => {
|
|
547
|
+
// Follow the app's resolved theme so the editor uses `vs` (light) or
|
|
548
|
+
// `vs-dark` (dark) and updates live when the user toggles the theme.
|
|
549
|
+
const { resolvedTheme } = useTheme();
|
|
550
|
+
const monacoTheme = MONACO_THEME_MAP[resolvedTheme];
|
|
551
|
+
|
|
409
552
|
// `MonacoEditorReactComp` captures `onTextChanged` once at editor-start, so
|
|
410
553
|
// the handler it calls would otherwise close over a stale `onChange` (bound
|
|
411
554
|
// to the value/sibling-config at mount time). Routing through a ref that we
|
|
@@ -430,8 +573,62 @@ export const TypefoxEditor = ({
|
|
|
430
573
|
// completion providers below register against a ready languages registry.
|
|
431
574
|
const [apiReady, setApiReady] = useState(false);
|
|
432
575
|
|
|
576
|
+
// Cold-init serialization (see the block comment above). Tracks whether the
|
|
577
|
+
// global services are up yet, and whether THIS editor is the designated
|
|
578
|
+
// initializer (the one that mounts first, with `vscodeApiConfig`).
|
|
579
|
+
const [servicesReady, setServicesReady] = useState(() =>
|
|
580
|
+
areVscodeServicesReady(),
|
|
581
|
+
);
|
|
582
|
+
const [isInitializer, setIsInitializer] = useState(false);
|
|
433
583
|
useEffect(() => {
|
|
434
|
-
if (
|
|
584
|
+
if (servicesReady) return;
|
|
585
|
+
return onVscodeServicesReady(() => setServicesReady(true));
|
|
586
|
+
}, [servicesReady]);
|
|
587
|
+
// Decide the initializer role in a layout effect (NOT during render) so it is
|
|
588
|
+
// StrictMode-safe: StrictMode runs setup -> cleanup -> setup, and the cleanup
|
|
589
|
+
// releases the claim, so exactly one editor ends up the stable holder. A
|
|
590
|
+
// `deferInit` editor (the hidden booter) never claims - the initializer must
|
|
591
|
+
// be a real, visible editor whose @typefox init can actually complete.
|
|
592
|
+
useLayoutEffect(() => {
|
|
593
|
+
if (areVscodeServicesReady() || deferInit) return;
|
|
594
|
+
const claimed = claimColdInit();
|
|
595
|
+
setIsInitializer(claimed);
|
|
596
|
+
return () => {
|
|
597
|
+
if (claimed) releaseColdInit();
|
|
598
|
+
};
|
|
599
|
+
}, [deferInit]);
|
|
600
|
+
|
|
601
|
+
// Mount the wrapper when the services are ready (we attach to them) OR when we
|
|
602
|
+
// are the initializer (we bring them up, passing `vscodeApiConfig`). Until
|
|
603
|
+
// then, a sized, non-animated placeholder so the layout doesn't jump.
|
|
604
|
+
const canMountWrapper = servicesReady || isInitializer;
|
|
605
|
+
|
|
606
|
+
// Keep the Monaco global theme and the variable-token decoration color in
|
|
607
|
+
// sync whenever the resolved app theme changes. Monaco's theme is a global
|
|
608
|
+
// imperative setting (not per-editor), so re-deriving `editorAppConfig`
|
|
609
|
+
// alone is not enough after the editor has started - this effect is what
|
|
610
|
+
// makes live toggling work.
|
|
611
|
+
//
|
|
612
|
+
// GATED on `apiReady`: `monaco.editor.setTheme()` resolves a service via
|
|
613
|
+
// `StandaloneServices.get()`, which AUTO-INITIALIZES the monaco-vscode
|
|
614
|
+
// services if they aren't up yet (CodinGame standaloneServices.js:963). If
|
|
615
|
+
// this ran before the wrapper's own init (e.g. on the deferred booter, which
|
|
616
|
+
// mounts but never gets `apiReady`), it would trip CodinGame's
|
|
617
|
+
// `servicesInitialized` flag and make the wrapper's later `initialize()` throw
|
|
618
|
+
// "Services are already initialized". The initial theme is already applied via
|
|
619
|
+
// `editorAppConfig.editorOptions.theme`; this effect only handles live
|
|
620
|
+
// toggling, which is always after the editor (and thus the API) has started.
|
|
621
|
+
useEffect(() => {
|
|
622
|
+
if (!apiReady) return;
|
|
623
|
+
monacoRuntime.editor.setTheme(monacoTheme);
|
|
624
|
+
ensureVariableTokenStyle({ resolvedTheme });
|
|
625
|
+
}, [apiReady, resolvedTheme, monacoTheme]);
|
|
626
|
+
|
|
627
|
+
useEffect(() => {
|
|
628
|
+
// GATED on `apiReady` for the same reason as the theme effect above:
|
|
629
|
+
// touching `typescriptDefaults` before the wrapper init can auto-initialize
|
|
630
|
+
// the services and collide with the wrapper's `initialize()`.
|
|
631
|
+
if (!apiReady || !isTsLike || typeDefinitions === undefined) {
|
|
435
632
|
return;
|
|
436
633
|
}
|
|
437
634
|
// Inject this editor's ambient `context` types. addExtraLib keys by path
|
|
@@ -447,7 +644,18 @@ export const TypefoxEditor = ({
|
|
|
447
644
|
return () => {
|
|
448
645
|
lib.dispose();
|
|
449
646
|
};
|
|
450
|
-
}, [isTsLike, typeDefinitions, modelId]);
|
|
647
|
+
}, [apiReady, isTsLike, typeDefinitions, modelId]);
|
|
648
|
+
|
|
649
|
+
// Mount the @checkstack/sdk editor bundle (script helpers + typed client) so
|
|
650
|
+
// `import ... from "@checkstack/sdk/healthcheck"` resolves with real types.
|
|
651
|
+
// Shared/module-scoped + reset-on-version (mountSdkTypes); the fetch lives in
|
|
652
|
+
// the consumer so this component is network-agnostic + DOM-test-free.
|
|
653
|
+
useEffect(() => {
|
|
654
|
+
if (!apiReady || !isTsLike || sdkTypes === undefined) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
mountSdkTypes({ files: sdkTypes, resetKey: sdkTypesResetKey });
|
|
658
|
+
}, [apiReady, isTsLike, sdkTypes, sdkTypesResetKey]);
|
|
451
659
|
|
|
452
660
|
// Lazy Automatic Type Acquisition (ATA). For TS/JS editors with an injected
|
|
453
661
|
// `acquireTypes` resolver, parse the buffer's bare import/require specifiers
|
|
@@ -555,7 +763,7 @@ export const TypefoxEditor = ({
|
|
|
555
763
|
return {
|
|
556
764
|
suggestions: entries.map((entry) => ({
|
|
557
765
|
label: entry.name,
|
|
558
|
-
kind:
|
|
766
|
+
kind: monacoRuntime.languages.CompletionItemKind.Module,
|
|
559
767
|
detail: entry.detail,
|
|
560
768
|
insertText: entry.name,
|
|
561
769
|
filterText: entry.name,
|
|
@@ -567,7 +775,7 @@ export const TypefoxEditor = ({
|
|
|
567
775
|
};
|
|
568
776
|
|
|
569
777
|
const disposables = (["typescript", "javascript"] as const).map((lang) =>
|
|
570
|
-
|
|
778
|
+
monacoRuntime.languages.registerCompletionItemProvider(lang, {
|
|
571
779
|
// Opening quotes start a specifier; `:` advances into a `node:`/`bun:`
|
|
572
780
|
// builtin; `/` advances into a scoped name or subpath.
|
|
573
781
|
triggerCharacters: ['"', "'", "/", ":"],
|
|
@@ -628,7 +836,7 @@ export const TypefoxEditor = ({
|
|
|
628
836
|
return {
|
|
629
837
|
suggestions: keys.map((key) => ({
|
|
630
838
|
label: `["${key}"]`,
|
|
631
|
-
kind:
|
|
839
|
+
kind: monacoRuntime.languages.CompletionItemKind.Property,
|
|
632
840
|
detail: objectExpression,
|
|
633
841
|
insertText: `["${key}"]`,
|
|
634
842
|
filterText: key,
|
|
@@ -657,7 +865,7 @@ export const TypefoxEditor = ({
|
|
|
657
865
|
};
|
|
658
866
|
|
|
659
867
|
const disposables = (["typescript", "javascript"] as const).map((lang) =>
|
|
660
|
-
|
|
868
|
+
monacoRuntime.languages.registerCompletionItemProvider(lang, {
|
|
661
869
|
triggerCharacters: ["."],
|
|
662
870
|
provideCompletionItems,
|
|
663
871
|
}),
|
|
@@ -708,7 +916,7 @@ export const TypefoxEditor = ({
|
|
|
708
916
|
)
|
|
709
917
|
.map((prop, index) => ({
|
|
710
918
|
label: `{{${prop.path}}}`,
|
|
711
|
-
kind:
|
|
919
|
+
kind: monacoRuntime.languages.CompletionItemKind.Variable,
|
|
712
920
|
detail: prop.type,
|
|
713
921
|
documentation: prop.description,
|
|
714
922
|
insertText: `{{${prop.path}}}`,
|
|
@@ -727,7 +935,7 @@ export const TypefoxEditor = ({
|
|
|
727
935
|
return { suggestions, incomplete: false };
|
|
728
936
|
};
|
|
729
937
|
|
|
730
|
-
const provider =
|
|
938
|
+
const provider = monacoRuntime.languages.registerCompletionItemProvider(
|
|
731
939
|
languageId,
|
|
732
940
|
{ triggerCharacters: ["{"], provideCompletionItems },
|
|
733
941
|
);
|
|
@@ -829,7 +1037,7 @@ export const TypefoxEditor = ({
|
|
|
829
1037
|
)
|
|
830
1038
|
.map((v, index) => ({
|
|
831
1039
|
label: `$${v.name}`,
|
|
832
|
-
kind:
|
|
1040
|
+
kind: monacoRuntime.languages.CompletionItemKind.Variable,
|
|
833
1041
|
detail: v.example ? `e.g. ${v.example}` : "shell env var",
|
|
834
1042
|
// Full name in the (wrapping) docs panel so long CHECKSTACK_* names
|
|
835
1043
|
// stay legible even when the suggest-list label truncates.
|
|
@@ -850,7 +1058,7 @@ export const TypefoxEditor = ({
|
|
|
850
1058
|
return { suggestions, incomplete: false };
|
|
851
1059
|
};
|
|
852
1060
|
|
|
853
|
-
const provider =
|
|
1061
|
+
const provider = monacoRuntime.languages.registerCompletionItemProvider("shell", {
|
|
854
1062
|
triggerCharacters: ["$", "{"],
|
|
855
1063
|
provideCompletionItems,
|
|
856
1064
|
});
|
|
@@ -866,7 +1074,7 @@ export const TypefoxEditor = ({
|
|
|
866
1074
|
if (!apiReady) {
|
|
867
1075
|
return;
|
|
868
1076
|
}
|
|
869
|
-
const model =
|
|
1077
|
+
const model = monacoRuntime.editor
|
|
870
1078
|
.getModels()
|
|
871
1079
|
.find((candidate) => candidate.uri.toString().includes(modelId));
|
|
872
1080
|
if (!model) {
|
|
@@ -876,14 +1084,14 @@ export const TypefoxEditor = ({
|
|
|
876
1084
|
severity: EditorMarker["severity"],
|
|
877
1085
|
): monaco.MarkerSeverity => {
|
|
878
1086
|
if (severity === "warning") {
|
|
879
|
-
return
|
|
1087
|
+
return monacoRuntime.MarkerSeverity.Warning;
|
|
880
1088
|
}
|
|
881
1089
|
if (severity === "info") {
|
|
882
|
-
return
|
|
1090
|
+
return monacoRuntime.MarkerSeverity.Info;
|
|
883
1091
|
}
|
|
884
|
-
return
|
|
1092
|
+
return monacoRuntime.MarkerSeverity.Error;
|
|
885
1093
|
};
|
|
886
|
-
|
|
1094
|
+
monacoRuntime.editor.setModelMarkers(
|
|
887
1095
|
model,
|
|
888
1096
|
"external-validation",
|
|
889
1097
|
(markers ?? []).map((marker) => ({
|
|
@@ -896,7 +1104,7 @@ export const TypefoxEditor = ({
|
|
|
896
1104
|
})),
|
|
897
1105
|
);
|
|
898
1106
|
return () => {
|
|
899
|
-
|
|
1107
|
+
monacoRuntime.editor.setModelMarkers(model, "external-validation", []);
|
|
900
1108
|
};
|
|
901
1109
|
}, [apiReady, markers, modelId]);
|
|
902
1110
|
|
|
@@ -918,7 +1126,7 @@ export const TypefoxEditor = ({
|
|
|
918
1126
|
const owner = "template-validation";
|
|
919
1127
|
const runValidation = (): void => {
|
|
920
1128
|
const diagnostics = validate(model.getValue());
|
|
921
|
-
|
|
1129
|
+
monacoRuntime.editor.setModelMarkers(
|
|
922
1130
|
model,
|
|
923
1131
|
owner,
|
|
924
1132
|
diagnostics.map((diagnostic) => {
|
|
@@ -932,7 +1140,7 @@ export const TypefoxEditor = ({
|
|
|
932
1140
|
endLineNumber: end.lineNumber,
|
|
933
1141
|
endColumn: end.column,
|
|
934
1142
|
message: diagnostic.message,
|
|
935
|
-
severity:
|
|
1143
|
+
severity: monacoRuntime.MarkerSeverity.Error,
|
|
936
1144
|
};
|
|
937
1145
|
}),
|
|
938
1146
|
);
|
|
@@ -943,25 +1151,10 @@ export const TypefoxEditor = ({
|
|
|
943
1151
|
});
|
|
944
1152
|
return () => {
|
|
945
1153
|
subscription.dispose();
|
|
946
|
-
|
|
1154
|
+
monacoRuntime.editor.setModelMarkers(model, owner, []);
|
|
947
1155
|
};
|
|
948
1156
|
}, [apiReady, language, modelId]);
|
|
949
1157
|
|
|
950
|
-
const vscodeApiConfig: MonacoVscodeApiConfig = {
|
|
951
|
-
// 'classic' is the standalone axis (no extension host); 'extended' is the
|
|
952
|
-
// extension-host axis we deliberately avoid in this migration.
|
|
953
|
-
$type: "classic",
|
|
954
|
-
// Register the missing ILanguageStatusService (see the override comment
|
|
955
|
-
// above) so focusing a JSON editor doesn't throw "addStatus is not
|
|
956
|
-
// supported".
|
|
957
|
-
serviceOverrides: { ...languageStatusServiceOverride },
|
|
958
|
-
viewsConfig: {
|
|
959
|
-
// Plain editor, no workbench views.
|
|
960
|
-
$type: "EditorService",
|
|
961
|
-
},
|
|
962
|
-
monacoWorkerFactory: ensureStandaloneWorkerFactory,
|
|
963
|
-
};
|
|
964
|
-
|
|
965
1158
|
const editorAppConfig: EditorAppConfig = {
|
|
966
1159
|
// Unique per instance (modelId includes a useId suffix) so multiple editors
|
|
967
1160
|
// sharing the same `id` prop don't collide in the wrapper's app registry.
|
|
@@ -974,10 +1167,12 @@ export const TypefoxEditor = ({
|
|
|
974
1167
|
},
|
|
975
1168
|
},
|
|
976
1169
|
editorOptions: {
|
|
977
|
-
//
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
theme
|
|
1170
|
+
// Derived from useTheme().resolvedTheme: "vs" for light, "vs-dark" for
|
|
1171
|
+
// dark. These are the two built-in classic themes available in the
|
|
1172
|
+
// standalone setup (the VS Code 'Default Dark Modern' theme requires the
|
|
1173
|
+
// extension-host theme-defaults extension, which the standalone setup
|
|
1174
|
+
// omits).
|
|
1175
|
+
theme: monacoTheme,
|
|
981
1176
|
minimap: { enabled: false },
|
|
982
1177
|
automaticLayout: true,
|
|
983
1178
|
// Force completions to come from the TS language service rather than
|
|
@@ -1001,12 +1196,30 @@ export const TypefoxEditor = ({
|
|
|
1001
1196
|
? { minHeight: `${minHeight}px`, height: "100%" }
|
|
1002
1197
|
: { minHeight: `${minHeight}px`, height: `${minHeight}px` };
|
|
1003
1198
|
|
|
1199
|
+
// Non-initializer editors wait for the services to be up before mounting (a
|
|
1200
|
+
// sized, non-animated placeholder until then, so the layout doesn't jump).
|
|
1201
|
+
// The initializer mounts immediately and brings the services up.
|
|
1202
|
+
if (!canMountWrapper) {
|
|
1203
|
+
return (
|
|
1204
|
+
<div
|
|
1205
|
+
style={containerStyle}
|
|
1206
|
+
className="w-full rounded-md bg-muted"
|
|
1207
|
+
aria-busy="true"
|
|
1208
|
+
aria-label={placeholder ?? "Loading editor"}
|
|
1209
|
+
/>
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1004
1213
|
return (
|
|
1005
1214
|
<MonacoEditorReactComp
|
|
1006
1215
|
style={containerStyle}
|
|
1007
1216
|
vscodeApiConfig={vscodeApiConfig}
|
|
1008
1217
|
editorAppConfig={editorAppConfig}
|
|
1009
1218
|
onTextChanged={handleTextChanged}
|
|
1219
|
+
// Route wrapper errors to our handler rather than letting @typefox reset
|
|
1220
|
+
// its run-queue lock and re-throw as an uncaught rejection (recommended by
|
|
1221
|
+
// the monaco-languageclient maintainers).
|
|
1222
|
+
onError={handleEditorError}
|
|
1010
1223
|
onEditorStartDone={() => {
|
|
1011
1224
|
// Per-editor ready signal for the completion providers. We use this
|
|
1012
1225
|
// (not onVscodeApiInitDone) because the vscode API initialises globally
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { MONACO_THEME_MAP, VARIABLE_TOKEN_COLOR } from "./editorTheme";
|
|
3
|
+
|
|
4
|
+
describe("MONACO_THEME_MAP", () => {
|
|
5
|
+
it("maps light to vs", () => {
|
|
6
|
+
expect(MONACO_THEME_MAP["light"]).toBe("vs");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("maps dark to vs-dark", () => {
|
|
10
|
+
expect(MONACO_THEME_MAP["dark"]).toBe("vs-dark");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("covers both resolved theme values", () => {
|
|
14
|
+
const keys = Object.keys(MONACO_THEME_MAP);
|
|
15
|
+
expect(keys).toContain("light");
|
|
16
|
+
expect(keys).toContain("dark");
|
|
17
|
+
expect(keys).toHaveLength(2);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("VARIABLE_TOKEN_COLOR", () => {
|
|
22
|
+
it("provides distinct colors for light and dark", () => {
|
|
23
|
+
expect(VARIABLE_TOKEN_COLOR["light"]).not.toBe(VARIABLE_TOKEN_COLOR["dark"]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("light color is a valid hex color", () => {
|
|
27
|
+
expect(VARIABLE_TOKEN_COLOR["light"]).toMatch(/^#[0-9a-fA-F]{6}$/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("dark color is a valid hex color", () => {
|
|
31
|
+
expect(VARIABLE_TOKEN_COLOR["dark"]).toMatch(/^#[0-9a-fA-F]{6}$/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("dark color matches the vs-dark variable token color", () => {
|
|
35
|
+
expect(VARIABLE_TOKEN_COLOR["dark"]).toBe("#9cdcfe");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("light color matches the vs light variable token color", () => {
|
|
39
|
+
expect(VARIABLE_TOKEN_COLOR["light"]).toBe("#0070c1");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure theme-mapping constants for the Monaco editor. Extracted here (and not
|
|
3
|
+
* inlined in TypefoxEditor.tsx) so the logic can be unit-tested without
|
|
4
|
+
* importing browser-only Monaco modules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Maps the app's resolved theme to the matching Monaco built-in theme
|
|
9
|
+
* identifier. Only the two built-in standalone themes are used: the VS Code
|
|
10
|
+
* 'Default Dark Modern' theme would require the extension-host theme-defaults
|
|
11
|
+
* extension, which the standalone setup omits.
|
|
12
|
+
*/
|
|
13
|
+
export const MONACO_THEME_MAP: Record<"light" | "dark", string> = {
|
|
14
|
+
light: "vs",
|
|
15
|
+
dark: "vs-dark",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Color used for the `{{ }}` / `$VAR` decoration in each Monaco theme.
|
|
20
|
+
* - vs-dark uses #9cdcfe (the VS Code dark "variable" token).
|
|
21
|
+
* - vs (light) uses #0070c1 (the VS Code light "variable.other" token).
|
|
22
|
+
*/
|
|
23
|
+
export const VARIABLE_TOKEN_COLOR: Record<"light" | "dark", string> = {
|
|
24
|
+
light: "#0070c1",
|
|
25
|
+
dark: "#9cdcfe",
|
|
26
|
+
};
|
|
@@ -38,7 +38,9 @@ export {
|
|
|
38
38
|
// Subscribe to / query the monaco-vscode "services ready" transition so a
|
|
39
39
|
// consumer (the automation editor's script validator + its hidden services
|
|
40
40
|
// booter) can react the moment the first editor initializes the services.
|
|
41
|
+
// Sourced from the Monaco-free signal module so this barrel re-export does NOT
|
|
42
|
+
// drag the `@codingame/*` stack onto pages that never mount an editor.
|
|
41
43
|
export {
|
|
42
44
|
onVscodeServicesReady,
|
|
43
45
|
areVscodeServicesReady,
|
|
44
|
-
} from "./
|
|
46
|
+
} from "./vscodeServicesSignal";
|