@checkstack/ui 1.10.0 → 1.12.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.
Files changed (72) hide show
  1. package/.storybook/main.ts +43 -0
  2. package/CHANGELOG.md +565 -0
  3. package/package.json +15 -7
  4. package/scripts/generate-stdlib-types.ts +25 -2
  5. package/src/components/ActionCard.tsx +309 -0
  6. package/src/components/CodeEditor/CodeEditor.tsx +132 -9
  7. package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -0
  8. package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
  9. package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
  10. package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
  11. package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
  12. package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
  13. package/src/components/CodeEditor/importSpecifiers.ts +267 -0
  14. package/src/components/CodeEditor/index.ts +26 -0
  15. package/src/components/CodeEditor/monacoTsService.ts +217 -0
  16. package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
  17. package/src/components/CodeEditor/popoutTitle.ts +31 -0
  18. package/src/components/CodeEditor/scriptContext.test.ts +41 -0
  19. package/src/components/CodeEditor/scriptContext.ts +76 -1
  20. package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
  21. package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
  22. package/src/components/CodeEditor/templateValidation.ts +51 -0
  23. package/src/components/CodeEditor/types.ts +168 -0
  24. package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
  25. package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
  26. package/src/components/CodeEditor/validateScripts.ts +132 -0
  27. package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
  28. package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
  29. package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
  30. package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
  31. package/src/components/Dialog.tsx +32 -11
  32. package/src/components/DurationInput.tsx +121 -0
  33. package/src/components/DynamicForm/DynamicForm.tsx +27 -1
  34. package/src/components/DynamicForm/FormField.tsx +138 -10
  35. package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
  36. package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
  37. package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
  38. package/src/components/DynamicForm/index.ts +6 -0
  39. package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
  40. package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
  41. package/src/components/DynamicForm/types.ts +83 -1
  42. package/src/components/DynamicForm/utils.ts +32 -0
  43. package/src/components/Popover.tsx +6 -1
  44. package/src/components/ScriptTestPanel.logic.test.ts +139 -0
  45. package/src/components/ScriptTestPanel.logic.ts +137 -0
  46. package/src/components/ScriptTestPanel.tsx +394 -0
  47. package/src/components/Sheet.tsx +21 -6
  48. package/src/components/TemplateInput.tsx +104 -0
  49. package/src/components/TemplateInputToggle.tsx +111 -0
  50. package/src/components/TemplateValueInput.test.ts +98 -0
  51. package/src/components/TemplateValueInput.tsx +470 -0
  52. package/src/components/TimeOfDayInput.tsx +116 -0
  53. package/src/components/VariablePicker.tsx +271 -0
  54. package/src/components/comboboxInteraction.ts +39 -0
  55. package/src/components/portalContainer.ts +24 -0
  56. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  57. package/src/hooks/useInitOnceForKey.ts +21 -18
  58. package/src/index.ts +9 -0
  59. package/stories/ActionCard.stories.tsx +122 -0
  60. package/stories/Alert.stories.tsx +5 -5
  61. package/stories/CodeEditor.stories.tsx +47 -2
  62. package/stories/DurationInput.stories.tsx +59 -0
  63. package/stories/ScriptTestPanel.stories.tsx +106 -0
  64. package/stories/SecretEnvEditor.stories.tsx +80 -0
  65. package/stories/TemplateInputToggle.stories.tsx +77 -0
  66. package/stories/TemplateValueInput.stories.tsx +65 -0
  67. package/stories/TimeOfDayInput.stories.tsx +34 -0
  68. package/stories/VariablePicker.stories.tsx +109 -0
  69. package/tsconfig.json +1 -0
  70. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  71. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  72. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
