@checkstack/ui 1.8.2 → 1.9.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 +69 -0
- package/package.json +6 -2
- package/scripts/generate-stdlib-types.ts +90 -0
- package/src/components/CodeEditor/CodeEditor.tsx +7 -0
- package/src/components/CodeEditor/MonacoEditor.tsx +203 -117
- package/src/components/CodeEditor/generateTypeDefinitions.ts +19 -26
- package/src/components/CodeEditor/generated/stdlib-types.json +1 -0
- package/src/components/CodeEditor/index.ts +7 -0
- package/src/components/CodeEditor/monacoStdlib.ts +62 -0
- package/src/components/CodeEditor/monacoWorkers.ts +118 -0
- package/src/components/CodeEditor/scriptContext.test.ts +280 -0
- package/src/components/CodeEditor/scriptContext.ts +467 -0
- package/src/components/CodeEditor/shellEnvVarMatcher.test.ts +95 -0
- package/src/components/CodeEditor/shellEnvVarMatcher.ts +70 -0
- package/src/components/DynamicForm/DynamicForm.tsx +6 -0
- package/src/components/DynamicForm/FormField.tsx +15 -0
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +111 -6
- package/src/components/DynamicForm/index.ts +2 -0
- package/src/components/DynamicForm/starterTemplateSelector.test.ts +96 -0
- package/src/components/DynamicForm/starterTemplateSelector.ts +32 -0
- package/src/components/DynamicForm/types.ts +34 -1
- package/src/components/LinksEditor.tsx +69 -34
- package/src/hooks/useInitOnceForKey.test.ts +127 -0
- package/src/hooks/useInitOnceForKey.ts +87 -0
- package/src/index.ts +1 -0
- package/tsconfig.json +3 -1
|
@@ -3,9 +3,16 @@ export {
|
|
|
3
3
|
type CodeEditorProps,
|
|
4
4
|
type CodeEditorLanguage,
|
|
5
5
|
type TemplateProperty,
|
|
6
|
+
type ShellEnvVar,
|
|
6
7
|
} from "./CodeEditor";
|
|
7
8
|
|
|
8
9
|
export {
|
|
9
10
|
generateTypeDefinitions,
|
|
10
11
|
type GenerateTypesOptions,
|
|
11
12
|
} from "./generateTypeDefinitions";
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
healthcheckScriptContext,
|
|
16
|
+
integrationScriptContext,
|
|
17
|
+
type ScriptEditorContext,
|
|
18
|
+
} from "./scriptContext";
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
/**
|
|
3
|
+
* Eager, module-load-time Monaco bootstrap. Three things happen here,
|
|
4
|
+
* in this exact order, before any `<CodeEditor>` instance can mount:
|
|
5
|
+
*
|
|
6
|
+
* 1. Bundle the per-language web workers via Vite's `?worker` import
|
|
7
|
+
* suffix and install them via `MonacoEnvironment.getWorker`.
|
|
8
|
+
* Without locally-bundled workers, the loader's default CDN path
|
|
9
|
+
* sometimes fails silently (CORS on the worker scripts) and the
|
|
10
|
+
* TS service falls back to no-language-service mode.
|
|
11
|
+
*
|
|
12
|
+
* 2. Configure the TypeScript language service — compiler options,
|
|
13
|
+
* diagnostics options, eager-model-sync. The TS service in
|
|
14
|
+
* monaco-editor is a singleton that initialises lazily the FIRST
|
|
15
|
+
* time it sees a TS/JS model. Doing this config inside a
|
|
16
|
+
* per-mount `onMount` (the previous shape) meant a shell editor
|
|
17
|
+
* mounted first would start the service with defaults, and the
|
|
18
|
+
* later TS editor's config arrived too late.
|
|
19
|
+
*
|
|
20
|
+
* 3. Tell `@monaco-editor/react`'s loader to use our locally-bundled
|
|
21
|
+
* Monaco instance via `loader.config({ monaco })`. Without this
|
|
22
|
+
* the loader pulls Monaco from a CDN at runtime — which defeats
|
|
23
|
+
* the worker setup above.
|
|
24
|
+
*
|
|
25
|
+
* This file is imported as a SIDE EFFECT from MonacoEditor.tsx, so the
|
|
26
|
+
* setup runs on the first import of CodeEditor regardless of which
|
|
27
|
+
* language ends up being mounted first.
|
|
28
|
+
*/
|
|
29
|
+
import * as monaco from "monaco-editor";
|
|
30
|
+
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
|
31
|
+
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
|
|
32
|
+
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
|
|
33
|
+
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
|
|
34
|
+
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
|
|
35
|
+
import { loader } from "@monaco-editor/react";
|
|
36
|
+
|
|
37
|
+
// ─── 1. Workers ──────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
interface MonacoEnvironmentLike {
|
|
40
|
+
getWorker(workerId: string, label: string): Worker;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
(
|
|
44
|
+
globalThis as unknown as { MonacoEnvironment: MonacoEnvironmentLike }
|
|
45
|
+
).MonacoEnvironment = {
|
|
46
|
+
getWorker(_workerId: string, label: string): Worker {
|
|
47
|
+
switch (label) {
|
|
48
|
+
case "json": {
|
|
49
|
+
return new jsonWorker();
|
|
50
|
+
}
|
|
51
|
+
case "css":
|
|
52
|
+
case "scss":
|
|
53
|
+
case "less": {
|
|
54
|
+
return new cssWorker();
|
|
55
|
+
}
|
|
56
|
+
case "html":
|
|
57
|
+
case "handlebars":
|
|
58
|
+
case "razor": {
|
|
59
|
+
return new htmlWorker();
|
|
60
|
+
}
|
|
61
|
+
case "typescript":
|
|
62
|
+
case "javascript": {
|
|
63
|
+
return new tsWorker();
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
return new editorWorker();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ─── 2. TypeScript language service ──────────────────────────────────────
|
|
73
|
+
//
|
|
74
|
+
// In monaco-editor 0.55 the canonical access path is `monaco.typescript`
|
|
75
|
+
// (the older `monaco.languages.typescript` is marked
|
|
76
|
+
// `{ deprecated: true }` in the type defs and emits a tombstone object).
|
|
77
|
+
// Both still resolve to the same singleton at runtime; we use the new
|
|
78
|
+
// path for type-safety.
|
|
79
|
+
|
|
80
|
+
const tsDefaults = monaco.typescript.typescriptDefaults;
|
|
81
|
+
const jsDefaults = monaco.typescript.javascriptDefaults;
|
|
82
|
+
|
|
83
|
+
for (const defaults of [tsDefaults, jsDefaults]) {
|
|
84
|
+
defaults.setCompilerOptions({
|
|
85
|
+
target: monaco.typescript.ScriptTarget.ESNext,
|
|
86
|
+
module: monaco.typescript.ModuleKind.ESNext,
|
|
87
|
+
moduleResolution: monaco.typescript.ModuleResolutionKind.NodeJs,
|
|
88
|
+
lib: ["esnext"],
|
|
89
|
+
// Pulls the bundled @types/node + bun-types declarations into the
|
|
90
|
+
// ambient scope, so `process`, `Buffer`, the `Bun` global, etc. are
|
|
91
|
+
// typed without the user needing any `/// <reference>`.
|
|
92
|
+
types: ["node", "bun-types"],
|
|
93
|
+
allowNonTsExtensions: false,
|
|
94
|
+
noEmit: true,
|
|
95
|
+
strict: true,
|
|
96
|
+
esModuleInterop: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Suppress diagnostics that don't apply to our script context:
|
|
100
|
+
// - 1108: "A 'return' statement can only be used within a function body"
|
|
101
|
+
// The runtime wraps legacy scripts in an async IIFE, so a
|
|
102
|
+
// top-level `return X;` is valid in the user's source.
|
|
103
|
+
defaults.setDiagnosticsOptions({
|
|
104
|
+
diagnosticCodesToIgnore: [1108],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Eagerly push models to the TS worker the moment they're created so
|
|
108
|
+
// diagnostics + completions are available on the first keystroke.
|
|
109
|
+
defaults.setEagerModelSync(true);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── 3. Loader ───────────────────────────────────────────────────────────
|
|
113
|
+
//
|
|
114
|
+
// Must be the LAST step: once a `<Editor>` mounts, `loader.init()` is
|
|
115
|
+
// called and config is locked. Pointing at our local `monaco` import
|
|
116
|
+
// bypasses the loader's default AMD/CDN path entirely.
|
|
117
|
+
|
|
118
|
+
loader.config({ monaco });
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
healthcheckScriptContext,
|
|
4
|
+
integrationScriptContext,
|
|
5
|
+
_internals,
|
|
6
|
+
} from "./scriptContext";
|
|
7
|
+
import type { JsonSchemaProperty } from "../DynamicForm/types";
|
|
8
|
+
|
|
9
|
+
describe("healthcheckScriptContext", () => {
|
|
10
|
+
it("emits both the global and module-export declarations of defineHealthCheck", () => {
|
|
11
|
+
const ctx = healthcheckScriptContext({});
|
|
12
|
+
// Module declaration is what `import { defineHealthCheck } from
|
|
13
|
+
// "@checkstack/healthcheck"` resolves to.
|
|
14
|
+
expect(ctx.typeDefinitions).toContain('declare module "@checkstack/healthcheck"');
|
|
15
|
+
expect(ctx.typeDefinitions).toContain("HealthCheckScriptResult");
|
|
16
|
+
// Global declaration is what makes Monaco autocomplete `defineHea…`
|
|
17
|
+
// _without_ requiring the user to type the import first (Monaco 0.55
|
|
18
|
+
// doesn't expose `includeCompletionsForModuleExports`).
|
|
19
|
+
expect(ctx.typeDefinitions).toMatch(/declare function defineHealthCheck/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("typed `context.config` reflects the supplied collector schema", () => {
|
|
23
|
+
const ctx = healthcheckScriptContext({
|
|
24
|
+
collectorConfigSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
host: { type: "string", description: "Target hostname" },
|
|
28
|
+
port: { type: "integer" },
|
|
29
|
+
enabled: { type: "boolean" },
|
|
30
|
+
},
|
|
31
|
+
required: ["host", "port"],
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Property names + descriptions should land in the generated context block.
|
|
36
|
+
expect(ctx.typeDefinitions).toContain("declare const context");
|
|
37
|
+
expect(ctx.typeDefinitions).toContain("readonly host: string");
|
|
38
|
+
expect(ctx.typeDefinitions).toContain("readonly port: number");
|
|
39
|
+
expect(ctx.typeDefinitions).toContain("readonly enabled?: boolean");
|
|
40
|
+
expect(ctx.typeDefinitions).toContain("Target hostname");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("falls back to a generic `Record<string, unknown>` config when no schema is given", () => {
|
|
44
|
+
const ctx = healthcheckScriptContext({});
|
|
45
|
+
// We ALWAYS declare `context` so the function-arg form
|
|
46
|
+
// `defineHealthCheck(ctx => …)` is properly typed too — without a
|
|
47
|
+
// schema, both the global and the function param fall back to a
|
|
48
|
+
// generic config shape.
|
|
49
|
+
expect(ctx.typeDefinitions).toContain("declare const context");
|
|
50
|
+
expect(ctx.typeDefinitions).toContain("Record<string, unknown>");
|
|
51
|
+
expect(ctx.typeDefinitions).toContain("HealthCheckScriptResult");
|
|
52
|
+
expect(ctx.typeDefinitions).toContain("HealthCheckScriptContext");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("types the `defineHealthCheck` callback parameter from the schema (not `unknown`)", () => {
|
|
56
|
+
// Regression guard: the previous version had `(ctx: unknown) => …`,
|
|
57
|
+
// so `ctx.config.host` produced "'ctx' is of type 'unknown'". The
|
|
58
|
+
// callback param must reference the shared HealthCheckScriptContext
|
|
59
|
+
// type that's also used for the global `context`.
|
|
60
|
+
const ctx = healthcheckScriptContext({
|
|
61
|
+
collectorConfigSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: { host: { type: "string" } },
|
|
64
|
+
required: ["host"],
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
expect(ctx.typeDefinitions).toMatch(
|
|
68
|
+
/\(ctx:\s*HealthCheckScriptContext\)/,
|
|
69
|
+
);
|
|
70
|
+
// The shared context type carries the schema-derived field.
|
|
71
|
+
expect(ctx.typeDefinitions).toContain("readonly host: string");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("exposes inline TS + shell starters", () => {
|
|
75
|
+
const ctx = healthcheckScriptContext({});
|
|
76
|
+
expect(ctx.starterTemplates.typescript).toBeDefined();
|
|
77
|
+
expect(ctx.starterTemplates.javascript).toBeDefined();
|
|
78
|
+
expect(ctx.starterTemplates.shell).toBeDefined();
|
|
79
|
+
expect(ctx.starterTemplates.typescript).toContain('import { loadavg } from "node:os"');
|
|
80
|
+
expect(ctx.starterTemplates.typescript).toContain("defineHealthCheck");
|
|
81
|
+
// Portable load-average read via `uptime` (works on Linux + macOS).
|
|
82
|
+
expect(ctx.starterTemplates.shell).toContain("uptime");
|
|
83
|
+
expect(ctx.starterTemplates.shell).toContain("load average");
|
|
84
|
+
expect(ctx.starterTemplates.shell).toContain("awk");
|
|
85
|
+
// Guard against a Linux-only regression: /proc/loadavg doesn't
|
|
86
|
+
// exist on macOS, so the starter must not depend on it.
|
|
87
|
+
expect(ctx.starterTemplates.shell).not.toContain("/proc/loadavg");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("exposes the safe-env-vars whitelist for shell completion", () => {
|
|
91
|
+
const ctx = healthcheckScriptContext({});
|
|
92
|
+
const names = ctx.shellEnvVars.map((v) => v.name);
|
|
93
|
+
// Sanity-check a few entries from the whitelist that scripts will
|
|
94
|
+
// most often actually reference.
|
|
95
|
+
expect(names).toContain("PATH");
|
|
96
|
+
expect(names).toContain("HOME");
|
|
97
|
+
expect(names).toContain("TZ");
|
|
98
|
+
// Integration-only vars must NOT leak into the healthcheck context.
|
|
99
|
+
expect(names).not.toContain("EVENT_ID");
|
|
100
|
+
expect(names).not.toContain("PAYLOAD_TITLE");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("integrationScriptContext", () => {
|
|
105
|
+
it("emits both the global and module-export declarations of defineIntegration", () => {
|
|
106
|
+
const ctx = integrationScriptContext({});
|
|
107
|
+
expect(ctx.typeDefinitions).toContain('declare module "@checkstack/integration"');
|
|
108
|
+
expect(ctx.typeDefinitions).toContain("IntegrationScriptResult");
|
|
109
|
+
// Global form — analogous to defineHealthCheck.
|
|
110
|
+
expect(ctx.typeDefinitions).toMatch(/declare function defineIntegration/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("typed `context.event.payload` reflects the supplied payload schema", () => {
|
|
114
|
+
const ctx = integrationScriptContext({
|
|
115
|
+
eventPayloadSchema: {
|
|
116
|
+
type: "object",
|
|
117
|
+
properties: {
|
|
118
|
+
title: { type: "string" },
|
|
119
|
+
severity: { type: "string", enum: ["low", "high"] },
|
|
120
|
+
},
|
|
121
|
+
required: ["title"],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(ctx.typeDefinitions).toContain("readonly title: string");
|
|
126
|
+
// Enum should narrow to the literal union.
|
|
127
|
+
expect(ctx.typeDefinitions).toContain('"low" | "high"');
|
|
128
|
+
expect(ctx.typeDefinitions).toContain("declare const context");
|
|
129
|
+
expect(ctx.typeDefinitions).toContain("eventId");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("types the `defineIntegration` callback parameter from the schema (not `unknown`)", () => {
|
|
133
|
+
// Regression guard for the screenshot the user sent: typing
|
|
134
|
+
// `context.event.eventId` inside `defineIntegration(async (context)
|
|
135
|
+
// => …)` produced "'context' is of type 'unknown'" because the
|
|
136
|
+
// virtual module declared the param as `unknown`. Must reference
|
|
137
|
+
// the shared `IntegrationScriptContext` type instead.
|
|
138
|
+
const ctx = integrationScriptContext({
|
|
139
|
+
eventPayloadSchema: {
|
|
140
|
+
type: "object",
|
|
141
|
+
properties: { title: { type: "string" } },
|
|
142
|
+
required: ["title"],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
expect(ctx.typeDefinitions).toMatch(
|
|
146
|
+
/\(ctx:\s*IntegrationScriptContext\)/,
|
|
147
|
+
);
|
|
148
|
+
// The shared context type carries the schema-derived payload field.
|
|
149
|
+
expect(ctx.typeDefinitions).toContain("readonly title: string");
|
|
150
|
+
// And the always-present event metadata.
|
|
151
|
+
expect(ctx.typeDefinitions).toContain("readonly eventId: string");
|
|
152
|
+
expect(ctx.typeDefinitions).toContain("readonly deliveryId: string");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("falls back to `Record<string, unknown>` payload when no schema is given", () => {
|
|
156
|
+
const ctx = integrationScriptContext({});
|
|
157
|
+
expect(ctx.typeDefinitions).toContain("declare const context");
|
|
158
|
+
expect(ctx.typeDefinitions).toContain("IntegrationScriptContext");
|
|
159
|
+
expect(ctx.typeDefinitions).toContain("Record<string, unknown>");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("flattens nested payload schemas into PAYLOAD_* shell env-var hints", () => {
|
|
163
|
+
const ctx = integrationScriptContext({
|
|
164
|
+
eventPayloadSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
title: { type: "string", description: "Incident title" },
|
|
168
|
+
severity: { type: "string" },
|
|
169
|
+
metadata: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
region: { type: "string" },
|
|
173
|
+
host: { type: "string" },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const names = ctx.shellEnvVars.map((v) => v.name);
|
|
181
|
+
// Top-level scalars are flattened with the PAYLOAD_ prefix.
|
|
182
|
+
expect(names).toContain("PAYLOAD_TITLE");
|
|
183
|
+
expect(names).toContain("PAYLOAD_SEVERITY");
|
|
184
|
+
// Nested objects are recursively flattened with underscore-joined keys.
|
|
185
|
+
expect(names).toContain("PAYLOAD_METADATA_REGION");
|
|
186
|
+
expect(names).toContain("PAYLOAD_METADATA_HOST");
|
|
187
|
+
// The intermediate object key itself is NOT emitted (only its leaves).
|
|
188
|
+
expect(names).not.toContain("PAYLOAD_METADATA");
|
|
189
|
+
|
|
190
|
+
const title = ctx.shellEnvVars.find((v) => v.name === "PAYLOAD_TITLE");
|
|
191
|
+
expect(title?.description).toBe("Incident title");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("always includes the platform-injected core integration env vars", () => {
|
|
195
|
+
const ctx = integrationScriptContext({});
|
|
196
|
+
const names = ctx.shellEnvVars.map((v) => v.name);
|
|
197
|
+
expect(names).toContain("EVENT_ID");
|
|
198
|
+
expect(names).toContain("EVENT_TIMESTAMP");
|
|
199
|
+
expect(names).toContain("DELIVERY_ID");
|
|
200
|
+
expect(names).toContain("SUBSCRIPTION_ID");
|
|
201
|
+
expect(names).toContain("SUBSCRIPTION_NAME");
|
|
202
|
+
// Safe-vars are also still present.
|
|
203
|
+
expect(names).toContain("PATH");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("exposes inline TS + shell starters that reference the right variables", () => {
|
|
207
|
+
const ctx = integrationScriptContext({});
|
|
208
|
+
expect(ctx.starterTemplates.typescript).toContain("defineIntegration");
|
|
209
|
+
expect(ctx.starterTemplates.typescript).toContain("context.event.eventId");
|
|
210
|
+
expect(ctx.starterTemplates.shell).toContain("$EVENT_ID");
|
|
211
|
+
expect(ctx.starterTemplates.shell).toContain("$PAYLOAD_TITLE");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("falls back gracefully when the payload schema isn't available", () => {
|
|
215
|
+
const ctx = integrationScriptContext({});
|
|
216
|
+
// No `PAYLOAD_*` hints yet, but the core vars and starters are usable.
|
|
217
|
+
const payloadVars = ctx.shellEnvVars.filter((v) =>
|
|
218
|
+
v.name.startsWith("PAYLOAD_"),
|
|
219
|
+
);
|
|
220
|
+
expect(payloadVars).toHaveLength(0);
|
|
221
|
+
expect(ctx.starterTemplates.typescript).toBeDefined();
|
|
222
|
+
expect(ctx.starterTemplates.shell).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("flattenSchemaToEnvVars (internal)", () => {
|
|
227
|
+
const { flattenSchemaToEnvVars } = _internals;
|
|
228
|
+
|
|
229
|
+
it("returns an empty list for non-object schemas", () => {
|
|
230
|
+
expect(flattenSchemaToEnvVars({ type: "string" })).toEqual([]);
|
|
231
|
+
expect(flattenSchemaToEnvVars({ type: "number" })).toEqual([]);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("normalises field names: uppercases and replaces dots/dashes", () => {
|
|
235
|
+
const schema: JsonSchemaProperty = {
|
|
236
|
+
type: "object",
|
|
237
|
+
properties: {
|
|
238
|
+
"user.email": { type: "string" },
|
|
239
|
+
"first-name": { type: "string" },
|
|
240
|
+
"field!with$weird?chars": { type: "string" },
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const names = flattenSchemaToEnvVars(schema).map((v) => v.name);
|
|
245
|
+
expect(names).toContain("PAYLOAD_USER_EMAIL");
|
|
246
|
+
expect(names).toContain("PAYLOAD_FIRST_NAME");
|
|
247
|
+
// Special characters are stripped, not preserved.
|
|
248
|
+
expect(names).toContain("PAYLOAD_FIELDWITHWEIRDCHARS");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("respects a custom prefix for nested flattening", () => {
|
|
252
|
+
const schema: JsonSchemaProperty = {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
title: { type: "string" },
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const result = flattenSchemaToEnvVars(schema, "EVENT");
|
|
260
|
+
expect(result[0]?.name).toBe("EVENT_TITLE");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("includes example placeholders sized to the property type", () => {
|
|
264
|
+
const schema: JsonSchemaProperty = {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
name: { type: "string" },
|
|
268
|
+
count: { type: "number" },
|
|
269
|
+
on: { type: "boolean" },
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const byName = Object.fromEntries(
|
|
274
|
+
flattenSchemaToEnvVars(schema).map((v) => [v.name, v]),
|
|
275
|
+
);
|
|
276
|
+
expect(byName.PAYLOAD_NAME?.example).toBe("<string>");
|
|
277
|
+
expect(byName.PAYLOAD_COUNT?.example).toBe("<number>");
|
|
278
|
+
expect(byName.PAYLOAD_ON?.example).toBe("<true|false>");
|
|
279
|
+
});
|
|
280
|
+
});
|