@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.
- package/.storybook/main.ts +43 -0
- package/CHANGELOG.md +565 -0
- package/package.json +15 -7
- package/scripts/generate-stdlib-types.ts +25 -2
- package/src/components/ActionCard.tsx +309 -0
- package/src/components/CodeEditor/CodeEditor.tsx +132 -9
- package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -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/generated/builtin-modules.json +1 -0
- package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
- package/src/components/CodeEditor/importSpecifiers.ts +267 -0
- package/src/components/CodeEditor/index.ts +26 -0
- package/src/components/CodeEditor/monacoTsService.ts +217 -0
- package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
- package/src/components/CodeEditor/popoutTitle.ts +31 -0
- package/src/components/CodeEditor/scriptContext.test.ts +41 -0
- package/src/components/CodeEditor/scriptContext.ts +76 -1
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/templateValidation.ts +51 -0
- package/src/components/CodeEditor/types.ts +168 -0
- package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
- package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
- package/src/components/CodeEditor/validateScripts.ts +132 -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/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +27 -1
- package/src/components/DynamicForm/FormField.tsx +138 -10
- package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +6 -0
- package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
- package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
- package/src/components/DynamicForm/types.ts +83 -1
- package/src/components/DynamicForm/utils.ts +32 -0
- package/src/components/Popover.tsx +6 -1
- package/src/components/ScriptTestPanel.logic.test.ts +139 -0
- package/src/components/ScriptTestPanel.logic.ts +137 -0
- package/src/components/ScriptTestPanel.tsx +394 -0
- package/src/components/Sheet.tsx +21 -6
- 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/TimeOfDayInput.tsx +116 -0
- package/src/components/VariablePicker.tsx +271 -0
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/hooks/useInitOnceForKey.test.ts +27 -0
- package/src/hooks/useInitOnceForKey.ts +21 -18
- package/src/index.ts +9 -0
- package/stories/ActionCard.stories.tsx +122 -0
- package/stories/Alert.stories.tsx +5 -5
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/TemplateInputToggle.stories.tsx +77 -0
- package/stories/TemplateValueInput.stories.tsx +65 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/stories/VariablePicker.stories.tsx +109 -0
- package/tsconfig.json +1 -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
|
@@ -89,6 +89,22 @@ interface HealthCheckScriptResult {
|
|
|
89
89
|
interface HealthCheckScriptContext {
|
|
90
90
|
/** Strongly-typed collector configuration. */
|
|
91
91
|
readonly config: ${configType};
|
|
92
|
+
/** Metadata about the health check this run is for. */
|
|
93
|
+
readonly check: {
|
|
94
|
+
/** The health check configuration id. */
|
|
95
|
+
readonly id: string;
|
|
96
|
+
/** The health check's display name (falls back to the id). */
|
|
97
|
+
readonly name: string;
|
|
98
|
+
/** The configured run interval, in seconds. */
|
|
99
|
+
readonly intervalSeconds: number;
|
|
100
|
+
};
|
|
101
|
+
/** Metadata about the system this check runs for. */
|
|
102
|
+
readonly system: {
|
|
103
|
+
/** The system id. */
|
|
104
|
+
readonly id: string;
|
|
105
|
+
/** The system's display name (falls back to the id). */
|
|
106
|
+
readonly name: string;
|
|
107
|
+
};
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
/**
|
|
@@ -328,6 +344,51 @@ const SAFE_SHELL_VARS: ShellEnvVar[] = [
|
|
|
328
344
|
{ name: "SHELL", description: "User's login shell." },
|
|
329
345
|
];
|
|
330
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Run-context vars the satellite injects into every shell health-check
|
|
349
|
+
* run, describing the check + system it's for. Mirrors the reserved
|
|
350
|
+
* `CHECKSTACK_*` keys set by `healthcheck-script-backend`'s shell
|
|
351
|
+
* collector. User-supplied `env` values override these.
|
|
352
|
+
*/
|
|
353
|
+
const HEALTHCHECK_RUN_CONTEXT_VARS: ShellEnvVar[] = [
|
|
354
|
+
{ name: "CHECKSTACK_CHECK_ID", description: "This health check's configuration id." },
|
|
355
|
+
{
|
|
356
|
+
name: "CHECKSTACK_CHECK_NAME",
|
|
357
|
+
description: "This health check's display name (falls back to the id).",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "CHECKSTACK_CHECK_INTERVAL_SECONDS",
|
|
361
|
+
description: "The configured run interval, in seconds.",
|
|
362
|
+
},
|
|
363
|
+
{ name: "CHECKSTACK_SYSTEM_ID", description: "The id of the system being checked." },
|
|
364
|
+
{
|
|
365
|
+
name: "CHECKSTACK_SYSTEM_NAME",
|
|
366
|
+
description: "The system's display name (falls back to the id).",
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
|
|
370
|
+
/** A valid POSIX shell identifier — only these can be referenced as `$NAME`. */
|
|
371
|
+
const SHELL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Turn a user's custom `env` object (e.g. a script action's or health
|
|
375
|
+
* check's "Env (JSON)" field) into `$`-completion hints. Defensive about
|
|
376
|
+
* the loosely-typed form value: non-objects yield nothing, and only keys
|
|
377
|
+
* that are valid shell identifiers are surfaced (a `$my-var` completion
|
|
378
|
+
* wouldn't be usable). Exported so editors that build their shell env
|
|
379
|
+
* suggestions outside the `*ScriptContext` helpers (e.g. the automation
|
|
380
|
+
* action editor) can merge custom env keys the same way.
|
|
381
|
+
*/
|
|
382
|
+
export function customShellEnvVars(env: unknown): ShellEnvVar[] {
|
|
383
|
+
if (typeof env !== "object" || env === null || Array.isArray(env)) return [];
|
|
384
|
+
return Object.keys(env)
|
|
385
|
+
.filter((name) => SHELL_IDENTIFIER.test(name))
|
|
386
|
+
.map((name) => ({
|
|
387
|
+
name,
|
|
388
|
+
description: "Custom variable from this check's Env (JSON) field.",
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
|
|
331
392
|
/**
|
|
332
393
|
* Vars injected by the integration platform on every delivery. Per-
|
|
333
394
|
* payload-field `PAYLOAD_*` vars are appended at call time based on the
|
|
@@ -406,6 +467,13 @@ function flattenSchemaToEnvVars(
|
|
|
406
467
|
*/
|
|
407
468
|
export function healthcheckScriptContext(input: {
|
|
408
469
|
collectorConfigSchema?: JsonSchemaProperty;
|
|
470
|
+
/**
|
|
471
|
+
* The collector's current `env` value (the "Env (JSON)" field). Its
|
|
472
|
+
* keys are surfaced as `$`-completions so a user's own declared vars
|
|
473
|
+
* autocomplete alongside the whitelist + reserved run-context vars.
|
|
474
|
+
* Typed `unknown` because it comes from the loosely-typed form value.
|
|
475
|
+
*/
|
|
476
|
+
customEnv?: unknown;
|
|
409
477
|
}): ScriptEditorContext {
|
|
410
478
|
const configType = input.collectorConfigSchema
|
|
411
479
|
? jsonSchemaToTypeScript(input.collectorConfigSchema)
|
|
@@ -418,7 +486,14 @@ export function healthcheckScriptContext(input: {
|
|
|
418
486
|
javascript: HEALTHCHECK_INLINE_TS_STARTER,
|
|
419
487
|
shell: HEALTHCHECK_SHELL_STARTER,
|
|
420
488
|
},
|
|
421
|
-
|
|
489
|
+
// Most-relevant-first: the user's own declared env, then this check's
|
|
490
|
+
// run-context metadata, then the generic OS whitelist (the suggest list
|
|
491
|
+
// is ordered by insertion index, so order here is what the user sees).
|
|
492
|
+
shellEnvVars: [
|
|
493
|
+
...customShellEnvVars(input.customEnv),
|
|
494
|
+
...HEALTHCHECK_RUN_CONTEXT_VARS,
|
|
495
|
+
...SAFE_SHELL_VARS,
|
|
496
|
+
],
|
|
422
497
|
};
|
|
423
498
|
}
|
|
424
499
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildValidationSource,
|
|
4
|
+
flattenDiagnosticMessage,
|
|
5
|
+
mapWorkerDiagnostics,
|
|
6
|
+
offsetToPosition,
|
|
7
|
+
type RawTsDiagnostic,
|
|
8
|
+
} from "./scriptDiagnostics";
|
|
9
|
+
|
|
10
|
+
describe("offsetToPosition", () => {
|
|
11
|
+
it("returns 1-based line/column", () => {
|
|
12
|
+
const text = "ab\ncde\nf";
|
|
13
|
+
expect(offsetToPosition(text, 0)).toEqual({ line: 1, column: 1 });
|
|
14
|
+
expect(offsetToPosition(text, 1)).toEqual({ line: 1, column: 2 });
|
|
15
|
+
// offset 3 is the 'c' (first char after the first newline)
|
|
16
|
+
expect(offsetToPosition(text, 3)).toEqual({ line: 2, column: 1 });
|
|
17
|
+
expect(offsetToPosition(text, 7)).toEqual({ line: 3, column: 1 });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("clamps an out-of-range offset to the text length", () => {
|
|
21
|
+
expect(offsetToPosition("ab", 999)).toEqual({ line: 1, column: 3 });
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("flattenDiagnosticMessage", () => {
|
|
26
|
+
it("passes through a plain string", () => {
|
|
27
|
+
expect(flattenDiagnosticMessage("boom")).toBe("boom");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("flattens a nested chain depth-first", () => {
|
|
31
|
+
expect(
|
|
32
|
+
flattenDiagnosticMessage({
|
|
33
|
+
messageText: "top",
|
|
34
|
+
next: [
|
|
35
|
+
{ messageText: "child-a" },
|
|
36
|
+
{ messageText: "child-b", next: [{ messageText: "grandchild" }] },
|
|
37
|
+
],
|
|
38
|
+
}),
|
|
39
|
+
).toBe("top child-a child-b grandchild");
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("buildValidationSource", () => {
|
|
44
|
+
it("prepends the type defs and reports the prefix line count", () => {
|
|
45
|
+
const { text, prependedLineCount } = buildValidationSource({
|
|
46
|
+
typeDefinitions: "declare const context: { x: number };", // 1 line
|
|
47
|
+
source: "context.x;",
|
|
48
|
+
});
|
|
49
|
+
expect(prependedLineCount).toBe(1);
|
|
50
|
+
expect(text).toBe("declare const context: { x: number };\ncontext.x;");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("counts multi-line type defs", () => {
|
|
54
|
+
const { prependedLineCount } = buildValidationSource({
|
|
55
|
+
typeDefinitions: "line1\nline2\nline3",
|
|
56
|
+
source: "x",
|
|
57
|
+
});
|
|
58
|
+
expect(prependedLineCount).toBe(3);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("mapWorkerDiagnostics", () => {
|
|
63
|
+
// 2 lines of type defs prepended; user source starts at combined line 3.
|
|
64
|
+
const { text, prependedLineCount } = buildValidationSource({
|
|
65
|
+
typeDefinitions: "declare const context: {\n readonly a: number;\n};",
|
|
66
|
+
source: "const z = context.b;\n", // `b` doesn't exist -> error on user line 1
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const errorOffset = text.indexOf("context.b") + "context.".length; // points at `b`
|
|
70
|
+
|
|
71
|
+
it("shifts a real type error back onto the user's source line", () => {
|
|
72
|
+
const diagnostics: RawTsDiagnostic[] = [
|
|
73
|
+
{
|
|
74
|
+
start: errorOffset,
|
|
75
|
+
length: 1,
|
|
76
|
+
category: 1, // Error
|
|
77
|
+
code: 2339,
|
|
78
|
+
messageText: "Property 'b' does not exist on type",
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
const mapped = mapWorkerDiagnostics({
|
|
82
|
+
diagnostics,
|
|
83
|
+
validationText: text,
|
|
84
|
+
prependedLineCount,
|
|
85
|
+
});
|
|
86
|
+
expect(mapped).toHaveLength(1);
|
|
87
|
+
expect(mapped[0]).toMatchObject({
|
|
88
|
+
severity: "error",
|
|
89
|
+
line: 1,
|
|
90
|
+
message: "Property 'b' does not exist on type",
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("drops ignored codes (lazy-ATA module resolution, top-level return)", () => {
|
|
95
|
+
const diagnostics: RawTsDiagnostic[] = [
|
|
96
|
+
{ start: errorOffset, category: 1, code: 2307, messageText: "Cannot find module 'lodash'" },
|
|
97
|
+
{ start: errorOffset, category: 1, code: 1108, messageText: "A 'return' statement" },
|
|
98
|
+
];
|
|
99
|
+
expect(
|
|
100
|
+
mapWorkerDiagnostics({ diagnostics, validationText: text, prependedLineCount }),
|
|
101
|
+
).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("drops diagnostics that land inside the prepended type-def prefix", () => {
|
|
105
|
+
const diagnostics: RawTsDiagnostic[] = [
|
|
106
|
+
{ start: 0, category: 1, code: 2300, messageText: "noise in generated types" },
|
|
107
|
+
];
|
|
108
|
+
expect(
|
|
109
|
+
mapWorkerDiagnostics({ diagnostics, validationText: text, prependedLineCount }),
|
|
110
|
+
).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("drops non-error/-warning categories and unpositioned diagnostics", () => {
|
|
114
|
+
const diagnostics: RawTsDiagnostic[] = [
|
|
115
|
+
{ start: errorOffset, category: 2, code: 9999, messageText: "suggestion" },
|
|
116
|
+
{ category: 1, code: 2339, messageText: "global, no position" },
|
|
117
|
+
];
|
|
118
|
+
expect(
|
|
119
|
+
mapWorkerDiagnostics({ diagnostics, validationText: text, prependedLineCount }),
|
|
120
|
+
).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("keeps warnings", () => {
|
|
124
|
+
const diagnostics: RawTsDiagnostic[] = [
|
|
125
|
+
{ start: errorOffset, category: 0, code: 6133, messageText: "'z' is declared but never read" },
|
|
126
|
+
];
|
|
127
|
+
const mapped = mapWorkerDiagnostics({
|
|
128
|
+
diagnostics,
|
|
129
|
+
validationText: text,
|
|
130
|
+
prependedLineCount,
|
|
131
|
+
});
|
|
132
|
+
expect(mapped).toHaveLength(1);
|
|
133
|
+
expect(mapped[0]?.severity).toBe("warning");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Pure helpers for the headless script validator (`validateScripts.ts`).
|
|
2
|
+
//
|
|
3
|
+
// Kept free of any `monaco` / browser import so this logic is unit-testable
|
|
4
|
+
// under bun. The browser-only worker glue lives in `validateScripts.ts` and
|
|
5
|
+
// composes these helpers.
|
|
6
|
+
//
|
|
7
|
+
// Strategy: to type-check a user script against its generated `context.d.ts`
|
|
8
|
+
// WITHOUT polluting the shared TS service's global scope (which would collide
|
|
9
|
+
// with any mounted editor's own `declare const context`), we PREPEND the
|
|
10
|
+
// generated type declarations onto the user's source and validate that single
|
|
11
|
+
// combined file. Inside one module/script file the prepended `declare const
|
|
12
|
+
// context` is in scope for the user's code below it, but it never leaks to
|
|
13
|
+
// other files. Diagnostics that land in the prepended region are dropped, and
|
|
14
|
+
// the rest are shifted back by the number of prepended lines so positions map
|
|
15
|
+
// onto the user's original source.
|
|
16
|
+
|
|
17
|
+
/** A type error/warning located in the user's original (un-prepended) source. */
|
|
18
|
+
export interface ScriptDiagnostic {
|
|
19
|
+
severity: "error" | "warning";
|
|
20
|
+
/** Flattened, human-readable message. */
|
|
21
|
+
message: string;
|
|
22
|
+
/** 1-based line in the user's source. */
|
|
23
|
+
line: number;
|
|
24
|
+
/** 1-based column. */
|
|
25
|
+
column: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Diagnostic codes ignored during headless validation because they reflect the
|
|
30
|
+
* sandbox/loading model rather than a real mistake in the user's logic:
|
|
31
|
+
*
|
|
32
|
+
* - 1108: a top-level `return` is legal (the runtime wraps scripts in an async
|
|
33
|
+
* IIFE) - same suppression the editor applies.
|
|
34
|
+
* - 2307 / 2792: "cannot find module 'x'". Type acquisition (ATA) is lazy and
|
|
35
|
+
* only runs for the editor that is open, so a collapsed-card script's
|
|
36
|
+
* imports have no fetched types yet. Flagging these would be a false
|
|
37
|
+
* positive, so module-resolution failures are not surfaced here (the
|
|
38
|
+
* backend typecheck - deferred - is the place to enforce imports).
|
|
39
|
+
* - 7016: "could not find a declaration file for module 'x'" - same reason.
|
|
40
|
+
*/
|
|
41
|
+
export const IGNORED_DIAGNOSTIC_CODES: ReadonlySet<number> = new Set([
|
|
42
|
+
1108, 2307, 2792, 7016,
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// `ts.DiagnosticCategory`: Warning = 0, Error = 1, Suggestion = 2, Message = 3.
|
|
46
|
+
const CATEGORY_ERROR = 1;
|
|
47
|
+
const CATEGORY_WARNING = 0;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Minimal shape of a TypeScript worker diagnostic (subset of monaco's
|
|
51
|
+
* `Diagnostic`). `messageText` is either a string or a nested chain.
|
|
52
|
+
*/
|
|
53
|
+
export interface RawTsDiagnostic {
|
|
54
|
+
start?: number;
|
|
55
|
+
length?: number;
|
|
56
|
+
messageText: string | DiagnosticMessageChain;
|
|
57
|
+
category: number;
|
|
58
|
+
code: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DiagnosticMessageChain {
|
|
62
|
+
messageText: string;
|
|
63
|
+
next?: DiagnosticMessageChain[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Flatten a (possibly nested) diagnostic message chain to a single string. */
|
|
67
|
+
export function flattenDiagnosticMessage(
|
|
68
|
+
messageText: string | DiagnosticMessageChain,
|
|
69
|
+
): string {
|
|
70
|
+
if (typeof messageText === "string") {
|
|
71
|
+
return messageText;
|
|
72
|
+
}
|
|
73
|
+
const parts: string[] = [];
|
|
74
|
+
const walk = (chain: DiagnosticMessageChain): void => {
|
|
75
|
+
parts.push(chain.messageText);
|
|
76
|
+
for (const child of chain.next ?? []) {
|
|
77
|
+
walk(child);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
walk(messageText);
|
|
81
|
+
return parts.join(" ");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Convert a 0-based character offset into a 1-based {line, column}. */
|
|
85
|
+
export function offsetToPosition(
|
|
86
|
+
text: string,
|
|
87
|
+
offset: number,
|
|
88
|
+
): { line: number; column: number } {
|
|
89
|
+
let line = 1;
|
|
90
|
+
let column = 1;
|
|
91
|
+
const end = Math.min(offset, text.length);
|
|
92
|
+
for (let i = 0; i < end; i++) {
|
|
93
|
+
if (text[i] === "\n") {
|
|
94
|
+
line += 1;
|
|
95
|
+
column = 1;
|
|
96
|
+
} else {
|
|
97
|
+
column += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { line, column };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the combined source to validate: the generated `typeDefinitions`
|
|
105
|
+
* (the `declare const context` + any ambient augmentations) prepended to the
|
|
106
|
+
* user's source. Returns the combined text plus the number of lines the prefix
|
|
107
|
+
* occupies, so diagnostics can be shifted back onto the user's source.
|
|
108
|
+
*/
|
|
109
|
+
export function buildValidationSource({
|
|
110
|
+
typeDefinitions,
|
|
111
|
+
source,
|
|
112
|
+
}: {
|
|
113
|
+
typeDefinitions: string;
|
|
114
|
+
source: string;
|
|
115
|
+
}): { text: string; prependedLineCount: number } {
|
|
116
|
+
// The prefix occupies one line per line of `typeDefinitions`; the trailing
|
|
117
|
+
// "\n" we add then places the user's source on the next line. So the user's
|
|
118
|
+
// line N maps to combined line N + prependedLineCount.
|
|
119
|
+
const prependedLineCount = typeDefinitions.split("\n").length;
|
|
120
|
+
return {
|
|
121
|
+
text: `${typeDefinitions}\n${source}`,
|
|
122
|
+
prependedLineCount,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Map raw worker diagnostics onto the user's source: drop ignored codes,
|
|
128
|
+
* non-error/-warning categories, unpositioned (global) diagnostics, and any
|
|
129
|
+
* that fall inside the prepended type-definition prefix; shift the rest back.
|
|
130
|
+
*/
|
|
131
|
+
export function mapWorkerDiagnostics({
|
|
132
|
+
diagnostics,
|
|
133
|
+
validationText,
|
|
134
|
+
prependedLineCount,
|
|
135
|
+
}: {
|
|
136
|
+
diagnostics: RawTsDiagnostic[];
|
|
137
|
+
validationText: string;
|
|
138
|
+
prependedLineCount: number;
|
|
139
|
+
}): ScriptDiagnostic[] {
|
|
140
|
+
const result: ScriptDiagnostic[] = [];
|
|
141
|
+
for (const diagnostic of diagnostics) {
|
|
142
|
+
if (IGNORED_DIAGNOSTIC_CODES.has(diagnostic.code)) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const severity =
|
|
146
|
+
diagnostic.category === CATEGORY_ERROR
|
|
147
|
+
? "error"
|
|
148
|
+
: diagnostic.category === CATEGORY_WARNING
|
|
149
|
+
? "warning"
|
|
150
|
+
: undefined;
|
|
151
|
+
if (severity === undefined) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (diagnostic.start === undefined) {
|
|
155
|
+
// No position - a whole-file diagnostic. Not attributable to a user line,
|
|
156
|
+
// and the inline strategy shouldn't produce these; skip.
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const position = offsetToPosition(validationText, diagnostic.start);
|
|
160
|
+
if (position.line <= prependedLineCount) {
|
|
161
|
+
// Lands in the generated prefix, not the user's code.
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
result.push({
|
|
165
|
+
severity,
|
|
166
|
+
message: flattenDiagnosticMessage(diagnostic.messageText),
|
|
167
|
+
line: position.line - prependedLineCount,
|
|
168
|
+
column: position.column,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Shared bits for template-aware language validation (monaco -> @typefox
|
|
2
|
+
// migration). Markup editors (json / yaml / xml) hold a TEMPLATE that renders
|
|
3
|
+
// to the target language, not the language itself: a value can be templated in
|
|
4
|
+
// any position, including unquoted ones (e.g. a number `timeout: {{x}}`). So we
|
|
5
|
+
// validate "is this valid once the templates are filled in?" - substitute each
|
|
6
|
+
// `{{ ... }}` with a same-length neutral value, then parse with a real parser.
|
|
7
|
+
//
|
|
8
|
+
// Per-language validators live in validate{Json,Yaml,Xml}Template.ts and all
|
|
9
|
+
// return TemplateDiagnostic[]; the editor maps offsets to monaco markers.
|
|
10
|
+
|
|
11
|
+
export interface TemplateDiagnostic {
|
|
12
|
+
/** 0-based character offset into the ORIGINAL text. */
|
|
13
|
+
offset: number;
|
|
14
|
+
/** Length of the offending span (>= 1). */
|
|
15
|
+
length: number;
|
|
16
|
+
/** Human-readable message. */
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// `[^{}]*` (not `[^}]*`) so an unclosed `{{` can't swallow up to a later `}}`.
|
|
21
|
+
const TEMPLATE_PATTERN = /\{\{[^{}]*\}\}/g;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Replace each `{{ ... }}` with `0` padded to the same length with spaces - a
|
|
25
|
+
* neutral value token, valid in value positions (a number followed by
|
|
26
|
+
* whitespace) and harmless inside strings/text. Same length keeps byte offsets
|
|
27
|
+
* identical, so parser offsets map 1:1 onto the original text.
|
|
28
|
+
*/
|
|
29
|
+
export const substituteTemplates = (text: string): string =>
|
|
30
|
+
text.replaceAll(
|
|
31
|
+
TEMPLATE_PATTERN,
|
|
32
|
+
(match) => `0${" ".repeat(match.length - 1)}`,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** Convert a 1-based line/column (some parsers report this) to a 0-based offset. */
|
|
36
|
+
export const lineColumnToOffset = ({
|
|
37
|
+
text,
|
|
38
|
+
line,
|
|
39
|
+
column,
|
|
40
|
+
}: {
|
|
41
|
+
text: string;
|
|
42
|
+
line: number;
|
|
43
|
+
column: number;
|
|
44
|
+
}): number => {
|
|
45
|
+
const lines = text.split("\n");
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (let index = 0; index < line - 1 && index < lines.length; index += 1) {
|
|
48
|
+
offset += (lines[index]?.length ?? 0) + 1; // +1 for the newline
|
|
49
|
+
}
|
|
50
|
+
return offset + Math.max(column - 1, 0);
|
|
51
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Shared, pure type definitions for the CodeEditor public API.
|
|
2
|
+
//
|
|
3
|
+
// NOTE: this file MUST stay free of monaco / @codingame / vite imports so that
|
|
4
|
+
// SSR/test/node paths can reference these types (and the package barrel can
|
|
5
|
+
// re-export them) without pulling in the browser-only editor stack.
|
|
6
|
+
|
|
7
|
+
export type CodeEditorLanguage =
|
|
8
|
+
| "json"
|
|
9
|
+
| "yaml"
|
|
10
|
+
| "xml"
|
|
11
|
+
| "markdown"
|
|
12
|
+
| "javascript"
|
|
13
|
+
| "typescript"
|
|
14
|
+
| "shell";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A single payload property available for templating. Used by both the
|
|
18
|
+
* Monaco-backed `CodeEditor` and the lightweight single-line
|
|
19
|
+
* `TemplateValueInput` for the simple "insert a `{{ path }}` reference" flow.
|
|
20
|
+
*/
|
|
21
|
+
export interface TemplateProperty {
|
|
22
|
+
/** Full canonical path to the property, e.g., "trigger.payload.title". */
|
|
23
|
+
path: string;
|
|
24
|
+
/**
|
|
25
|
+
* Runtime-parseable `{{ }}` insertion text, e.g.
|
|
26
|
+
* `artifacts["integration-jira.issue"].issueKey`. When present this is
|
|
27
|
+
* what gets inserted; consumers fall back to `path` when it's absent.
|
|
28
|
+
*/
|
|
29
|
+
templateRef?: string;
|
|
30
|
+
/** Type label rendered on the right, e.g. "string", "number". */
|
|
31
|
+
type: string;
|
|
32
|
+
/** Optional description of the property. */
|
|
33
|
+
description?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Known discrete values for this field, when the schema declares an `enum`.
|
|
36
|
+
* Consumed by the template-completion provider to suggest concrete values
|
|
37
|
+
* after a comparator (e.g. `"low"` / `"high"` for a severity field).
|
|
38
|
+
*/
|
|
39
|
+
enumValues?: Array<string | number | boolean>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A single environment variable available to shell scripts. Surfaced as a
|
|
44
|
+
* completion item after the user types `$` or `${` in shell mode.
|
|
45
|
+
*/
|
|
46
|
+
export interface ShellEnvVar {
|
|
47
|
+
/** Variable name without the leading `$`, e.g. `EVENT_ID`. */
|
|
48
|
+
name: string;
|
|
49
|
+
/** Optional description shown in the completion popup. */
|
|
50
|
+
description?: string;
|
|
51
|
+
/** Optional example value shown in the completion item detail. */
|
|
52
|
+
example?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One declaration file returned by an `AcquireTypes` resolver. */
|
|
56
|
+
export interface AcquiredTypeFile {
|
|
57
|
+
/**
|
|
58
|
+
* Real `node_modules/...`-relative path (e.g.
|
|
59
|
+
* `node_modules/@types/lodash/index.d.ts`). Registered at `file:///<path>`
|
|
60
|
+
* so TypeScript's NodeJs + `@types` resolution can find it.
|
|
61
|
+
*/
|
|
62
|
+
path: string;
|
|
63
|
+
/** Verbatim declaration content (UNWRAPPED — no `declare module` envelope). */
|
|
64
|
+
content: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolver for lazy Automatic Type Acquisition (ATA). Given a bare package
|
|
69
|
+
* specifier (e.g. `lodash`), returns the declaration-file closure to register
|
|
70
|
+
* with the TypeScript service (own types and/or the `@types/*` companion).
|
|
71
|
+
* Returns an empty array when the package has no acquirable types. Plugin-
|
|
72
|
+
* agnostic: the concrete fetch (route URL + lockfile hash + auth) is injected
|
|
73
|
+
* by the consumer (see `@checkstack/script-packages-frontend`).
|
|
74
|
+
*/
|
|
75
|
+
export type AcquireTypes = (
|
|
76
|
+
specifier: string,
|
|
77
|
+
) => Promise<AcquiredTypeFile[]>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* An externally-supplied diagnostic to render as an inline squiggle. Positions
|
|
81
|
+
* are 1-based line/column (the editor's convention). Callers compute these from
|
|
82
|
+
* their own validation (e.g. mapping a definition issue path back to a YAML
|
|
83
|
+
* node range) and pass them via `markers`.
|
|
84
|
+
*/
|
|
85
|
+
export interface EditorMarker {
|
|
86
|
+
startLineNumber: number;
|
|
87
|
+
startColumn: number;
|
|
88
|
+
endLineNumber: number;
|
|
89
|
+
endColumn: number;
|
|
90
|
+
message: string;
|
|
91
|
+
severity?: "error" | "warning" | "info";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CodeEditorProps {
|
|
95
|
+
/** Unique identifier for the editor. */
|
|
96
|
+
id?: string;
|
|
97
|
+
/** Current value of the editor. */
|
|
98
|
+
value: string;
|
|
99
|
+
/** Callback when the value changes. */
|
|
100
|
+
onChange: (value: string) => void;
|
|
101
|
+
/** Language for syntax highlighting. */
|
|
102
|
+
language?: CodeEditorLanguage;
|
|
103
|
+
/** Minimum height of the editor, as a CSS length (e.g. "240px"). */
|
|
104
|
+
minHeight?: string;
|
|
105
|
+
/** Whether the editor is read-only. */
|
|
106
|
+
readOnly?: boolean;
|
|
107
|
+
/** Placeholder text when empty. */
|
|
108
|
+
placeholder?: string;
|
|
109
|
+
/**
|
|
110
|
+
* TypeScript type definitions to inject for IntelliSense (the `context.d.ts`).
|
|
111
|
+
* Generated from JSON schemas for context-aware autocomplete. For
|
|
112
|
+
* `typescript` / `javascript` editors, non-identifier object keys in these
|
|
113
|
+
* definitions (e.g. artifact ids) are offered as `["key"]` bracket
|
|
114
|
+
* completions automatically.
|
|
115
|
+
*/
|
|
116
|
+
typeDefinitions?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Optional template properties for autocomplete. When provided, typing "{{"
|
|
119
|
+
* triggers autocomplete with the available template variables.
|
|
120
|
+
*/
|
|
121
|
+
templateProperties?: TemplateProperty[];
|
|
122
|
+
/**
|
|
123
|
+
* Optional environment-variable hints for shell mode. When provided and
|
|
124
|
+
* `language === "shell"`, they autocomplete after `$` and `${`.
|
|
125
|
+
*/
|
|
126
|
+
shellEnvVars?: ShellEnvVar[];
|
|
127
|
+
/**
|
|
128
|
+
* Externally-computed diagnostics rendered as inline squiggles (under a
|
|
129
|
+
* dedicated marker owner, so they coexist with the editor's own language
|
|
130
|
+
* diagnostics).
|
|
131
|
+
*/
|
|
132
|
+
markers?: EditorMarker[];
|
|
133
|
+
/**
|
|
134
|
+
* Lazy Automatic Type Acquisition resolver. When provided (TS/JS editors),
|
|
135
|
+
* the editor parses bare `import`/`require` specifiers from the buffer and
|
|
136
|
+
* calls this for each NEW package, registering the returned declaration
|
|
137
|
+
* files so `import { x } from "pkg"` autocompletes. Injected by the
|
|
138
|
+
* consumer so `@checkstack/ui` stays plugin-agnostic.
|
|
139
|
+
*/
|
|
140
|
+
acquireTypes?: AcquireTypes;
|
|
141
|
+
/**
|
|
142
|
+
* Identity of the current package install (the lockfile hash). When it
|
|
143
|
+
* changes (a new install), the editor resets its acquired-set so types
|
|
144
|
+
* refresh against the new install.
|
|
145
|
+
*/
|
|
146
|
+
acquireResetKey?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Importable installed package NAMES (TS/JS editors). When provided, the
|
|
149
|
+
* editor suggests these while the cursor is inside an import specifier
|
|
150
|
+
* string (`import {} from "lod"` -> `lodash`), solving the lazy-ATA
|
|
151
|
+
* catch-22 where no module is registered until its name is typed. Must
|
|
152
|
+
* already exclude `@types/*` companions. Injected by the consumer.
|
|
153
|
+
*/
|
|
154
|
+
importablePackages?: string[];
|
|
155
|
+
/**
|
|
156
|
+
* Whether to show the "expand editor" affordance that opens the editor in a
|
|
157
|
+
* large full-screen overlay for comfortably editing big scripts. Defaults to
|
|
158
|
+
* `true`. Set `false` to suppress it (e.g. for tiny single-line snippets).
|
|
159
|
+
*/
|
|
160
|
+
allowPopout?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Optional override for the overlay dialog title. When omitted, the title is
|
|
163
|
+
* derived from `language` (e.g. "Edit script - TypeScript"). Lets a consumer
|
|
164
|
+
* surface a field-specific label (e.g. a DynamicForm field name) while
|
|
165
|
+
* keeping `@checkstack/ui` plugin-agnostic.
|
|
166
|
+
*/
|
|
167
|
+
title?: string;
|
|
168
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { validateJsonTemplate } from "./validateJsonTemplate";
|
|
3
|
+
|
|
4
|
+
describe("validateJsonTemplate", () => {
|
|
5
|
+
it("accepts valid JSON", () => {
|
|
6
|
+
expect(validateJsonTemplate('{"a": 1, "b": "two"}')).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("accepts empty content", () => {
|
|
10
|
+
expect(validateJsonTemplate("")).toEqual([]);
|
|
11
|
+
expect(validateJsonTemplate(" \n ")).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("accepts a template as a quoted string value", () => {
|
|
15
|
+
expect(
|
|
16
|
+
validateJsonTemplate('{"repo": "{{trigger.payload.repository}}"}'),
|
|
17
|
+
).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("accepts a template embedded inside a string", () => {
|
|
21
|
+
expect(validateJsonTemplate('{"msg": "build {{id}} done"}')).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("accepts an UNQUOTED template value (e.g. a number)", () => {
|
|
25
|
+
// This is the case a plain JSON validator would reject.
|
|
26
|
+
expect(
|
|
27
|
+
validateJsonTemplate('{"timeout": {{trigger.payload.timeout}}}'),
|
|
28
|
+
).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("accepts a template as an array element", () => {
|
|
32
|
+
expect(validateJsonTemplate('{"items": [{{count}}, 2]}')).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("flags a genuine structural error (missing comma)", () => {
|
|
36
|
+
const diagnostics = validateJsonTemplate('{"a": 1 "b": 2}');
|
|
37
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("flags an unclosed object", () => {
|
|
41
|
+
const diagnostics = validateJsonTemplate('{"a": 1');
|
|
42
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("flags a genuine error even when templates are present, at the right offset", () => {
|
|
46
|
+
// The `2` is missing its preceding colon; offset must point into the
|
|
47
|
+
// ORIGINAL text (same-length substitution preserves offsets).
|
|
48
|
+
const text = '{"a": {{x}}, "b" 2}';
|
|
49
|
+
const diagnostics = validateJsonTemplate(text);
|
|
50
|
+
expect(diagnostics.length).toBeGreaterThan(0);
|
|
51
|
+
// The error should land at/after the `2`, not inside the template.
|
|
52
|
+
const firstOffset = diagnostics[0]?.offset ?? -1;
|
|
53
|
+
expect(text.slice(firstOffset, firstOffset + 1)).toBe("2");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("does not let an unclosed `{{` swallow a later template", () => {
|
|
57
|
+
// `{{` on its own is not a complete template, so substitution leaves it;
|
|
58
|
+
// the real `{{y}}` value is still substituted and the JSON stays valid.
|
|
59
|
+
expect(validateJsonTemplate('{"a": "{{", "b": {{y}}}')).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|