@@ -1,616 +0,0 @@
1
- // Side-effect import: bundles Monaco's per-language workers via Vite
2
- // `?worker` imports and points `@monaco-editor/react`'s loader at the
3
- // local Monaco rather than its CDN default. Must run before any
4
- // `<Editor>` mount. See monacoWorkers.ts for the full explanation
5
- // (TL;DR: without local workers, the TS service can't spawn `ts.worker`,
6
- // so TypeScript editors silently degrade to no-language-service mode
7
- // while shell editors keep working because they only need the generic
8
- // editor worker).
9
- import "./monacoWorkers";
10
-
11
- import React from "react";
12
- import Editor, {
13
- type OnMount,
14
- type Monaco,
15
- } from "@monaco-editor/react";
16
- import type { editor, Position } from "monaco-editor";
17
- import { detectOpenTemplate, detectAutoClosedBraces } from "./templateUtils";
18
- import { ensureMonacoStdlib } from "./monacoStdlib";
19
- import {
20
- matchShellEnvVarTrigger,
21
- buildShellEnvVarInsertText,
22
- } from "./shellEnvVarMatcher";
23
-
24
- // Note on loader config: previously this module called
25
- // loader.config({ "vs/nls": { availableLanguages: { "*": "en" } } })
26
- // to keep Monaco in English when the loader fetched it from a CDN. We
27
- // now bundle Monaco locally (see monacoWorkers.ts → loader.config({
28
- // monaco })), which short-circuits the AMD loader entirely — the NLS
29
- // config can't apply because no remote Monaco is ever fetched. English
30
- // is the default for the locally bundled `monaco-editor` package, so
31
- // removing the call has no observable effect.
32
-
33
- export type CodeEditorLanguage =
34
- | "json"
35
- | "yaml"
36
- | "xml"
37
- | "markdown"
38
- | "javascript"
39
- | "typescript"
40
- | "shell";
41
-
42
- /**
43
- * A single payload property available for templating
44
- */
45
- export interface TemplateProperty {
46
- /** Full path to the property, e.g., "payload.incident.title" */
47
- path: string;
48
- /** Type of the property, e.g., "string", "number", "boolean" */
49
- type: string;
50
- /** Optional description of the property */
51
- description?: string;
52
- }
53
-
54
- /**
55
- * A single environment variable available to shell scripts. Surfaced as
56
- * a Monaco completion item after the user types `$` or `${` in shell mode.
57
- */
58
- export interface ShellEnvVar {
59
- /** Variable name without the leading `$`, e.g. `EVENT_ID`. */
60
- name: string;
61
- /** Optional description shown in the completion popup. */
62
- description?: string;
63
- /** Optional example value shown in the completion item detail. */
64
- example?: string;
65
- }
66
-
67
- export interface CodeEditorProps {
68
- /** Unique identifier for the editor */
69
- id?: string;
70
- /** Current value of the editor */
71
- value: string;
72
- /** Callback when the value changes */
73
- onChange: (value: string) => void;
74
- /** Language for syntax highlighting */
75
- language?: CodeEditorLanguage;
76
- /** Minimum height of the editor */
77
- minHeight?: string;
78
- /** Whether the editor is read-only */
79
- readOnly?: boolean;
80
- /** Placeholder text when empty */
81
- placeholder?: string;
82
- /**
83
- * TypeScript type definitions to inject for IntelliSense.
84
- * Generated from JSON schemas for context-aware autocomplete.
85
- */
86
- typeDefinitions?: string;
87
- /**
88
- * Optional template properties for autocomplete.
89
- * When provided, typing "{{" triggers autocomplete with available template variables.
90
- */
91
- templateProperties?: TemplateProperty[];
92
- /**
93
- * Optional environment-variable hints for shell mode. When provided and
94
- * `language === "shell"`, Monaco autocompletes them after `$` and `${`.
95
- * Use this to surface platform-injected vars like `EVENT_ID` or
96
- * `PAYLOAD_*` without forcing users to memorise the names.
97
- */
98
- shellEnvVars?: ShellEnvVar[];
99
- }
100
-
101
- // Map language names to Monaco language IDs
102
- const languageMap: Record<string, string> = {
103
- json: "json",
104
- yaml: "yaml",
105
- xml: "xml",
106
- markdown: "markdown",
107
- javascript: "javascript",
108
- typescript: "typescript",
109
- shell: "shell",
110
- };
111
-
112
- /**
113
- * File-extension hint used in the per-editor Monaco model URI. Monaco
114
- * decides which language service to use partly from the URI's extension,
115
- * so a path like `<id>.ts` reliably yields the TypeScript service even
116
- * if the `language` prop is still being reconciled by the React wrapper
117
- * during a fast tab-switch. This is what fixes the "TS check now has
118
- * shell highlighting after I created an SSH check" bug: without
119
- * different paths, every `<CodeEditor>` mount reuses Monaco's _default_
120
- * model — and the model's previous language sticks around.
121
- */
122
- const LANGUAGE_EXTENSIONS: Record<string, string> = {
123
- json: "json",
124
- yaml: "yaml",
125
- xml: "xml",
126
- markdown: "md",
127
- javascript: "js",
128
- typescript: "ts",
129
- shell: "sh",
130
- "json-template": "json",
131
- };
132
-
133
- // Track if we've registered the json-template language
134
- let jsonTemplateLanguageRegistered = false;
135
-
136
- /**
137
- * Default type definitions injected on top of the bundled Node/Bun stdlib.
138
- *
139
- * `ensureMonacoStdlib` mounts the real upstream `@types/node` + `bun-types`
140
- * d.ts files into Monaco's virtual filesystem, so `import { loadavg } from
141
- * "node:os"`, `fetch`, `console`, `URL`, etc. are all already declared.
142
- * This block only adds what's _specific_ to the inline-script runner —
143
- * the `context` global and the expected return shape.
144
- *
145
- * Used when no custom typeDefinitions prop is passed.
146
- */
147
- const DEFAULT_BACKEND_TYPE_DEFINITIONS = `
148
- /** Expected return shape for inline-script health checks. */
149
- interface HealthCheckScriptResult {
150
- /** Whether the health check passed. */
151
- success: boolean;
152
- /** Optional status message — surfaces in the run detail. */
153
- message?: string;
154
- /** Optional numeric value — feeds the value chart + anomaly detection. */
155
- value?: number;
156
- }
157
-
158
- /**
159
- * Runtime context exposed by the inline-script runner.
160
- * Available as a global, so just reference \`context.config\`.
161
- */
162
- declare const context: {
163
- /** The raw collector configuration object. */
164
- readonly config: Record<string, unknown>;
165
- };
166
- `;
167
-
168
- /**
169
- * A code editor component with syntax highlighting, IntelliSense, and template autocomplete.
170
- * Uses Monaco Editor (VS Code's editor) for full TypeScript/JavaScript language support.
171
- */
172
- export const CodeEditor: React.FC<CodeEditorProps> = ({
173
- id,
174
- value,
175
- onChange,
176
- language = "json",
177
- minHeight = "100px",
178
- readOnly = false,
179
- placeholder,
180
- typeDefinitions,
181
- templateProperties,
182
- shellEnvVars,
183
- }) => {
184
- const editorRef = React.useRef<editor.IStandaloneCodeEditor | null>(null);
185
- const monacoRef = React.useRef<Monaco | null>(null);
186
- const disposablesRef = React.useRef<{ dispose: () => void }[]>([]);
187
- // Track when editor is mounted to re-trigger useEffects
188
- const [isEditorMounted, setIsEditorMounted] = React.useState(false);
189
-
190
- // Register json-template language BEFORE editor creates the model
191
- const handleEditorWillMount = (monaco: Monaco) => {
192
- if (!jsonTemplateLanguageRegistered) {
193
- monaco.languages.register({ id: "json-template" });
194
- // Use JSON syntax highlighting
195
- monaco.languages.setLanguageConfiguration("json-template", {
196
- comments: {
197
- lineComment: "//",
198
- blockComment: ["/*", "*/"],
199
- },
200
- brackets: [
201
- ["{", "}"],
202
- ["[", "]"],
203
- ],
204
- autoClosingPairs: [
205
- { open: "{", close: "}" },
206
- { open: "[", close: "]" },
207
- { open: '"', close: '"' },
208
- ],
209
- });
210
- // Custom tokenizer that highlights {{...}} templates specially
211
- monaco.languages.setMonarchTokensProvider("json-template", {
212
- tokenizer: {
213
- root: [
214
- [/\{\{[^}]*\}\}/, "variable"], // Template syntax - highlight specially
215
- [/"([^"\\]|\\.)*$/, "string.invalid"], // Unterminated string
216
- [/"/, "string", "@string"],
217
- [/[{}[\]]/, "delimiter.bracket"],
218
- [/-?\d+\.?\d*([eE][-+]?\d+)?/, "number"],
219
- [/true|false/, "keyword"],
220
- [/null/, "keyword"],
221
- [/[,:]/, "delimiter"],
222
- [/\s+/, "white"],
223
- ],
224
- string: [
225
- [/[^\\"]+/, "string"],
226
- [/\\./, "string.escape"],
227
- [/"/, "string", "@pop"],
228
- ],
229
- },
230
- });
231
- jsonTemplateLanguageRegistered = true;
232
- }
233
- };
234
-
235
- // Handle editor mount
236
- const handleEditorDidMount: OnMount = (editor, monaco) => {
237
- editorRef.current = editor;
238
- monacoRef.current = monaco;
239
- setIsEditorMounted(true);
240
-
241
-
242
-
243
- // Per-editor TS/JS setup. All GLOBAL TS-service config (compiler
244
- // options, diagnostics options, eager model sync) is done once at
245
- // module load in monacoWorkers.ts — doing it here, after the
246
- // service may have already initialised lazily from a previous
247
- // mount, was the root cause of the "open shell first, TS service
248
- // is broken" bug.
249
- //
250
- // The per-editor `addExtraLib` for THIS editor's `context.d.ts` is
251
- // handled by the useEffect below (so it can also refresh when the
252
- // `typeDefinitions` prop changes), keyed by `modelIdRef.current` so
253
- // two TS editors with different schemas don't share a virtual path.
254
- //
255
- // Here we only kick off the lazy stdlib bundle so the ~3 MB chunk
256
- // starts streaming on the first TS editor mount.
257
- if (language === "typescript" || language === "javascript") {
258
- void ensureMonacoStdlib(monaco);
259
- }
260
-
261
- // Handle validation for template syntax in JSON
262
- // Since we're using json-template language (no built-in validation),
263
- // we run our own validation with preprocessed content.
264
- if (
265
- templateProperties &&
266
- templateProperties.length > 0 &&
267
- language === "json"
268
- ) {
269
- const model = editor.getModel();
270
- if (model) {
271
- // Custom validation function
272
- const runCustomValidation = () => {
273
- const content = model.getValue();
274
-
275
- // Replace {{...}} templates with valid JSON strings of same length
276
- // Using same length ensures error positions map correctly
277
- const preprocessed = content.replaceAll(
278
- /\{\{[^}]*\}\}/g,
279
- (match) => `"${"_".repeat(Math.max(0, match.length - 2))}"`,
280
- );
281
-
282
- // Try to parse the preprocessed JSON
283
- const markers: editor.IMarkerData[] = [];
284
- try {
285
- JSON.parse(preprocessed);
286
- } catch (error) {
287
- if (error instanceof SyntaxError) {
288
- // Extract position from error message (varies by browser)
289
- const message = error.message;
290
- // Try to find line/column info - JSON.parse errors are at position
291
- const posMatch = message.match(/position (\d+)/i);
292
-
293
- if (posMatch) {
294
- const pos = Number.parseInt(posMatch[1], 10);
295
- const position = model.getPositionAt(pos);
296
- markers.push({
297
- severity: monaco.MarkerSeverity.Error,
298
- message: message,
299
- startLineNumber: position.lineNumber,
300
- startColumn: position.column,
301
- endLineNumber: position.lineNumber,
302
- endColumn: position.column + 1,
303
- });
304
- } else {
305
- // Fallback: put error at start
306
- markers.push({
307
- severity: monaco.MarkerSeverity.Error,
308
- message: message,
309
- startLineNumber: 1,
310
- startColumn: 1,
311
- endLineNumber: 1,
312
- endColumn: 2,
313
- });
314
- }
315
- }
316
- }
317
-
318
- monaco.editor.setModelMarkers(model, "json-template", markers);
319
- };
320
-
321
- // Run validation on content changes
322
- const contentListener = model.onDidChangeContent(() => {
323
- runCustomValidation();
324
- });
325
-
326
- // Run initial validation
327
- runCustomValidation();
328
-
329
- disposablesRef.current.push(contentListener, {
330
- dispose: () => {
331
- // Re-enable Monaco's JSON validation when unmounted
332
- monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
333
- validate: true,
334
- });
335
- },
336
- });
337
- }
338
- }
339
- };
340
-
341
- // Track the template provider separately so we can update it
342
- const templateProviderRef = React.useRef<{ dispose: () => void } | undefined>(
343
- undefined,
344
- );
345
-
346
- // Cleanup on unmount
347
- React.useEffect(() => {
348
- return () => {
349
- for (const d of disposablesRef.current) {
350
- d.dispose();
351
- }
352
- disposablesRef.current = [];
353
- if (templateProviderRef.current) {
354
- templateProviderRef.current.dispose();
355
- }
356
- };
357
- }, []);
358
-
359
- // Update template completion provider when templateProperties changes
360
- React.useEffect(() => {
361
- if (!monacoRef.current) return;
362
-
363
- if (templateProviderRef.current) {
364
- templateProviderRef.current.dispose();
365
- }
366
-
367
- if (!templateProperties || templateProperties.length === 0) return;
368
-
369
- const monaco = monacoRef.current;
370
-
371
- // Compute effective language - same logic as used for Editor
372
- const hasTemplates = templateProperties && templateProperties.length > 0;
373
- const providerLanguage =
374
- hasTemplates && language === "json"
375
- ? "json-template"
376
- : (languageMap[language] ?? language);
377
-
378
- const provider = monaco.languages.registerCompletionItemProvider(
379
- providerLanguage,
380
- {
381
- triggerCharacters: ["{"],
382
- provideCompletionItems: (
383
- model: editor.ITextModel,
384
- position: Position,
385
- ) => {
386
- // Get full content and cursor offset for utility functions
387
- const content = model.getValue();
388
- const cursorOffset = model.getOffsetAt(position);
389
-
390
- // Use tested utility to detect if we're in an open template
391
- const openTemplate = detectOpenTemplate({ content, cursorOffset });
392
-
393
- if (!openTemplate.isInTemplate) {
394
- return { suggestions: [] };
395
- }
396
-
397
- const query = openTemplate.query.toLowerCase();
398
- const startColumn = openTemplate.startColumn;
399
-
400
- // Check if Monaco auto-closed with }} after cursor using tested utility
401
- const autoClosedBraces = detectAutoClosedBraces({
402
- content,
403
- cursorOffset,
404
- });
405
- const endColumn = position.column + autoClosedBraces;
406
-
407
- const suggestions = templateProperties
408
- .filter(
409
- (prop) => query === "" || prop.path.toLowerCase().includes(query),
410
- )
411
- .map((prop, index) => ({
412
- label: `{{${prop.path}}}`,
413
- kind: monaco.languages.CompletionItemKind.Variable,
414
- detail: prop.type,
415
- documentation: prop.description,
416
- insertText: `{{${prop.path}}}`,
417
- // Use sortText starting with space to appear before other completions
418
- sortText: ` ${String(index).padStart(4, "0")}`,
419
- // Use the query as filterText so it matches what user typed
420
- filterText: `{{${query}${prop.path}`,
421
- preselect: index === 0, // Preselect first item
422
- range: {
423
- startLineNumber: position.lineNumber,
424
- startColumn: startColumn,
425
- endLineNumber: position.lineNumber,
426
- endColumn: endColumn,
427
- },
428
- }));
429
-
430
- return { suggestions, incomplete: false };
431
- },
432
- },
433
- );
434
-
435
- templateProviderRef.current = provider;
436
-
437
- return () => {
438
- provider.dispose();
439
- };
440
- }, [templateProperties, language, isEditorMounted]);
441
-
442
- // Install (and refresh) the per-editor context.d.ts.
443
- //
444
- // - Includes `isEditorMounted` in the deps so the effect runs after
445
- // `monacoRef.current` is wired up by `handleEditorDidMount`.
446
- // - Uses a unique virtual path keyed by this editor instance
447
- // (`context-<modelId>.d.ts`) so two TS editors with different
448
- // collector schemas can't overwrite each other's context types.
449
- // - Returns a cleanup that disposes the registered lib, both on
450
- // unmount and when `typeDefinitions` / `language` change.
451
- React.useEffect(() => {
452
- if (!isEditorMounted) return;
453
- if (!monacoRef.current) return;
454
- if (language !== "typescript" && language !== "javascript") return;
455
-
456
- const monaco = monacoRef.current;
457
- const defaults =
458
- language === "typescript"
459
- ? monaco.typescript.typescriptDefaults
460
- : monaco.typescript.javascriptDefaults;
461
-
462
- const definitions = typeDefinitions ?? DEFAULT_BACKEND_TYPE_DEFINITIONS;
463
- const contextLibPath = `file:///context-${modelIdRef.current}.d.ts`;
464
- const lib = defaults.addExtraLib(definitions, contextLibPath);
465
-
466
- return () => {
467
- lib.dispose();
468
- };
469
- }, [typeDefinitions, language, isEditorMounted]);
470
-
471
- // Shell env-var completion. When the caller passes `shellEnvVars`, we
472
- // register a Monaco provider that suggests variable names after the
473
- // user types `$` or `${` — and also brace-closes `${name}` correctly.
474
- // Re-registers when the var list or language changes.
475
- React.useEffect(() => {
476
- if (!isEditorMounted) return;
477
- if (!monacoRef.current) return;
478
- if (language !== "shell") return;
479
- if (!shellEnvVars || shellEnvVars.length === 0) return;
480
-
481
- const monaco = monacoRef.current;
482
- const provider = monaco.languages.registerCompletionItemProvider("shell", {
483
- triggerCharacters: ["$", "{"],
484
- provideCompletionItems: (model: editor.ITextModel, position: Position) => {
485
- const lineText = model.getLineContent(position.lineNumber);
486
- const textBefore = lineText.slice(0, position.column - 1);
487
- const match = matchShellEnvVarTrigger(textBefore);
488
- if (!match) return { suggestions: [] };
489
-
490
- const startColumn = position.column - match.prefixLength;
491
-
492
- const suggestions = shellEnvVars
493
- .filter(
494
- (v) =>
495
- match.query === "" ||
496
- v.name.toUpperCase().includes(match.query),
497
- )
498
- .map((v, index) => ({
499
- label: `$${v.name}`,
500
- kind: monaco.languages.CompletionItemKind.Variable,
501
- detail: v.example ? `e.g. ${v.example}` : "shell env var",
502
- documentation: v.description,
503
- insertText: buildShellEnvVarInsertText(match, v.name),
504
- sortText: ` ${String(index).padStart(4, "0")}`,
505
- filterText: `$${v.name}`,
506
- range: {
507
- startLineNumber: position.lineNumber,
508
- startColumn,
509
- endLineNumber: position.lineNumber,
510
- endColumn: position.column,
511
- },
512
- }));
513
-
514
- return { suggestions, incomplete: false };
515
- },
516
- });
517
-
518
- return () => {
519
- provider.dispose();
520
- };
521
- }, [shellEnvVars, language, isEditorMounted]);
522
-
523
- // Calculate height from minHeight
524
- const heightValue = minHeight.replace("px", "");
525
- const numericHeight = Number.parseInt(heightValue, 10) || 100;
526
-
527
- // Compute effective language - use json-template when we have templates for JSON
528
- const hasTemplates = templateProperties && templateProperties.length > 0;
529
- const effectiveLanguage =
530
- hasTemplates && language === "json"
531
- ? "json-template"
532
- : (languageMap[language] ?? language);
533
-
534
- // Stable, unique-per-instance model path. We need this for two reasons:
535
- //
536
- // 1. Without a `path`, all `@monaco-editor/react` instances share the
537
- // single default in-memory model. When you switch between a TS
538
- // inline-script check and an SSH (shell) check, the new mount
539
- // inherits the previous model's language, producing visibly wrong
540
- // syntax highlighting until something else triggers a refresh.
541
- //
542
- // 2. The extension portion (`.ts`, `.sh`, `.json`, ...) is one of the
543
- // signals Monaco's language service uses to decide which tokenizer
544
- // and worker to apply, so encoding the effective language in the
545
- // path makes the language choice unambiguous.
546
- //
547
- // We deliberately use a fresh UUID per mount (in a `useRef`, lazy
548
- // init) rather than `useId`: `useId`'s output is derived from the
549
- // call-site's position in the React tree, so a remount of the same
550
- // `<CodeEditor>` slot — typical when the user switches between two
551
- // healthcheck pages that React Router reuses — can return the same
552
- // id as before. `@monaco-editor/react` then reuses the previous
553
- // model (and its language) from Monaco's registry, which is exactly
554
- // the "TS check now has shell highlighting" bug.
555
- const modelIdRef = React.useRef<string | null>(null);
556
- if (modelIdRef.current === null) {
557
- modelIdRef.current = `cs-editor-${
558
- typeof crypto !== "undefined" && "randomUUID" in crypto
559
- ? crypto.randomUUID()
560
- : Math.random().toString(36).slice(2, 11)
561
- }`;
562
- }
563
- const pathExtension =
564
- LANGUAGE_EXTENSIONS[effectiveLanguage] ?? effectiveLanguage;
565
- const modelPath = `${modelIdRef.current}.${pathExtension}`;
566
-
567
- return (
568
- <div
569
- id={id}
570
- className="w-full rounded-md border border-input bg-background font-mono text-sm focus-within:ring-2 focus-within:ring-ring focus-within:border-transparent transition-all box-border overflow-visible"
571
- style={{ minHeight }}
572
- >
573
- <Editor
574
- height={`${Math.max(numericHeight, 100)}px`}
575
- language={effectiveLanguage}
576
- path={modelPath}
577
- value={value}
578
- onChange={(newValue) => onChange(newValue ?? "")}
579
- beforeMount={handleEditorWillMount}
580
- onMount={handleEditorDidMount}
581
- options={{
582
- readOnly,
583
- minimap: { enabled: false },
584
- lineNumbers: "on",
585
- lineNumbersMinChars: 3,
586
- folding: false,
587
- wordWrap: "on",
588
- scrollBeyondLastLine: false,
589
- fontSize: 14,
590
- fontFamily: "ui-monospace, monospace",
591
- tabSize: 2,
592
- automaticLayout: true,
593
- scrollbar: {
594
- vertical: "auto",
595
- horizontal: "auto",
596
- verticalScrollbarSize: 10,
597
- horizontalScrollbarSize: 10,
598
- },
599
- overviewRulerLanes: 0,
600
- hideCursorInOverviewRuler: true,
601
- overviewRulerBorder: false,
602
- renderLineHighlight: "none",
603
- contextmenu: false,
604
- // Placeholder support via aria-label
605
- ariaLabel: placeholder ?? "Code editor",
606
- }}
607
- theme="vs-dark"
608
- loading={
609
- <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
610
- Loading editor...
611
- </div>
612
- }
613
- />
614
- </div>
615
- );
616
- };
@@ -1,62 +0,0 @@
1
- import type { Monaco } from "@monaco-editor/react";
2
-
3
- /**
4
- * Lazy-load the bundled `@types/node` + `bun-types` declarations and mount
5
- * them into a Monaco TypeScript service.
6
- *
7
- * The bundle lives at [generated/stdlib-types.json](./generated/stdlib-types.json)
8
- * and is produced by `bun run generate:monaco-types`. It's ~3 MB of upstream
9
- * `.d.ts` content, which is why we **dynamically import** it: bundlers
10
- * (Vite, Rspack, …) split the JSON into its own chunk so it never blocks the
11
- * initial frontend load. The chunk is fetched the first time the user opens
12
- * an inline-script editor and cached for the rest of the session.
13
- *
14
- * Each file is registered at its real `node_modules/...` virtual path, so
15
- * Monaco resolves cross-file `/// <reference>` directives just like the
16
- * canonical TypeScript service would on disk.
17
- */
18
-
19
- type StdlibBundle = Record<string, string>;
20
-
21
- let bundlePromise: Promise<StdlibBundle> | undefined;
22
-
23
- function loadBundle(): Promise<StdlibBundle> {
24
- if (!bundlePromise) {
25
- bundlePromise = import("./generated/stdlib-types.json").then(
26
- (mod) => mod.default as StdlibBundle,
27
- );
28
- }
29
- return bundlePromise;
30
- }
31
-
32
- const installedFor = new WeakSet<object>();
33
-
34
- /**
35
- * Register the bundled stdlib types with the given Monaco instance.
36
- *
37
- * Safe to call multiple times — the work is gated on a per-Monaco
38
- * `WeakSet` so we only `addExtraLib` once per instance, even across many
39
- * editor mounts.
40
- *
41
- * @returns true once the stdlib is installed (or was already).
42
- */
43
- export async function ensureMonacoStdlib(monaco: Monaco): Promise<boolean> {
44
- if (installedFor.has(monaco)) return true;
45
-
46
- const bundle = await loadBundle();
47
-
48
- // TypeScript and JavaScript share a single TS service in Monaco, but each
49
- // service tracks its own extra-libs. Register against both so the same
50
- // bundle covers either language mode.
51
- for (const defaults of [
52
- monaco.languages.typescript.typescriptDefaults,
53
- monaco.languages.typescript.javascriptDefaults,
54
- ]) {
55
- for (const [path, content] of Object.entries(bundle)) {
56
- defaults.addExtraLib(content, `file:///${path}`);
57
- }
58
- }
59
-
60
- installedFor.add(monaco);
61
- return true;
62
- }