@checkstack/ui 1.10.0 → 1.11.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 +384 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +2 -2
- package/src/components/ActionCard.tsx +221 -0
- package/src/components/CodeEditor/CodeEditor.tsx +51 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +868 -0
- package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
- package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
- package/src/components/CodeEditor/index.ts +2 -0
- package/src/components/CodeEditor/scriptContext.test.ts +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +109 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
- package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
- package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
- package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
- package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
- package/src/components/DynamicForm/DynamicForm.tsx +2 -0
- package/src/components/DynamicForm/FormField.tsx +29 -9
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +16 -7
- package/src/components/DynamicForm/types.ts +11 -0
- package/src/components/TemplateInput.tsx +104 -0
- package/src/components/TemplateInputToggle.tsx +111 -0
- package/src/components/TemplateValueInput.test.ts +98 -0
- package/src/components/TemplateValueInput.tsx +470 -0
- package/src/components/VariablePicker.tsx +271 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +5 -0
- package/stories/ActionCard.stories.tsx +62 -0
- package/stories/Alert.stories.tsx +5 -5
- package/stories/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
- package/src/components/CodeEditor/monacoStdlib.ts +0 -62
- 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
|
-
}
|