@checkstack/ui 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main.ts +43 -0
- package/CHANGELOG.md +326 -0
- package/package.json +23 -18
- package/scripts/generate-stdlib-types.ts +23 -0
- package/src/components/Accordion.tsx +17 -9
- package/src/components/ActionCard.tsx +99 -11
- package/src/components/BrandIcon.tsx +57 -0
- package/src/components/CodeEditor/CodeEditor.tsx +159 -14
- package/src/components/CodeEditor/TypefoxEditor.tsx +537 -168
- package/src/components/CodeEditor/editorTheme.test.ts +41 -0
- package/src/components/CodeEditor/editorTheme.ts +26 -0
- 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/monacoGuard.ts +76 -0
- package/src/components/CodeEditor/monacoTsService.ts +185 -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 +15 -7
- package/src/components/CodeEditor/scriptContext.ts +12 -18
- package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
- package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
- package/src/components/CodeEditor/types.ts +79 -0
- package/src/components/CodeEditor/validateScripts.ts +172 -0
- package/src/components/CodeEditor/vscodeServicesSignal.ts +72 -0
- package/src/components/ConfirmationModal.tsx +7 -1
- package/src/components/Dialog.tsx +32 -11
- package/src/components/DurationInput.tsx +121 -0
- package/src/components/DynamicForm/DynamicForm.tsx +119 -47
- package/src/components/DynamicForm/DynamicOptionsField.tsx +19 -14
- package/src/components/DynamicForm/FormField.tsx +183 -15
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +78 -2
- package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
- package/src/components/DynamicForm/index.ts +20 -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 +134 -1
- package/src/components/DynamicForm/utils.test.ts +38 -0
- package/src/components/DynamicForm/utils.ts +54 -0
- package/src/components/DynamicForm/validation.logic.test.ts +255 -0
- package/src/components/DynamicForm/validation.logic.ts +210 -0
- package/src/components/DynamicIcon.tsx +39 -17
- package/src/components/Markdown.tsx +68 -2
- package/src/components/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/Spinner.tsx +56 -0
- package/src/components/StatusBadge.tsx +78 -0
- package/src/components/StrategyConfigCard.tsx +3 -3
- package/src/components/Tabs.tsx +7 -1
- package/src/components/TimeOfDayInput.tsx +116 -0
- package/src/components/UserMenu.logic.test.ts +37 -0
- package/src/components/UserMenu.logic.ts +30 -0
- package/src/components/UserMenu.tsx +40 -12
- package/src/components/comboboxInteraction.ts +39 -0
- package/src/components/iconRegistry.tsx +27 -0
- package/src/components/portalContainer.ts +24 -0
- package/src/index.ts +7 -0
- package/stories/ActionCard.stories.tsx +60 -0
- package/stories/CodeEditor.stories.tsx +47 -2
- package/stories/DurationInput.stories.tsx +59 -0
- package/stories/Introduction.mdx +1 -1
- package/stories/Markdown.stories.tsx +56 -0
- package/stories/ScriptTestPanel.stories.tsx +106 -0
- package/stories/SecretEnvEditor.stories.tsx +80 -0
- package/stories/Spinner.stories.tsx +90 -0
- package/stories/TimeOfDayInput.stories.tsx +34 -0
- package/tsconfig.json +4 -0
|
@@ -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
|
+
}
|
|
@@ -52,6 +52,30 @@ export interface ShellEnvVar {
|
|
|
52
52
|
example?: string;
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
79
|
/**
|
|
56
80
|
* An externally-supplied diagnostic to render as an inline squiggle. Positions
|
|
57
81
|
* are 1-based line/column (the editor's convention). Callers compute these from
|
|
@@ -106,4 +130,59 @@ export interface CodeEditorProps {
|
|
|
106
130
|
* diagnostics).
|
|
107
131
|
*/
|
|
108
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
|
+
* The running release's `@checkstack/sdk` editor bundle as virtual `.d.ts`
|
|
149
|
+
* files (TS/JS editors). Makes `import { defineHealthCheck } from
|
|
150
|
+
* "@checkstack/sdk/healthcheck"` resolve with real, version-matched types.
|
|
151
|
+
* Fetched live by the consumer so `@checkstack/ui` stays network-agnostic.
|
|
152
|
+
*/
|
|
153
|
+
sdkTypes?: ReadonlyArray<AcquiredTypeFile>;
|
|
154
|
+
/**
|
|
155
|
+
* Release-version reset key for `sdkTypes`. When it changes, the mounted SDK
|
|
156
|
+
* libs reset so the editor never serves stale SDK types after an upgrade.
|
|
157
|
+
*/
|
|
158
|
+
sdkTypesResetKey?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Importable installed package NAMES (TS/JS editors). When provided, the
|
|
161
|
+
* editor suggests these while the cursor is inside an import specifier
|
|
162
|
+
* string (`import {} from "lod"` -> `lodash`), solving the lazy-ATA
|
|
163
|
+
* catch-22 where no module is registered until its name is typed. Must
|
|
164
|
+
* already exclude `@types/*` companions. Injected by the consumer.
|
|
165
|
+
*/
|
|
166
|
+
importablePackages?: string[];
|
|
167
|
+
/**
|
|
168
|
+
* Whether to show the "expand editor" affordance that opens the editor in a
|
|
169
|
+
* large full-screen overlay for comfortably editing big scripts. Defaults to
|
|
170
|
+
* `true`. Set `false` to suppress it (e.g. for tiny single-line snippets).
|
|
171
|
+
*/
|
|
172
|
+
allowPopout?: boolean;
|
|
173
|
+
/**
|
|
174
|
+
* Optional override for the overlay dialog title. When omitted, the title is
|
|
175
|
+
* derived from `language` (e.g. "Edit script - TypeScript"). Lets a consumer
|
|
176
|
+
* surface a field-specific label (e.g. a DynamicForm field name) while
|
|
177
|
+
* keeping `@checkstack/ui` plugin-agnostic.
|
|
178
|
+
*/
|
|
179
|
+
title?: string;
|
|
180
|
+
/**
|
|
181
|
+
* When `true`, this editor never CLAIMS the one-time monaco-vscode cold init -
|
|
182
|
+
* it waits for another (visible) editor to bring the services up, then mounts.
|
|
183
|
+
* Set this for OFFSCREEN/hidden editors (e.g. the automation
|
|
184
|
+
* `ScriptServicesBooter`): a hidden editor's init may never complete, so it
|
|
185
|
+
* must not be the sole initializer. Defaults to `false`.
|
|
186
|
+
*/
|
|
187
|
+
deferInit?: boolean;
|
|
109
188
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Headless TypeScript / JavaScript script validator.
|
|
2
|
+
//
|
|
3
|
+
// Type-checks user scripts against their generated `context` types WITHOUT a
|
|
4
|
+
// mounted editor, so an automation's collapsed-card scripts (or any script the
|
|
5
|
+
// user isn't currently looking at) still surface type errors. It drives the
|
|
6
|
+
// SAME standalone TS worker the editor uses, via off-screen models.
|
|
7
|
+
//
|
|
8
|
+
// Browser-only (imports `monaco` + the worker accessor). The pure mapping /
|
|
9
|
+
// offset logic lives in `scriptDiagnostics.ts` and is unit-tested there; this
|
|
10
|
+
// module is the thin async glue and is intentionally not unit-tested (no DOM /
|
|
11
|
+
// worker under bun).
|
|
12
|
+
//
|
|
13
|
+
// Why prepend instead of an extra-lib: a global `context` extra-lib would
|
|
14
|
+
// collide with any open editor's own `declare const context` (extra-libs are
|
|
15
|
+
// global to the shared service). Prepending the type defs onto each validated
|
|
16
|
+
// source keeps `context` scoped to that one off-screen file. See
|
|
17
|
+
// `buildValidationSource`.
|
|
18
|
+
//
|
|
19
|
+
// The Monaco editor API, the standalone TS worker accessors, and the shared
|
|
20
|
+
// `monacoTsService` setup are imported LAZILY (in-body `await import(...)`)
|
|
21
|
+
// rather than at module scope. This keeps the entire `@codingame/*` stack off
|
|
22
|
+
// the `@checkstack/ui` barrel: `validateTypeScriptSources` is re-exported from
|
|
23
|
+
// the barrel, so a static Monaco import here would ship Monaco to every page
|
|
24
|
+
// that touches the barrel (e.g. the login page). The lazy imports only resolve
|
|
25
|
+
// once an editor has already brought the services up, so they hit an
|
|
26
|
+
// already-loaded chunk and add no extra cost.
|
|
27
|
+
import { areVscodeServicesReady } from "./vscodeServicesSignal";
|
|
28
|
+
import {
|
|
29
|
+
buildValidationSource,
|
|
30
|
+
mapWorkerDiagnostics,
|
|
31
|
+
type RawTsDiagnostic,
|
|
32
|
+
type ScriptDiagnostic,
|
|
33
|
+
} from "./scriptDiagnostics";
|
|
34
|
+
|
|
35
|
+
type MonacoEditorApi = typeof import("@codingame/monaco-vscode-editor-api");
|
|
36
|
+
type TsLanguageFeatures =
|
|
37
|
+
typeof import("@codingame/monaco-vscode-standalone-typescript-language-features");
|
|
38
|
+
|
|
39
|
+
export type { ScriptDiagnostic } from "./scriptDiagnostics";
|
|
40
|
+
|
|
41
|
+
export interface ScriptValidationInput {
|
|
42
|
+
/** Caller-chosen identity; the returned map is keyed by this. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** The user's script source (without any generated type prefix). */
|
|
45
|
+
source: string;
|
|
46
|
+
/** Generated `context.d.ts` (+ ambient augmentations) for this script. */
|
|
47
|
+
typeDefinitions: string;
|
|
48
|
+
language: "typescript" | "javascript";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Monotonic across overlapping validation passes so two passes never reuse the
|
|
52
|
+
// same off-screen model URI (which would race on create/dispose).
|
|
53
|
+
let runCounter = 0;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate each script against its `typeDefinitions`. Returns a map keyed by
|
|
57
|
+
* input `id` to that script's diagnostics (empty array = clean). Never throws:
|
|
58
|
+
* any worker/setup failure resolves to no diagnostics so validation can never
|
|
59
|
+
* break the editor it augments.
|
|
60
|
+
*
|
|
61
|
+
* Runs serially. The inline-prepend strategy removes the shared-state
|
|
62
|
+
* constraint that would otherwise force serialization, but validation is not
|
|
63
|
+
* latency-critical and serial keeps worker load predictable.
|
|
64
|
+
*/
|
|
65
|
+
export async function validateTypeScriptSources({
|
|
66
|
+
sources,
|
|
67
|
+
}: {
|
|
68
|
+
sources: ScriptValidationInput[];
|
|
69
|
+
}): Promise<Map<string, ScriptDiagnostic[]>> {
|
|
70
|
+
const results = new Map<string, ScriptDiagnostic[]>();
|
|
71
|
+
if (sources.length === 0) {
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
// CRITICAL: only proceed once an editor has initialized the monaco-vscode
|
|
75
|
+
// services. Initializing them here would collide with the editor wrapper's
|
|
76
|
+
// one-time init ("Services are already initialized") and break the editor.
|
|
77
|
+
if (!areVscodeServicesReady()) {
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Lazy-load the Monaco stack only now that an editor has brought the services
|
|
82
|
+
// up (keeps these heavy `@codingame/*` modules off the barrel - see the
|
|
83
|
+
// module header). Because services are ready, these chunks are already
|
|
84
|
+
// resolved, so the imports are effectively free here.
|
|
85
|
+
let monaco: MonacoEditorApi;
|
|
86
|
+
let getJavaScriptWorker: TsLanguageFeatures["getJavaScriptWorker"];
|
|
87
|
+
let getTypeScriptWorker: TsLanguageFeatures["getTypeScriptWorker"];
|
|
88
|
+
try {
|
|
89
|
+
const [editorApi, tsLanguageFeatures, { ensureStandaloneStdlib }] =
|
|
90
|
+
await Promise.all([
|
|
91
|
+
import("@codingame/monaco-vscode-editor-api"),
|
|
92
|
+
import(
|
|
93
|
+
"@codingame/monaco-vscode-standalone-typescript-language-features"
|
|
94
|
+
),
|
|
95
|
+
import("./monacoTsService"),
|
|
96
|
+
]);
|
|
97
|
+
monaco = editorApi;
|
|
98
|
+
getJavaScriptWorker = tsLanguageFeatures.getJavaScriptWorker;
|
|
99
|
+
getTypeScriptWorker = tsLanguageFeatures.getTypeScriptWorker;
|
|
100
|
+
await ensureStandaloneStdlib();
|
|
101
|
+
} catch {
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const input of sources) {
|
|
106
|
+
try {
|
|
107
|
+
results.set(
|
|
108
|
+
input.id,
|
|
109
|
+
await validateOne({
|
|
110
|
+
input,
|
|
111
|
+
monaco,
|
|
112
|
+
getJavaScriptWorker,
|
|
113
|
+
getTypeScriptWorker,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
} catch {
|
|
117
|
+
results.set(input.id, []);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function validateOne({
|
|
124
|
+
input,
|
|
125
|
+
monaco,
|
|
126
|
+
getJavaScriptWorker,
|
|
127
|
+
getTypeScriptWorker,
|
|
128
|
+
}: {
|
|
129
|
+
input: ScriptValidationInput;
|
|
130
|
+
monaco: MonacoEditorApi;
|
|
131
|
+
getJavaScriptWorker: TsLanguageFeatures["getJavaScriptWorker"];
|
|
132
|
+
getTypeScriptWorker: TsLanguageFeatures["getTypeScriptWorker"];
|
|
133
|
+
}): Promise<ScriptDiagnostic[]> {
|
|
134
|
+
const { text, prependedLineCount } = buildValidationSource({
|
|
135
|
+
typeDefinitions: input.typeDefinitions,
|
|
136
|
+
source: input.source,
|
|
137
|
+
});
|
|
138
|
+
const ext = input.language === "javascript" ? "js" : "ts";
|
|
139
|
+
const uri = monaco.Uri.parse(
|
|
140
|
+
`file:///script-validation/${runCounter++}.${ext}`,
|
|
141
|
+
);
|
|
142
|
+
monaco.editor.getModel(uri)?.dispose();
|
|
143
|
+
const model = monaco.editor.createModel(text, input.language, uri);
|
|
144
|
+
try {
|
|
145
|
+
const getWorker =
|
|
146
|
+
input.language === "javascript"
|
|
147
|
+
? await getJavaScriptWorker()
|
|
148
|
+
: await getTypeScriptWorker();
|
|
149
|
+
const client = await getWorker(uri);
|
|
150
|
+
const fileName = uri.toString();
|
|
151
|
+
const [syntactic, semantic] = await Promise.all([
|
|
152
|
+
client.getSyntacticDiagnostics(fileName),
|
|
153
|
+
client.getSemanticDiagnostics(fileName),
|
|
154
|
+
]);
|
|
155
|
+
const diagnostics: RawTsDiagnostic[] = [...syntactic, ...semantic].map(
|
|
156
|
+
(diagnostic) => ({
|
|
157
|
+
start: diagnostic.start,
|
|
158
|
+
length: diagnostic.length,
|
|
159
|
+
messageText: diagnostic.messageText,
|
|
160
|
+
category: diagnostic.category,
|
|
161
|
+
code: diagnostic.code,
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
return mapWorkerDiagnostics({
|
|
165
|
+
diagnostics,
|
|
166
|
+
validationText: text,
|
|
167
|
+
prependedLineCount,
|
|
168
|
+
});
|
|
169
|
+
} finally {
|
|
170
|
+
model.dispose();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Lightweight, Monaco-free signal for the one-time "monaco-vscode services
|
|
2
|
+
// ready" transition.
|
|
3
|
+
//
|
|
4
|
+
// Extracted from `monacoTsService` so consumers that only need to OBSERVE
|
|
5
|
+
// readiness (the `@checkstack/ui` barrel re-export, the automation editor's
|
|
6
|
+
// `ScriptServicesBooter` + headless validator) do NOT transitively pull the
|
|
7
|
+
// entire `@codingame/*` Monaco stack into their bundle. Importing this module
|
|
8
|
+
// loads zero Monaco code, so pages that never mount an editor (e.g. the login
|
|
9
|
+
// page) stay Monaco-free.
|
|
10
|
+
//
|
|
11
|
+
// Why a global flag at all: the monaco-vscode API initializes globally exactly
|
|
12
|
+
// ONCE, and that init is owned by the editor wrapper (`MonacoEditorReactComp`),
|
|
13
|
+
// which throws "Services are already initialized" if anything else inits first.
|
|
14
|
+
// So the headless validator must NOT touch the worker / models until an editor
|
|
15
|
+
// has brought the services up. The editor flips this flag from its
|
|
16
|
+
// `onEditorStartDone` (via `markVscodeServicesReady`); the validator checks
|
|
17
|
+
// `areVscodeServicesReady()` and otherwise no-ops. Net effect: scripts validate
|
|
18
|
+
// once any script editor has been opened this session (covering collapsed cards
|
|
19
|
+
// from then on); a never-opened, all-collapsed automation is left to the
|
|
20
|
+
// deferred backend typecheck.
|
|
21
|
+
|
|
22
|
+
let vscodeServicesReady = false;
|
|
23
|
+
const servicesReadyListeners = new Set<() => void>();
|
|
24
|
+
|
|
25
|
+
/** Called by the editor once the monaco-vscode services have initialized. */
|
|
26
|
+
export const markVscodeServicesReady = (): void => {
|
|
27
|
+
if (vscodeServicesReady) return;
|
|
28
|
+
vscodeServicesReady = true;
|
|
29
|
+
for (const listener of servicesReadyListeners) listener();
|
|
30
|
+
servicesReadyListeners.clear();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** True once an editor has initialized the monaco-vscode services. */
|
|
34
|
+
export const areVscodeServicesReady = (): boolean => vscodeServicesReady;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Subscribe to the one-time "services ready" transition. Fires immediately if
|
|
38
|
+
* already ready. Returns an unsubscribe. Lets the headless validator re-run the
|
|
39
|
+
* moment the first editor brings the services up (otherwise a never-edited
|
|
40
|
+
* definition would not re-validate just because a card was opened).
|
|
41
|
+
*/
|
|
42
|
+
export const onVscodeServicesReady = (listener: () => void): (() => void) => {
|
|
43
|
+
if (vscodeServicesReady) {
|
|
44
|
+
listener();
|
|
45
|
+
return () => {};
|
|
46
|
+
}
|
|
47
|
+
servicesReadyListeners.add(listener);
|
|
48
|
+
return () => servicesReadyListeners.delete(listener);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ─── Cold-init serialization ─────────────────────────────────────────────────
|
|
52
|
+
// monaco-vscode services initialize globally exactly once. @typefox's React
|
|
53
|
+
// wrapper performs that init when given a `vscodeApiConfig`, and it is
|
|
54
|
+
// StrictMode-safe for a SINGLE editor - but two editors mounting at once both
|
|
55
|
+
// race the init and corrupt it. So exactly ONE editor claims the cold init and
|
|
56
|
+
// mounts (with `vscodeApiConfig`); every other editor waits for
|
|
57
|
+
// `areVscodeServicesReady()` before mounting (it then attaches to the
|
|
58
|
+
// already-initialized services). The claim is released if the claiming editor
|
|
59
|
+
// unmounts before services come up, so a sibling can take over.
|
|
60
|
+
let coldInitClaimed = false;
|
|
61
|
+
|
|
62
|
+
/** First caller (while not ready and unclaimed) gets the cold-init role. */
|
|
63
|
+
export const claimColdInit = (): boolean => {
|
|
64
|
+
if (vscodeServicesReady || coldInitClaimed) return false;
|
|
65
|
+
coldInitClaimed = true;
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Release the claim if services never came up (claimer unmounted early). */
|
|
70
|
+
export const releaseColdInit = (): void => {
|
|
71
|
+
if (!vscodeServicesReady) coldInitClaimed = false;
|
|
72
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { cn } from "../utils";
|
|
3
3
|
import { Button } from "./Button";
|
|
4
|
+
import { usePerformance } from "./PerformanceProvider";
|
|
4
5
|
import { AlertTriangle, X } from "lucide-react";
|
|
5
6
|
|
|
6
7
|
export interface ConfirmationModalProps {
|
|
@@ -26,6 +27,8 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
|
|
26
27
|
variant = "danger",
|
|
27
28
|
isLoading = false,
|
|
28
29
|
}) => {
|
|
30
|
+
const { isLowPower } = usePerformance();
|
|
31
|
+
|
|
29
32
|
if (!isOpen) return;
|
|
30
33
|
|
|
31
34
|
const handleConfirm = () => {
|
|
@@ -65,7 +68,10 @@ export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
|
|
|
65
68
|
onClick={handleBackdropClick}
|
|
66
69
|
>
|
|
67
70
|
<div
|
|
68
|
-
className=
|
|
71
|
+
className={cn(
|
|
72
|
+
"bg-background rounded-lg shadow-xl max-w-md w-full mx-4 my-4 max-h-[calc(100dvh-2rem)] overflow-y-auto pointer-events-auto",
|
|
73
|
+
!isLowPower && "animate-in fade-in zoom-in duration-200",
|
|
74
|
+
)}
|
|
69
75
|
role="dialog"
|
|
70
76
|
aria-modal="true"
|
|
71
77
|
aria-labelledby="modal-title"
|
|
@@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
5
|
import { cn } from "../utils";
|
|
6
6
|
import { usePerformance } from "./PerformanceProvider";
|
|
7
|
+
import { PortalContainerContext } from "./portalContainer";
|
|
7
8
|
|
|
8
9
|
const Dialog = DialogPrimitive.Root;
|
|
9
10
|
|
|
@@ -64,12 +65,23 @@ const DialogContent = React.forwardRef<
|
|
|
64
65
|
DialogContentProps & DialogContentExtraProps
|
|
65
66
|
>(({ className, children, size, hideCloseButton, ...props }, ref) => {
|
|
66
67
|
const { isLowPower } = usePerformance();
|
|
68
|
+
// Expose the content element so popovers/comboboxes inside the dialog portal
|
|
69
|
+
// INTO it, otherwise the modal scroll-lock blocks their internal scrolling.
|
|
70
|
+
const [content, setContent] = React.useState<HTMLDivElement | null>(null);
|
|
71
|
+
const setRefs = React.useCallback(
|
|
72
|
+
(node: HTMLDivElement | null) => {
|
|
73
|
+
setContent(node);
|
|
74
|
+
if (typeof ref === "function") ref(node);
|
|
75
|
+
else if (ref) ref.current = node;
|
|
76
|
+
},
|
|
77
|
+
[ref],
|
|
78
|
+
);
|
|
67
79
|
return (
|
|
68
80
|
<DialogPortal>
|
|
69
81
|
<DialogOverlay />
|
|
70
82
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
|
|
71
83
|
<DialogPrimitive.Content
|
|
72
|
-
ref={
|
|
84
|
+
ref={setRefs}
|
|
73
85
|
className={cn(
|
|
74
86
|
"pointer-events-auto relative",
|
|
75
87
|
dialogContentVariants({ size }),
|
|
@@ -79,16 +91,25 @@ const DialogContent = React.forwardRef<
|
|
|
79
91
|
)}
|
|
80
92
|
{...props}
|
|
81
93
|
>
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
</
|
|
91
|
-
|
|
94
|
+
<PortalContainerContext.Provider value={content}>
|
|
95
|
+
{/* `min-h-0 flex-1` lets this wrapper fill the height when a
|
|
96
|
+
consumer makes `DialogContent` a tall flex column (e.g. the
|
|
97
|
+
CodeEditor popout, so a `fillHeight` editor fills the body).
|
|
98
|
+
Inert for the default (non-flex) dialog: `flex-1` only affects
|
|
99
|
+
flex items, and `min-h-0` is the block default. */}
|
|
100
|
+
<div className="-mx-2 px-2 flex min-h-0 flex-1 flex-col gap-6">
|
|
101
|
+
{children}
|
|
102
|
+
</div>
|
|
103
|
+
{!hideCloseButton && (
|
|
104
|
+
<DialogPrimitive.Close
|
|
105
|
+
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
106
|
+
aria-label="Close"
|
|
107
|
+
>
|
|
108
|
+
<X className="h-4 w-4" />
|
|
109
|
+
<span className="sr-only">Close</span>
|
|
110
|
+
</DialogPrimitive.Close>
|
|
111
|
+
)}
|
|
112
|
+
</PortalContainerContext.Provider>
|
|
92
113
|
</DialogPrimitive.Content>
|
|
93
114
|
</div>
|
|
94
115
|
</DialogPortal>
|