@sverka/playground 0.2.10 → 0.2.12

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/dist/index.d.mts CHANGED
@@ -123,5 +123,73 @@ export declare class Entry {
123
123
  */
124
124
  export declare function runPipeline(project: Project): Promise<PipelineResult>;
125
125
  //#endregion
126
- export type { Construct, Finding, FindingSource, PipelineResult, PlaygroundFinding, Severity, Step, StepResult };
126
+ //#region src/share.d.ts
127
+ interface ShareableRun {
128
+ readonly schema: "sverka.playground/v1";
129
+ readonly code: string;
130
+ readonly findings?: readonly Finding[];
131
+ }
132
+ type PlaygroundErrorCode = "TRANSPILE_FAILED" | "DECODE_FAILED" | "PAYLOAD_TOO_LARGE";
133
+ export declare class PlaygroundError extends Error {
134
+ readonly code: PlaygroundErrorCode;
135
+ constructor(code: PlaygroundErrorCode, message: string);
136
+ }
137
+ /** Fragment lengths are browser-dependent; warn past 32 KB of payload. */
138
+ export declare const SHARE_PAYLOAD_WARN_BYTES: number;
139
+ /** Encode a run into a share link. `base` is the page URL the link targets
140
+ * (`location.origin + location.pathname` at runtime). */
141
+ export declare function encodeShareLink(run: ShareableRun, base?: string): string;
142
+ /** Decode a `#c=` fragment back into a ShareableRun. Corrupt, oversized, or
143
+ * shape-invalid payloads throw PlaygroundError — callers fall back to the
144
+ * default template. */
145
+ export declare function decodeShareLink(hash: string): ShareableRun;
146
+ //#endregion
147
+ //#region src/embed.d.ts
148
+ interface MountRunnerOptions {
149
+ /** Initial editor source. Defaults to the playground template. */
150
+ readonly code?: string;
151
+ /** Render the source as a fixed listing instead of an editable field. */
152
+ readonly readonly?: boolean;
153
+ /** Run `code` immediately on mount. Defaults to false — embed code may
154
+ * come from a share link or CMS, so mounting never executes it unless
155
+ * the embedder opts in or the user clicks Run. */
156
+ readonly autoRun?: boolean;
157
+ /** Called once per completed run. */
158
+ readonly onRun?: (result: PipelineResult) => void;
159
+ }
160
+ interface MountedRunner {
161
+ /** Remove the runner's DOM and listeners. */
162
+ dispose(): void;
163
+ }
164
+ /**
165
+ * Mount a self-contained runner inside `el`. `dispose()` tears the mount
166
+ * down. With `autoRun: true` the runner executes `code` once on mount;
167
+ * otherwise it waits for the Run button — embeds never auto-execute code
168
+ * the embedder didn't explicitly allow.
169
+ */
170
+ export declare function mountRunner(el: HTMLElement, opts?: MountRunnerOptions): MountedRunner;
171
+ //#endregion
172
+ //#region src/engine.d.ts
173
+ /** Default template shown in the editor. */
174
+ export declare const DEFAULT_CODE: string;
175
+ /**
176
+ * Evaluate user code and return the Project.
177
+ *
178
+ * SECURITY: This uses `new Function()` to execute user-provided code.
179
+ * This is intentional — the playground is a local development tool where
180
+ * the user writes and runs their own pipeline code. The code runs in the
181
+ * browser's main page context (not a sandboxed iframe or worker) because
182
+ * the playground needs to support synchronous FunctionStep execution and
183
+ * direct access to the pipeline constructs. This is the same trust model
184
+ * as a local REPL or `node -e`.
185
+ */
186
+ export declare function evaluateUserCode(code: string): Project;
187
+ /**
188
+ * Run a pipeline with a timeout. If a step returns a never-resolving
189
+ * promise, the timeout ensures the UI recovers instead of staying
190
+ * stuck in "Running" state forever.
191
+ */
192
+ export declare function runPipelineWithTimeout(project: Project, timeoutMs: number): Promise<Awaited<ReturnType<typeof runPipeline>>>;
193
+ //#endregion
194
+ export type { Construct, Finding, FindingSource, MountRunnerOptions, MountedRunner, PipelineResult, PlaygroundErrorCode, PlaygroundFinding, Severity, ShareableRun, Step, StepResult };
127
195
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/pipeline.ts","../src/runner.ts"],"mappings":";;KAGY;;UAGK;EACf;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;;;;UAKe;EACf;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;;;UAIe;EACf;EACA;EACA,UAAU;EACV;EACA;;;UAIe;EACf,OAAO;EACP,UAAU;EACV;EACA;;;;;uBCrDoB;WACX;WACA,OAAO;EAEJ,YAAA,OAAO,kBAAkB;;MAMjC;;;qBASO,gBAAgB;WAClB,WAAW;EAER,YAAA;;EAKZ,YAAY,UAAU;;;qBAMX,iBAAiB;WACnB,OAAO;WACP,SAAS;EAEN,YAAA,SAAS,SAAS;;EAM9B,QAAQ,MAAM;;EAKd,SAAS,OAAO;;;uBAMI,aAAa;WACxB;EAEG,YAAA,UAAU,UAAU,YAAY;;WAOnC,WAAW,QAAQ,uBAAuB;;;qBAIxC,qBAAqB;WACvB,UAAU,QAAQ,uBAAuB;EAGhD,YAAA,UAAU,UACV,YACA;IACE,UAAU,QAAQ,uBAAuB;IACzC;;EAOJ,WAAW,QAAQ,uBAAuB;;;qBAM/B;WACF,UAAU;WACV;WACA;IAAW;;WACX;EAGP,YAAA,UAAU,UACV,YACA;IAAW;MAAW;;IAAgB;;;;;;;;;;;;;wBCtBpB,YAAY,SAAS,UAAU,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/pipeline.ts","../src/runner.ts","../src/share.ts","../src/embed.ts","../src/engine.ts"],"mappings":";;KAGY;;UAGK;EACf;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;;;;UAKe;EACf;EACA;EACA;EACA,UAAU;EACV;EACA;EACA;;;UAIe;EACf;EACA;EACA,UAAU;EACV;EACA;;;UAIe;EACf,OAAO;EACP,UAAU;EACV;EACA;;;;;uBCrDoB;WACX;WACA,OAAO;EAEJ,YAAA,OAAO,kBAAkB;;MAMjC;;;qBASO,gBAAgB;WAClB,WAAW;EAER,YAAA;;EAKZ,YAAY,UAAU;;;qBAMX,iBAAiB;WACnB,OAAO;WACP,SAAS;EAEN,YAAA,SAAS,SAAS;;EAM9B,QAAQ,MAAM;;EAKd,SAAS,OAAO;;;uBAMI,aAAa;WACxB;EAEG,YAAA,UAAU,UAAU,YAAY;;WAOnC,WAAW,QAAQ,uBAAuB;;;qBAIxC,qBAAqB;WACvB,UAAU,QAAQ,uBAAuB;EAGhD,YAAA,UAAU,UACV,YACA;IACE,UAAU,QAAQ,uBAAuB;IACzC;;EAOJ,WAAW,QAAQ,uBAAuB;;;qBAM/B;WACF,UAAU;WACV;WACA;IAAW;;WACX;EAGP,YAAA,UAAU,UACV,YACA;IAAW;MAAW;;IAAgB;;;;;;;;;;;;;wBCtBpB,YAAY,SAAS,UAAU,QAAQ;;;UC3E5C;WACN;WACA;WACA,oBAAoB;;KAGnB;qBAGC,wBAAwB;WAC1B,MAAM;EACH,YAAA,MAAM,qBAAqB;;;qBAQ5B;;;wBAkFG,gBAAgB,KAAK,cAAc;;;;wBAkCnC,gBAAgB,eAAe;;;UC/H9B;;WAEN;;WAEA;;;;WAIA;;WAEA,SAAS,QAAQ;;UAGX;;EAEf;;;;;;;;wBAkKc,YACd,IAAI,aACJ,OAAM,qBACL;;;;qBC9KU;;;;;;;;;;;;wBAmWG,iBAAiB,eAAe;;;;;;wBAqB1B,uBACpB,SAAS,SACT,oBACC,QAAQ,QAAQ,kBAAkB"}
package/dist/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { Inflate, deflateSync } from "fflate";
2
+ import { generateSarifHtml } from "@sverka/sarif-viewer-web/html-generator";
1
3
  //#region src/pipeline.ts
2
4
  /** Base class for all constructs. */
3
5
  var Construct = class {
@@ -162,6 +164,868 @@ async function runPipeline(project) {
162
164
  };
163
165
  }
164
166
  //#endregion
165
- export { Entry, FunctionStep, Pipeline, Project, runPipeline };
167
+ //#region src/share.ts
168
+ var PlaygroundError = class extends Error {
169
+ code;
170
+ constructor(code, message) {
171
+ super(message);
172
+ this.name = "PlaygroundError";
173
+ this.code = code;
174
+ }
175
+ };
176
+ /** Fragment lengths are browser-dependent; warn past 32 KB of payload. */
177
+ const SHARE_PAYLOAD_WARN_BYTES = 32768;
178
+ /** Hard cap on inflated share payloads — a compressed fragment is cheap to
179
+ * craft into a deflate bomb, so decoding refuses past 1 MB of JSON. */
180
+ const SHARE_MAX_INFLATED_BYTES = 1048576;
181
+ const SHARE_PREFIX = "#c=";
182
+ function toBase64Url(bytes) {
183
+ let bin = "";
184
+ for (const b of bytes) bin += String.fromCharCode(b);
185
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
186
+ }
187
+ function fromBase64Url(text) {
188
+ const b64 = text.replace(/-/g, "+").replace(/_/g, "/");
189
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
190
+ const bin = atob(padded);
191
+ const bytes = new Uint8Array(bin.length);
192
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
193
+ return bytes;
194
+ }
195
+ /** Inflate with a hard output cap — bounded memory on hostile payloads. */
196
+ function inflateBounded(bytes) {
197
+ const chunks = [];
198
+ let total = 0;
199
+ let overflow = false;
200
+ new Inflate((chunk) => {
201
+ total += chunk.length;
202
+ if (total > SHARE_MAX_INFLATED_BYTES) {
203
+ overflow = true;
204
+ return;
205
+ }
206
+ chunks.push(chunk);
207
+ }).push(bytes, true);
208
+ if (overflow) throw new PlaygroundError("PAYLOAD_TOO_LARGE", `share link inflates past ${SHARE_MAX_INFLATED_BYTES} bytes`);
209
+ const out = new Uint8Array(total);
210
+ let offset = 0;
211
+ for (const chunk of chunks) {
212
+ out.set(chunk, offset);
213
+ offset += chunk.length;
214
+ }
215
+ return out;
216
+ }
217
+ const SEVERITIES = [
218
+ "info",
219
+ "low",
220
+ "medium",
221
+ "high",
222
+ "critical"
223
+ ];
224
+ const FINDING_STRING_FIELDS = [
225
+ "rule",
226
+ "file",
227
+ "message"
228
+ ];
229
+ const FINDING_NUMBER_FIELDS = ["startLine", "endLine"];
230
+ /** Structural check — a shared finding reaches the HTML report, so every
231
+ * field that gets rendered must be the expected scalar. */
232
+ function isFindingShape(v) {
233
+ if (typeof v !== "object" || v === null) return false;
234
+ const f = v;
235
+ if (!FINDING_STRING_FIELDS.every((k) => typeof f[k] === "string")) return false;
236
+ if (!SEVERITIES.includes(f.severity)) return false;
237
+ return FINDING_NUMBER_FIELDS.every((k) => typeof f[k] === "number" && Number.isFinite(f[k]));
238
+ }
239
+ /** Encode a run into a share link. `base` is the page URL the link targets
240
+ * (`location.origin + location.pathname` at runtime). */
241
+ function encodeShareLink(run, base = "") {
242
+ const payload = toBase64Url(deflateSync(new TextEncoder().encode(JSON.stringify(run))));
243
+ return `${base}${SHARE_PREFIX}${payload}`;
244
+ }
245
+ /** Extract the payload tail from a `#c=` fragment or a bare payload. */
246
+ function shareFragment(hash) {
247
+ const marker = hash.indexOf(SHARE_PREFIX);
248
+ if (marker >= 0) return hash.slice(marker + 3);
249
+ return hash.startsWith("#") ? hash.slice(1) : hash;
250
+ }
251
+ /** Shape assertion — anything else is a corrupt or hostile payload. */
252
+ function assertRunShape(parsed) {
253
+ if (typeof parsed !== "object" || parsed === null || parsed.schema !== "sverka.playground/v1" || typeof parsed.code !== "string") throw new Error("payload is not a sverka.playground/v1 run");
254
+ const findings = parsed.findings;
255
+ if (findings === void 0) return;
256
+ if (!Array.isArray(findings) || !findings.every(isFindingShape)) throw new Error("payload findings failed shape validation");
257
+ }
258
+ /** Decode a `#c=` fragment back into a ShareableRun. Corrupt, oversized, or
259
+ * shape-invalid payloads throw PlaygroundError — callers fall back to the
260
+ * default template. */
261
+ function decodeShareLink(hash) {
262
+ try {
263
+ const json = new TextDecoder().decode(inflateBounded(fromBase64Url(shareFragment(hash))));
264
+ const parsed = JSON.parse(json);
265
+ assertRunShape(parsed);
266
+ return parsed;
267
+ } catch (e) {
268
+ if (e instanceof PlaygroundError) throw e;
269
+ throw new PlaygroundError("DECODE_FAILED", `share link could not be decoded: ${e instanceof Error ? e.message : String(e)}`);
270
+ }
271
+ }
272
+ //#endregion
273
+ //#region src/scanner.ts
274
+ /** Non-ASCII whitespace code points beyond the Latin-1 range. */
275
+ const WS_POINTS = /* @__PURE__ */ new Set([
276
+ 160,
277
+ 5760,
278
+ 8232,
279
+ 8233,
280
+ 8239,
281
+ 8287,
282
+ 12288,
283
+ 65279
284
+ ]);
285
+ /** Whitespace per the JS grammar: \t \n \v \f \r space, NBSP, the unicode
286
+ * space separators (0x2000–0x200a), and ZWNBSP. */
287
+ function isWsChar(c) {
288
+ if (c === void 0) return false;
289
+ const n = c.codePointAt(0) ?? 0;
290
+ if (n >= 9 && n <= 13 || n === 32) return true;
291
+ return n >= 8192 && n <= 8202 || WS_POINTS.has(n);
292
+ }
293
+ /** ASCII letter-or-digit — digits and both letter cases. */
294
+ function isAsciiLetterOrDigit(n) {
295
+ return n >= 48 && n <= 57 || n >= 65 && n <= 90 || n >= 97 && n <= 122;
296
+ }
297
+ /** Identifier-part chars, approximated: ASCII ident chars plus any
298
+ * non-ASCII char that isn't JS whitespace — `export` + a non-ASCII
299
+ * letter keeps its boundary. Over-approximation only ever suppresses
300
+ * a false keyword match — it never corrupts code. */
301
+ function isIdentChar(c) {
302
+ if (c === void 0) return false;
303
+ const n = c.codePointAt(0) ?? 0;
304
+ if (n >= 128) return !isWsChar(c);
305
+ return isAsciiLetterOrDigit(n) || c === "_" || c === "$";
306
+ }
307
+ function isQuote(c) {
308
+ return c === "\"" || c === "'" || c === "`";
309
+ }
310
+ function skipIdent(code, i) {
311
+ while (i < code.length && isIdentChar(code[i])) i++;
312
+ return i;
313
+ }
314
+ /** Index of the `\n` ending the `//` comment at `at` (code.length if none). */
315
+ function lineCommentEnd(code, at) {
316
+ const end = code.indexOf("\n", at + 2);
317
+ return end === -1 ? code.length : end;
318
+ }
319
+ /** Index just past the closing star-slash ending the block comment at
320
+ * `at` — the literal marker is never written inside this doc comment
321
+ * or it would terminate the comment itself. */
322
+ function blockCommentEnd(code, at) {
323
+ const end = code.indexOf("*/", at + 2);
324
+ return end === -1 ? code.length : end + 2;
325
+ }
326
+ /** True when `at` starts a `//` or `/*` comment. */
327
+ function commentAt(code, at) {
328
+ return code[at] === "/" && (code[at + 1] === "/" || code[at + 1] === "*");
329
+ }
330
+ /** End index of the string literal starting at `at` (quote char).
331
+ * Template literals may nest `${}` expressions — tracked by brace depth. */
332
+ function scanString(code, at) {
333
+ const quote = code[at];
334
+ let i = at + 1;
335
+ while (i < code.length) {
336
+ const c = code[i];
337
+ if (c === "\\") {
338
+ i += 2;
339
+ continue;
340
+ }
341
+ if (c === quote) return i + 1;
342
+ if (quote === "`" && c === "$" && code[i + 1] === "{") {
343
+ i = scanTemplateExpr(code, i + 1);
344
+ continue;
345
+ }
346
+ i++;
347
+ }
348
+ return i;
349
+ }
350
+ /** End index of a regex literal starting at `at` (`/`): closes at the
351
+ * first unescaped `/` outside a `[]` class, then consumes flags. Stops at
352
+ * a newline or EOF when unterminated — the evaluator reports the syntax. */
353
+ function scanRegex(code, at) {
354
+ let i = at + 1;
355
+ let inClass = false;
356
+ while (i < code.length) {
357
+ const c = code[i];
358
+ if (c === "\\") {
359
+ i += 2;
360
+ continue;
361
+ }
362
+ if (c === "[") inClass = true;
363
+ else if (c === "]") inClass = false;
364
+ else if (c === "/" && !inClass) return skipIdent(code, i + 1);
365
+ else if (c === "\n") return i;
366
+ i++;
367
+ }
368
+ return i;
369
+ }
370
+ /** End index of the opaque region at `i`, or i+1 for ordinary chars —
371
+ * plus whether the region was a string literal (an operand). */
372
+ function scanOpaqueEnd(code, i) {
373
+ const c = code[i];
374
+ if (isQuote(c)) return {
375
+ end: scanString(code, i),
376
+ isString: true
377
+ };
378
+ if (c === "/" && code[i + 1] === "/") return {
379
+ end: lineCommentEnd(code, i),
380
+ isString: false
381
+ };
382
+ if (c === "/" && code[i + 1] === "*") return {
383
+ end: blockCommentEnd(code, i),
384
+ isString: false
385
+ };
386
+ return {
387
+ end: i + 1,
388
+ isString: false
389
+ };
390
+ }
391
+ /** Keywords and contextual keywords after which `/` opens a regex rather
392
+ * than divides. Everything else — plain identifiers plus literals such
393
+ * as `this`/`true` — ends an operand, making `/` a division. */
394
+ const NON_OPERAND_WORDS = /* @__PURE__ */ new Set([
395
+ "await",
396
+ "break",
397
+ "case",
398
+ "catch",
399
+ "class",
400
+ "const",
401
+ "continue",
402
+ "debugger",
403
+ "default",
404
+ "delete",
405
+ "do",
406
+ "else",
407
+ "enum",
408
+ "export",
409
+ "extends",
410
+ "finally",
411
+ "for",
412
+ "function",
413
+ "if",
414
+ "implements",
415
+ "import",
416
+ "in",
417
+ "instanceof",
418
+ "interface",
419
+ "let",
420
+ "new",
421
+ "package",
422
+ "private",
423
+ "protected",
424
+ "public",
425
+ "return",
426
+ "static",
427
+ "switch",
428
+ "throw",
429
+ "try",
430
+ "typeof",
431
+ "var",
432
+ "void",
433
+ "while",
434
+ "with",
435
+ "yield",
436
+ "as",
437
+ "async",
438
+ "from",
439
+ "get",
440
+ "of",
441
+ "set",
442
+ "type",
443
+ "declare"
444
+ ]);
445
+ /** True when `word` ends an operand — a following `/` is division. */
446
+ function operandAfterWord(word) {
447
+ return !NON_OPERAND_WORDS.has(word);
448
+ }
449
+ /** Consume one token at `i` for the brace-matching scans: opaque regions
450
+ * and regex literals advance wholesale, identifiers update operand
451
+ * state from the keyword table, braces report a depth delta. */
452
+ function scanToken(code, i, operandEnd) {
453
+ const opaque = scanOpaqueEnd(code, i);
454
+ if (opaque.end > i + 1) return {
455
+ end: opaque.end,
456
+ operandEnd: opaque.isString || operandEnd,
457
+ brace: 0
458
+ };
459
+ const ch = code[i];
460
+ if (ch === "/" && !operandEnd) return {
461
+ end: scanRegex(code, i),
462
+ operandEnd: true,
463
+ brace: 0
464
+ };
465
+ if (isWsChar(ch)) return {
466
+ end: i + 1,
467
+ operandEnd,
468
+ brace: 0
469
+ };
470
+ if (isIdentChar(ch)) {
471
+ const wend = skipIdent(code, i);
472
+ return {
473
+ end: wend,
474
+ operandEnd: operandAfterWord(code.slice(i, wend)),
475
+ brace: 0
476
+ };
477
+ }
478
+ if (ch === "{") return {
479
+ end: i + 1,
480
+ operandEnd: false,
481
+ brace: 1
482
+ };
483
+ if (ch === "}") return {
484
+ end: i + 1,
485
+ operandEnd: true,
486
+ brace: -1
487
+ };
488
+ return {
489
+ end: i + 1,
490
+ operandEnd: false,
491
+ brace: 0
492
+ };
493
+ }
494
+ /**
495
+ * End index of the `${` expression whose `{` is at `at`. Strings,
496
+ * comments, nested templates, and regex literals inside the expression
497
+ * are all skipped correctly: `` `${ x["`"] }` `` and `` `${ /"/ }` ``
498
+ * must not treat literal text as syntax.
499
+ */
500
+ function scanTemplateExpr(code, at) {
501
+ let depth = 1;
502
+ let i = at + 1;
503
+ let operandEnd = false;
504
+ while (i < code.length && depth > 0) {
505
+ const step = scanToken(code, i, operandEnd);
506
+ depth += step.brace;
507
+ operandEnd = step.operandEnd;
508
+ i = step.end;
509
+ }
510
+ return i;
511
+ }
512
+ /**
513
+ * Index of the `}` matching the `{` at `open`; -1 when unbalanced.
514
+ * Operand tracking decides whether a `/` divides or opens a regex —
515
+ * braces inside `/[{]/` never reach the depth counter.
516
+ */
517
+ function matchBrace(code, open) {
518
+ let depth = 0;
519
+ let i = open;
520
+ let operandEnd = false;
521
+ while (i < code.length) {
522
+ const step = scanToken(code, i, operandEnd);
523
+ depth += step.brace;
524
+ operandEnd = step.operandEnd;
525
+ i = step.end;
526
+ if (step.brace === -1 && depth === 0) return i - 1;
527
+ }
528
+ return -1;
529
+ }
530
+ /** `word` starting at `at` with an identifier boundary on the right. */
531
+ function keywordAt(code, at, word) {
532
+ return code.startsWith(word, at) && !isIdentChar(code[at + word.length]);
533
+ }
534
+ /** Skip whitespace and comments (including newlines). */
535
+ function skipTrivia(code, at) {
536
+ let i = at;
537
+ for (;;) {
538
+ const c = code[i];
539
+ if (isWsChar(c)) {
540
+ i++;
541
+ continue;
542
+ }
543
+ if (c === "/" && code[i + 1] === "/") {
544
+ i = lineCommentEnd(code, i);
545
+ continue;
546
+ }
547
+ if (c === "/" && code[i + 1] === "*") {
548
+ i = blockCommentEnd(code, i);
549
+ continue;
550
+ }
551
+ return i;
552
+ }
553
+ }
554
+ //#endregion
555
+ //#region src/engine.ts
556
+ /** Default template shown in the editor. */
557
+ const DEFAULT_CODE = [
558
+ "import { Project, Pipeline, FunctionStep, Entry } from \"@sverka/playground\";",
559
+ "",
560
+ "const proj = new Project(\"demo\");",
561
+ "const checks = new Pipeline(proj, \"checks\");",
562
+ "",
563
+ "new FunctionStep(checks, \"lint\", {",
564
+ " fn: () => [",
565
+ " { rule: \"no-unused-vars\", file: \"src/index.ts\", line: 5, severity: \"high\", message: \"Variable 'x' is declared but never used\" },",
566
+ " { rule: \"no-console\", file: \"src/utils.ts\", line: 12, severity: \"medium\", message: \"Unexpected console.log statement\" },",
567
+ " ],",
568
+ "});",
569
+ "",
570
+ "new FunctionStep(checks, \"typecheck\", {",
571
+ " fn: () => [",
572
+ " { rule: \"ts2322\", file: \"src/types.ts\", line: 8, severity: \"critical\", message: \"Type 'string' is not assignable to type 'number'\" },",
573
+ " ],",
574
+ "});",
575
+ "",
576
+ "new FunctionStep(checks, \"test\", {",
577
+ " fn: () => [",
578
+ " { rule: \"assertion-failed\", file: \"test/index.test.ts\", line: 23, severity: \"high\", message: \"Expected 5 but got 3\" },",
579
+ " { rule: \"assertion-failed\", file: \"test/index.test.ts\", line: 45, severity: \"low\", message: \"Expected 'hello' but got 'world'\" },",
580
+ " ],",
581
+ "});",
582
+ "",
583
+ "new Entry(checks, \"on-push\", { trigger: { kind: \"push\" }, roots: [\"test\"] });",
584
+ "",
585
+ "export default proj;",
586
+ ""
587
+ ].join("\n");
588
+ /** Escape HTML special characters to prevent XSS. */
589
+ function escapeHtml(text) {
590
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
591
+ }
592
+ /** +1/−1 for bracket chars, 0 otherwise. */
593
+ function bracketDelta(c) {
594
+ if (c === "{" || c === "(" || c === "[") return 1;
595
+ if (c === "}" || c === ")" || c === "]") return -1;
596
+ return 0;
597
+ }
598
+ /** Chars that end an import statement at depth ≤0: `;` (consumed) or a
599
+ * newline after the module-specifier string (left in place). */
600
+ function stmtTerminator(c, depth, specSeen) {
601
+ if (c === ";" && depth <= 0) return "past";
602
+ if (c === "\n" && depth <= 0 && specSeen) return "at";
603
+ return null;
604
+ }
605
+ /** End index of the statement starting at `at`: first top-level `;`, or
606
+ * the first newline after the module-specifier string (covers multiline
607
+ * `import {…}\n from "x"` and `import "x"` without a semicolon). */
608
+ function scanStatementEnd(code, at) {
609
+ let i = at;
610
+ let depth = 0;
611
+ let specSeen = false;
612
+ while (i < code.length) {
613
+ const opaque = scanOpaqueEnd(code, i);
614
+ if (opaque.end > i + 1) {
615
+ specSeen = specSeen || opaque.isString;
616
+ i = opaque.end;
617
+ continue;
618
+ }
619
+ const term = stmtTerminator(code[i], depth, specSeen);
620
+ if (term === "past") return i + 1;
621
+ if (term === "at") return i;
622
+ depth += bracketDelta(code[i]);
623
+ i++;
624
+ }
625
+ return i;
626
+ }
627
+ /** Same-line statement tail: horizontal whitespace then an optional `;`.
628
+ * Never eats the newline — the following line is the next statement. */
629
+ function scanStmtTail(code, at) {
630
+ let i = at;
631
+ while (i < code.length && (code[i] === " " || code[i] === " ")) i++;
632
+ if (code[i] === ";") i++;
633
+ return i;
634
+ }
635
+ /** End index of the `from "…"` clause starting after `from`, or `at` when
636
+ * the clause is malformed — let the evaluator complain. */
637
+ function scanFromClause(code, next) {
638
+ const specAt = skipTrivia(code, next + 4);
639
+ const q = code[specAt];
640
+ if (q === "\"" || q === "'") return scanStmtTail(code, scanString(code, specAt));
641
+ return next;
642
+ }
643
+ /** End index of `export * [as ns] [from "…"]` — the `*` is at `at`. */
644
+ function scanExportStar(code, at) {
645
+ let i = skipTrivia(code, at + 1);
646
+ if (keywordAt(code, i, "as")) {
647
+ i = skipTrivia(code, i + 2);
648
+ i = skipIdent(code, i);
649
+ }
650
+ const next = skipTrivia(code, i);
651
+ if (keywordAt(code, next, "from")) return scanFromClause(code, next);
652
+ return scanStmtTail(code, i);
653
+ }
654
+ /**
655
+ * End index of `export {…}` / `export * [as ns] [from "…"]` starting at
656
+ * the `{` or `*`. A bare list ends at its `;` or newline — unlike an
657
+ * import, a newline after the closing brace must end the statement or
658
+ * the next line would be eaten with it.
659
+ */
660
+ function scanExportListEnd(code, at) {
661
+ if (code[at] === "*") return scanExportStar(code, at);
662
+ const close = matchBrace(code, at);
663
+ const i = close < 0 ? code.length : close + 1;
664
+ const next = skipTrivia(code, i);
665
+ if (keywordAt(code, next, "from")) return scanFromClause(code, next);
666
+ return scanStmtTail(code, i);
667
+ }
668
+ function atStmtLevel(st) {
669
+ return st.stack.length === 0 || st.stack.at(-1) === "block";
670
+ }
671
+ /** Copy a string literal or comment verbatim into `out`. */
672
+ function copyOpaque(code, st, out) {
673
+ const opaque = scanOpaqueEnd(code, st.i);
674
+ if (opaque.end <= st.i + 1) return false;
675
+ out.push(code.slice(st.i, opaque.end));
676
+ if (opaque.isString) {
677
+ st.stmtStart = false;
678
+ st.operandEnd = true;
679
+ }
680
+ st.i = opaque.end;
681
+ return true;
682
+ }
683
+ /** Handle `{}`/`()`/`[]` — pushes onto the block/expr stack and sets the
684
+ * operand context: `}` closing an object literal ends a value, `}`
685
+ * closing a block ends a statement. */
686
+ function applyBracket(code, st, out) {
687
+ const c = code[st.i];
688
+ if (c === "{") {
689
+ st.stack.push(st.stmtStart ? "block" : "expr");
690
+ st.operandEnd = false;
691
+ } else if (c === "}") {
692
+ const popped = st.stack.pop();
693
+ st.stmtStart = popped === "block";
694
+ st.operandEnd = popped === "expr";
695
+ } else if (c === "(" || c === "[") {
696
+ st.stack.push("expr");
697
+ st.stmtStart = false;
698
+ st.operandEnd = false;
699
+ } else if (c === ")" || c === "]") {
700
+ st.stack.pop();
701
+ st.stmtStart = false;
702
+ st.operandEnd = true;
703
+ } else return false;
704
+ out.push(c ?? "");
705
+ st.i++;
706
+ return true;
707
+ }
708
+ /** Handle `\n`, `;`, and whitespace — statement boundaries at statement
709
+ * level only (a `;` inside `for (;;)` doesn't count). A newline never
710
+ * resets operand context: `foo\n/bar/` divides, per ASI rules. */
711
+ function applyTerminator(code, st, out) {
712
+ const c = code[st.i];
713
+ if (c !== ";" && c !== "\n" && !isWsChar(c)) return false;
714
+ if (c === ";") {
715
+ if (atStmtLevel(st)) st.stmtStart = true;
716
+ st.operandEnd = false;
717
+ } else if (c === "\n" && atStmtLevel(st)) st.stmtStart = true;
718
+ out.push(c ?? "");
719
+ st.i++;
720
+ return true;
721
+ }
722
+ /** Handle `/` — a regex literal when an operand is expected, a division
723
+ * operator when one just ended. Comments never reach here (copyOpaque
724
+ * already consumed them). */
725
+ function applySlash(code, st, out) {
726
+ if (code[st.i] !== "/" || commentAt(code, st.i)) return false;
727
+ if (st.operandEnd) {
728
+ out.push("/");
729
+ st.i++;
730
+ st.operandEnd = false;
731
+ } else {
732
+ const end = scanRegex(code, st.i);
733
+ out.push(code.slice(st.i, end));
734
+ st.i = end;
735
+ st.operandEnd = true;
736
+ }
737
+ st.stmtStart = false;
738
+ return true;
739
+ }
740
+ /** Handle an `import` keyword at statement start. Dynamic `import (…)`
741
+ * and `import.meta` pass through verbatim — whitespace and comments may
742
+ * sit between the keyword and the paren. */
743
+ function tryImport(code, st, out) {
744
+ if (!st.stmtStart || !keywordAt(code, st.i, "import")) return false;
745
+ const nc = code[skipTrivia(code, st.i + 6)];
746
+ if (nc === "(" || nc === ".") {
747
+ out.push("import");
748
+ st.i += 6;
749
+ st.stmtStart = false;
750
+ st.operandEnd = true;
751
+ return true;
752
+ }
753
+ st.i = scanStatementEnd(code, st.i + 6);
754
+ st.stmtStart = false;
755
+ st.operandEnd = false;
756
+ return true;
757
+ }
758
+ /** Handle `export default` (→ `return`), `export {…}`/`export *` (removed)
759
+ * and `export <decl>` (keyword dropped — the eval context is a function
760
+ * body, where `export const` would be a syntax error). */
761
+ function tryExport(code, st, out) {
762
+ if (!st.stmtStart || !keywordAt(code, st.i, "export")) return false;
763
+ const j = skipTrivia(code, st.i + 6);
764
+ if (!st.exportDefaultDone && code.startsWith("default", j) && !isIdentChar(code[j + 7])) {
765
+ out.push("return ");
766
+ st.exportDefaultDone = true;
767
+ st.i = skipTrivia(code, j + 7);
768
+ st.stmtStart = false;
769
+ st.operandEnd = false;
770
+ return true;
771
+ }
772
+ if (code[j] === "{" || code[j] === "*") {
773
+ st.i = scanExportListEnd(code, j);
774
+ st.stmtStart = false;
775
+ st.operandEnd = false;
776
+ return true;
777
+ }
778
+ st.i = j;
779
+ st.stmtStart = false;
780
+ st.operandEnd = false;
781
+ return true;
782
+ }
783
+ /** Emit an identifier run — keyword table decides whether `/` after it
784
+ * divides (`foo /x/`) or opens a regex (`return /x/`). */
785
+ function tryWord(code, st, out) {
786
+ if (!isIdentChar(code[st.i])) return false;
787
+ const wend = skipIdent(code, st.i);
788
+ out.push(code.slice(st.i, wend));
789
+ st.operandEnd = operandAfterWord(code.slice(st.i, wend));
790
+ st.stmtStart = false;
791
+ st.i = wend;
792
+ return true;
793
+ }
794
+ /** Char handlers tried in order — first true wins. Any char left over is
795
+ * copied verbatim as a non-operand punctuation token. */
796
+ const HANDLERS = [
797
+ copyOpaque,
798
+ applyBracket,
799
+ applyTerminator,
800
+ applySlash,
801
+ tryImport,
802
+ tryExport,
803
+ tryWord
804
+ ];
805
+ function advanceScan(code, st, out) {
806
+ for (const handle of HANDLERS) if (handle(code, st, out)) return;
807
+ st.stmtStart = false;
808
+ st.operandEnd = false;
809
+ out.push(code[st.i] ?? "");
810
+ st.i++;
811
+ }
812
+ /**
813
+ * Strip import/export statements from user code for eval — a linear
814
+ * single-pass scan, so multiline imports and comments/strings are handled
815
+ * and pathological spacing cannot cause quadratic backtracking.
816
+ *
817
+ * `import …` statements are removed whole. `export default` becomes
818
+ * `return`, `export {…}`/`export *` are removed, and `export` before a
819
+ * declaration is dropped (the eval context is a function body, where
820
+ * `export const` would be a syntax error).
821
+ *
822
+ * Note: TypeScript-specific syntax (type annotations, interfaces, enums)
823
+ * is not stripped — the playground uses Monaco's TypeScript language mode
824
+ * for editing, but evaluation is plain JavaScript. Users should write
825
+ * JS-compatible code or use the `as any` escape hatch sparingly.
826
+ */
827
+ function preprocessCode(code) {
828
+ const out = [];
829
+ const st = {
830
+ i: 0,
831
+ stmtStart: true,
832
+ exportDefaultDone: false,
833
+ stack: [],
834
+ operandEnd: false
835
+ };
836
+ while (st.i < code.length) {
837
+ if (code[st.i] === void 0) break;
838
+ advanceScan(code, st, out);
839
+ }
840
+ return out.join("");
841
+ }
842
+ /**
843
+ * Evaluate user code and return the Project.
844
+ *
845
+ * SECURITY: This uses `new Function()` to execute user-provided code.
846
+ * This is intentional — the playground is a local development tool where
847
+ * the user writes and runs their own pipeline code. The code runs in the
848
+ * browser's main page context (not a sandboxed iframe or worker) because
849
+ * the playground needs to support synchronous FunctionStep execution and
850
+ * direct access to the pipeline constructs. This is the same trust model
851
+ * as a local REPL or `node -e`.
852
+ */
853
+ function evaluateUserCode(code) {
854
+ const params = [
855
+ "Project",
856
+ "Pipeline",
857
+ "FunctionStep",
858
+ "Entry",
859
+ preprocessCode(code)
860
+ ];
861
+ const result = new Function(...params)(Project, Pipeline, FunctionStep, Entry);
862
+ if (!(result instanceof Project)) throw new TypeError("Code must export a Project instance");
863
+ return result;
864
+ }
865
+ /**
866
+ * Run a pipeline with a timeout. If a step returns a never-resolving
867
+ * promise, the timeout ensures the UI recovers instead of staying
868
+ * stuck in "Running" state forever.
869
+ */
870
+ async function runPipelineWithTimeout(project, timeoutMs) {
871
+ let timer;
872
+ try {
873
+ return await Promise.race([runPipeline(project), new Promise((_, reject) => {
874
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Pipeline timed out after ${timeoutMs}ms`)), timeoutMs);
875
+ })]);
876
+ } finally {
877
+ if (timer !== void 0) clearTimeout(timer);
878
+ }
879
+ }
880
+ //#endregion
881
+ //#region src/embed.ts
882
+ const STYLE = `
883
+ font-family: monospace; font-size: 12px;
884
+ background: #0d1117; color: #c9d1d9;
885
+ border: 1px solid #30363d; border-radius: 6px;
886
+ display: flex; flex-direction: column; overflow: hidden;
887
+ `;
888
+ function createHeader(doc) {
889
+ const header = doc.createElement("div");
890
+ header.setAttribute("style", "display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.6rem;background:#161b22;border-bottom:1px solid #30363d;");
891
+ const runBtn = doc.createElement("button");
892
+ runBtn.textContent = "Run";
893
+ runBtn.setAttribute("style", "padding:0.2rem 0.8rem;border:1px solid #30363d;border-radius:6px;background:#1f6feb;color:#fff;cursor:pointer;font-size:0.8rem;");
894
+ const status = doc.createElement("span");
895
+ status.setAttribute("style", "font-size:0.75rem;color:#8b949e;");
896
+ status.textContent = "Ready";
897
+ header.append(runBtn, status);
898
+ return {
899
+ header,
900
+ runBtn,
901
+ status
902
+ };
903
+ }
904
+ function createCodeEl(doc, code, readonly) {
905
+ if (readonly) {
906
+ const pre = doc.createElement("pre");
907
+ pre.setAttribute("style", "margin:0;padding:0.75rem;overflow:auto;max-height:16rem;white-space:pre;");
908
+ pre.textContent = code;
909
+ return pre;
910
+ }
911
+ const textarea = doc.createElement("textarea");
912
+ textarea.value = code;
913
+ textarea.setAttribute("style", "flex:0 0 12rem;width:100%;background:#0d1117;color:#c9d1d9;border:none;border-bottom:1px solid #30363d;padding:0.75rem;resize:vertical;outline:none;font:inherit;");
914
+ return textarea;
915
+ }
916
+ function createFrame(doc) {
917
+ const frame = doc.createElement("iframe");
918
+ frame.setAttribute("sandbox", "allow-scripts");
919
+ frame.setAttribute("style", "flex:1;min-height:10rem;width:100%;border:none;background:#0d1117;");
920
+ return frame;
921
+ }
922
+ const BODY_DARK = "<body style=\"background:#0d1117;font-family:monospace;padding:1rem;";
923
+ /** Failure banner HTML — string concat keeps user data flowing through
924
+ * escapeHtml explicitly, never through template interpolation. */
925
+ function failureBanner(result) {
926
+ return [
927
+ "<div style=\"background:#3d1d1f;color:#f85149;padding:0.6rem 1rem;font-family:monospace;border-bottom:1px solid #30363d;\">",
928
+ "<strong>Run failed</strong>",
929
+ "<pre style=\"margin:0.4rem 0 0;white-space:pre-wrap;\">",
930
+ escapeHtml(result.steps.filter((s) => s.status === "failure").map((s) => `${s.stepId}: ${s.error ?? "failed"}`).join("\n") || `${result.steps.length} steps failed`),
931
+ "</pre></div>"
932
+ ].join("");
933
+ }
934
+ /** Document shown when the run failed — partial findings still render
935
+ * underneath the banner so finished steps aren't hidden by the error. */
936
+ function failureDoc(result, banner) {
937
+ if (result.findings.length === 0) return "<!DOCTYPE html><html>" + BODY_DARK + "color:#f85149;\">" + banner + "</body></html>";
938
+ const sarif = generateSarifHtml(result.findings);
939
+ return sarif.includes("<body>") ? sarif.replace("<body>", "<body>" + banner) : banner + sarif;
940
+ }
941
+ /** Document shown for a clean run with zero findings. */
942
+ function successDoc(result) {
943
+ return [
944
+ "<!DOCTYPE html><html>",
945
+ BODY_DARK,
946
+ "color:#3fb950;\">",
947
+ "<h3>No findings — all checks passed</h3><p>",
948
+ escapeHtml(String(result.steps.length)),
949
+ " steps in ",
950
+ escapeHtml(String(result.totalDurationMs)),
951
+ "ms</p></body></html>"
952
+ ].join("");
953
+ }
954
+ /** Document shown when the runner itself threw (eval or pipeline error). */
955
+ function errorDoc(e) {
956
+ const msg = e instanceof Error ? e.message : String(e);
957
+ return [
958
+ "<!DOCTYPE html><html>",
959
+ BODY_DARK,
960
+ "color:#f85149;\">",
961
+ "<h3>Error</h3><pre>",
962
+ escapeHtml(msg),
963
+ "</pre></body></html>"
964
+ ].join("");
965
+ }
966
+ function statusLine(result) {
967
+ if (result.success) return `${result.findings.length} findings · ${result.totalDurationMs}ms`;
968
+ return `${result.steps.filter((s) => s.status === "failure").length} step(s) failed · ${result.findings.length} findings`;
969
+ }
970
+ /** Render a completed run into the findings frame. */
971
+ function renderResult(result, showFindings) {
972
+ if (!result.success) {
973
+ showFindings(failureDoc(result, failureBanner(result)));
974
+ return;
975
+ }
976
+ if (result.findings.length === 0) {
977
+ showFindings(successDoc(result));
978
+ return;
979
+ }
980
+ showFindings(generateSarifHtml(result.findings));
981
+ }
982
+ /**
983
+ * Mount a self-contained runner inside `el`. `dispose()` tears the mount
984
+ * down. With `autoRun: true` the runner executes `code` once on mount;
985
+ * otherwise it waits for the Run button — embeds never auto-execute code
986
+ * the embedder didn't explicitly allow.
987
+ */
988
+ function mountRunner(el, opts = {}) {
989
+ const doc = el.ownerDocument;
990
+ const code = opts.code ?? DEFAULT_CODE;
991
+ const root = doc.createElement("div");
992
+ root.setAttribute("style", STYLE);
993
+ const { header, runBtn, status } = createHeader(doc);
994
+ const codeEl = createCodeEl(doc, code, opts.readonly === true);
995
+ const frame = createFrame(doc);
996
+ root.append(header, codeEl, frame);
997
+ el.appendChild(root);
998
+ const getCode = () => opts.readonly === true ? code : codeEl.value;
999
+ const run = async () => {
1000
+ runBtn.disabled = true;
1001
+ status.textContent = "Running...";
1002
+ let result;
1003
+ try {
1004
+ result = await runPipelineWithTimeout(evaluateUserCode(getCode()), 3e4);
1005
+ renderResult(result, (html) => {
1006
+ frame.srcdoc = html;
1007
+ });
1008
+ status.textContent = statusLine(result);
1009
+ } catch (e) {
1010
+ frame.srcdoc = errorDoc(e);
1011
+ status.textContent = "Error";
1012
+ runBtn.disabled = false;
1013
+ return;
1014
+ }
1015
+ runBtn.disabled = false;
1016
+ opts.onRun?.(result);
1017
+ };
1018
+ const onClick = () => {
1019
+ run();
1020
+ };
1021
+ runBtn.addEventListener("click", onClick);
1022
+ if (opts.autoRun === true) run();
1023
+ return { dispose() {
1024
+ runBtn.removeEventListener("click", onClick);
1025
+ root.remove();
1026
+ } };
1027
+ }
1028
+ //#endregion
1029
+ export { DEFAULT_CODE, Entry, FunctionStep, Pipeline, PlaygroundError, Project, SHARE_PAYLOAD_WARN_BYTES, decodeShareLink, encodeShareLink, evaluateUserCode, mountRunner, runPipeline, runPipelineWithTimeout };
166
1030
 
167
1031
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/pipeline.ts","../src/runner.ts"],"sourcesContent":["// @sverka/playground — minimal pipeline model. Browser-safe.\n// Mirrors @sverka/workflow Construct API but simplified for browser use.\n\nimport type { PlaygroundFinding } from \"./types.js\";\n\n/** Base class for all constructs. */\nexport abstract class Construct {\n readonly id: string;\n readonly scope: Construct | null;\n\n constructor(scope: Construct | null, id: string) {\n this.id = id;\n this.scope = scope;\n }\n\n /** Full path from root to this construct. */\n get path(): string {\n if (this.scope && this.scope.id !== \"root\") {\n return `${this.scope.path}/${this.id}`;\n }\n return this.id;\n }\n}\n\n/** Root construct representing a project. */\nexport class Project extends Construct {\n readonly pipelines: Pipeline[] = [];\n\n constructor(id: string) {\n super(null, id);\n }\n\n /** Add a pipeline to this project. */\n addPipeline(pipeline: Pipeline): void {\n this.pipelines.push(pipeline);\n }\n}\n\n/** A pipeline containing steps. */\nexport class Pipeline extends Construct {\n readonly steps: Step[] = [];\n readonly entries: Entry[] = [];\n\n constructor(project: Project, id: string) {\n super(project, id);\n project.addPipeline(this);\n }\n\n /** Add a step to this pipeline. */\n addStep(step: Step): void {\n this.steps.push(step);\n }\n\n /** Add an entry to this pipeline. */\n addEntry(entry: Entry): void {\n this.entries.push(entry);\n }\n}\n\n/** Base class for all step types. */\nexport abstract class Step extends Construct {\n readonly dependencies: string[];\n\n constructor(pipeline: Pipeline, id: string, dependencies: string[] = []) {\n super(pipeline, id);\n this.dependencies = dependencies;\n pipeline.addStep(this);\n }\n\n /** Execute this step and return findings. */\n abstract execute(): Promise<PlaygroundFinding[]> | PlaygroundFinding[];\n}\n\n/** A step that runs a JavaScript function in the browser. */\nexport class FunctionStep extends Step {\n readonly fn: () => Promise<PlaygroundFinding[]> | PlaygroundFinding[];\n\n constructor(\n pipeline: Pipeline,\n id: string,\n options: {\n fn: () => Promise<PlaygroundFinding[]> | PlaygroundFinding[];\n dependencies?: string[];\n },\n ) {\n super(pipeline, id, options.dependencies ?? []);\n this.fn = options.fn;\n }\n\n execute(): Promise<PlaygroundFinding[]> | PlaygroundFinding[] {\n return this.fn();\n }\n}\n\n/** An entry point that triggers a pipeline. */\nexport class Entry {\n readonly pipeline: Pipeline;\n readonly id: string;\n readonly trigger: { kind: string };\n readonly roots: string[];\n\n constructor(\n pipeline: Pipeline,\n id: string,\n options: { trigger: { kind: string }; roots: string[] },\n ) {\n this.pipeline = pipeline;\n this.id = id;\n this.trigger = options.trigger;\n this.roots = options.roots;\n pipeline.addEntry(this);\n }\n}\n","// @sverka/playground — pipeline runner. Browser-safe.\n// Executes FunctionSteps and collects findings.\n\nimport type { Project } from \"./pipeline.js\";\nimport type { Pipeline } from \"./pipeline.js\";\nimport type { Step } from \"./pipeline.js\";\nimport type {\n Finding,\n PlaygroundFinding,\n PipelineResult,\n StepResult,\n} from \"./types.js\";\n\n/** Simple deterministic hash for fingerprint generation. Browser-safe. */\nfunction simpleHash(input: string): string {\n let hash = 5381;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0;\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/** Convert a PlaygroundFinding to a full Finding with fingerprint and id. */\nfunction toFinding(pf: PlaygroundFinding, stepId: string): Finding {\n // Include checkId (stepId) in fingerprint so identical findings from\n // different checks get distinct fingerprints.\n const fingerprintInput = `${stepId}:${pf.rule}:${pf.file}:${pf.line}`;\n const fingerprint = simpleHash(fingerprintInput);\n const id = `${stepId}:${fingerprint}`;\n return {\n id,\n fingerprint,\n checkId: stepId,\n severity: pf.severity,\n confidence: 0.8,\n message: pf.message,\n rule: pf.rule,\n file: pf.file,\n startLine: pf.line,\n endLine: pf.line,\n source: {\n tool: pf.tool ?? stepId,\n version: null,\n format: \"custom\",\n originalRuleId: pf.rule,\n originalSeverity: null,\n },\n ...(pf.snippet ? { snippet: pf.snippet } : {}),\n };\n}\n\n/** Execute a single step and return its result. */\nasync function executeStep(step: Step): Promise<StepResult> {\n const start = Date.now();\n try {\n const playgroundFindings = await step.execute();\n const findings = playgroundFindings.map((pf) => toFinding(pf, step.id));\n return {\n stepId: step.id,\n status: \"success\",\n findings,\n durationMs: Date.now() - start,\n };\n } catch (e) {\n return {\n stepId: step.id,\n status: \"failure\",\n findings: [],\n durationMs: Date.now() - start,\n error: e instanceof Error ? e.message : String(e),\n };\n }\n}\n\n/**\n * Run all FunctionSteps in a project's pipelines and collect findings.\n * Browser-safe — no Node.js APIs, no child_process, no fs.\n *\n * If a pipeline has entries with `roots`, only steps whose id matches a\n * root are executed. If no entries exist or all entries have empty roots,\n * all steps run.\n */\nexport async function runPipeline(project: Project): Promise<PipelineResult> {\n const start = Date.now();\n const stepResults: StepResult[] = [];\n const allFindings: Finding[] = [];\n\n for (const pipeline of project.pipelines) {\n // Determine which steps to run based on entry roots.\n let stepsToRun = pipeline.steps;\n const entriesWithRoots = pipeline.entries.filter((e) => e.roots.length > 0);\n if (entriesWithRoots.length > 0) {\n const rootSet = new Set<string>();\n for (const entry of entriesWithRoots) {\n for (const root of entry.roots) rootSet.add(root);\n }\n stepsToRun = pipeline.steps.filter((s) => rootSet.has(s.id));\n }\n\n // Execute all selected steps in parallel — dependencies are for\n // ordering, not data flow in the playground.\n const results = await Promise.all(\n stepsToRun.map((step) => executeStep(step)),\n );\n\n for (const result of results) {\n stepResults.push(result);\n allFindings.push(...result.findings);\n }\n }\n\n return {\n steps: stepResults,\n findings: allFindings,\n totalDurationMs: Date.now() - start,\n success: stepResults.every((s) => s.status === \"success\"),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/pipeline.ts","../src/runner.ts","../src/share.ts","../src/scanner.ts","../src/engine.ts","../src/embed.ts"],"sourcesContent":["// @sverka/playground — minimal pipeline model. Browser-safe.\n// Mirrors @sverka/workflow Construct API but simplified for browser use.\n\nimport type { PlaygroundFinding } from \"./types.js\";\n\n/** Base class for all constructs. */\nexport abstract class Construct {\n readonly id: string;\n readonly scope: Construct | null;\n\n constructor(scope: Construct | null, id: string) {\n this.id = id;\n this.scope = scope;\n }\n\n /** Full path from root to this construct. */\n get path(): string {\n if (this.scope && this.scope.id !== \"root\") {\n return `${this.scope.path}/${this.id}`;\n }\n return this.id;\n }\n}\n\n/** Root construct representing a project. */\nexport class Project extends Construct {\n readonly pipelines: Pipeline[] = [];\n\n constructor(id: string) {\n super(null, id);\n }\n\n /** Add a pipeline to this project. */\n addPipeline(pipeline: Pipeline): void {\n this.pipelines.push(pipeline);\n }\n}\n\n/** A pipeline containing steps. */\nexport class Pipeline extends Construct {\n readonly steps: Step[] = [];\n readonly entries: Entry[] = [];\n\n constructor(project: Project, id: string) {\n super(project, id);\n project.addPipeline(this);\n }\n\n /** Add a step to this pipeline. */\n addStep(step: Step): void {\n this.steps.push(step);\n }\n\n /** Add an entry to this pipeline. */\n addEntry(entry: Entry): void {\n this.entries.push(entry);\n }\n}\n\n/** Base class for all step types. */\nexport abstract class Step extends Construct {\n readonly dependencies: string[];\n\n constructor(pipeline: Pipeline, id: string, dependencies: string[] = []) {\n super(pipeline, id);\n this.dependencies = dependencies;\n pipeline.addStep(this);\n }\n\n /** Execute this step and return findings. */\n abstract execute(): Promise<PlaygroundFinding[]> | PlaygroundFinding[];\n}\n\n/** A step that runs a JavaScript function in the browser. */\nexport class FunctionStep extends Step {\n readonly fn: () => Promise<PlaygroundFinding[]> | PlaygroundFinding[];\n\n constructor(\n pipeline: Pipeline,\n id: string,\n options: {\n fn: () => Promise<PlaygroundFinding[]> | PlaygroundFinding[];\n dependencies?: string[];\n },\n ) {\n super(pipeline, id, options.dependencies ?? []);\n this.fn = options.fn;\n }\n\n execute(): Promise<PlaygroundFinding[]> | PlaygroundFinding[] {\n return this.fn();\n }\n}\n\n/** An entry point that triggers a pipeline. */\nexport class Entry {\n readonly pipeline: Pipeline;\n readonly id: string;\n readonly trigger: { kind: string };\n readonly roots: string[];\n\n constructor(\n pipeline: Pipeline,\n id: string,\n options: { trigger: { kind: string }; roots: string[] },\n ) {\n this.pipeline = pipeline;\n this.id = id;\n this.trigger = options.trigger;\n this.roots = options.roots;\n pipeline.addEntry(this);\n }\n}\n","// @sverka/playground — pipeline runner. Browser-safe.\n// Executes FunctionSteps and collects findings.\n\nimport type { Project } from \"./pipeline.js\";\nimport type { Pipeline } from \"./pipeline.js\";\nimport type { Step } from \"./pipeline.js\";\nimport type {\n Finding,\n PlaygroundFinding,\n PipelineResult,\n StepResult,\n} from \"./types.js\";\n\n/** Simple deterministic hash for fingerprint generation. Browser-safe. */\nfunction simpleHash(input: string): string {\n let hash = 5381;\n for (let i = 0; i < input.length; i++) {\n hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0;\n }\n return (hash >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/** Convert a PlaygroundFinding to a full Finding with fingerprint and id. */\nfunction toFinding(pf: PlaygroundFinding, stepId: string): Finding {\n // Include checkId (stepId) in fingerprint so identical findings from\n // different checks get distinct fingerprints.\n const fingerprintInput = `${stepId}:${pf.rule}:${pf.file}:${pf.line}`;\n const fingerprint = simpleHash(fingerprintInput);\n const id = `${stepId}:${fingerprint}`;\n return {\n id,\n fingerprint,\n checkId: stepId,\n severity: pf.severity,\n confidence: 0.8,\n message: pf.message,\n rule: pf.rule,\n file: pf.file,\n startLine: pf.line,\n endLine: pf.line,\n source: {\n tool: pf.tool ?? stepId,\n version: null,\n format: \"custom\",\n originalRuleId: pf.rule,\n originalSeverity: null,\n },\n ...(pf.snippet ? { snippet: pf.snippet } : {}),\n };\n}\n\n/** Execute a single step and return its result. */\nasync function executeStep(step: Step): Promise<StepResult> {\n const start = Date.now();\n try {\n const playgroundFindings = await step.execute();\n const findings = playgroundFindings.map((pf) => toFinding(pf, step.id));\n return {\n stepId: step.id,\n status: \"success\",\n findings,\n durationMs: Date.now() - start,\n };\n } catch (e) {\n return {\n stepId: step.id,\n status: \"failure\",\n findings: [],\n durationMs: Date.now() - start,\n error: e instanceof Error ? e.message : String(e),\n };\n }\n}\n\n/**\n * Run all FunctionSteps in a project's pipelines and collect findings.\n * Browser-safe — no Node.js APIs, no child_process, no fs.\n *\n * If a pipeline has entries with `roots`, only steps whose id matches a\n * root are executed. If no entries exist or all entries have empty roots,\n * all steps run.\n */\nexport async function runPipeline(project: Project): Promise<PipelineResult> {\n const start = Date.now();\n const stepResults: StepResult[] = [];\n const allFindings: Finding[] = [];\n\n for (const pipeline of project.pipelines) {\n // Determine which steps to run based on entry roots.\n let stepsToRun = pipeline.steps;\n const entriesWithRoots = pipeline.entries.filter((e) => e.roots.length > 0);\n if (entriesWithRoots.length > 0) {\n const rootSet = new Set<string>();\n for (const entry of entriesWithRoots) {\n for (const root of entry.roots) rootSet.add(root);\n }\n stepsToRun = pipeline.steps.filter((s) => rootSet.has(s.id));\n }\n\n // Execute all selected steps in parallel — dependencies are for\n // ordering, not data flow in the playground.\n const results = await Promise.all(\n stepsToRun.map((step) => executeStep(step)),\n );\n\n for (const result of results) {\n stepResults.push(result);\n allFindings.push(...result.findings);\n }\n }\n\n return {\n steps: stepResults,\n findings: allFindings,\n totalDurationMs: Date.now() - start,\n success: stepResults.every((s) => s.status === \"success\"),\n };\n}\n","// Share links (Spec 53): a ShareableRun serializes into the URL fragment —\n// `#c=` keeps payloads out of server logs and needs zero backend.\n// Encoding: JSON → deflate-raw → base64url.\n\nimport { deflateSync, Inflate } from \"fflate\";\nimport type { Finding, Severity } from \"./types.js\";\n\nexport interface ShareableRun {\n readonly schema: \"sverka.playground/v1\";\n readonly code: string;\n readonly findings?: readonly Finding[];\n}\n\nexport type PlaygroundErrorCode =\n \"TRANSPILE_FAILED\" | \"DECODE_FAILED\" | \"PAYLOAD_TOO_LARGE\";\n\nexport class PlaygroundError extends Error {\n readonly code: PlaygroundErrorCode;\n constructor(code: PlaygroundErrorCode, message: string) {\n super(message);\n this.name = \"PlaygroundError\";\n this.code = code;\n }\n}\n\n/** Fragment lengths are browser-dependent; warn past 32 KB of payload. */\nexport const SHARE_PAYLOAD_WARN_BYTES = 32 * 1024;\n\n/** Hard cap on inflated share payloads — a compressed fragment is cheap to\n * craft into a deflate bomb, so decoding refuses past 1 MB of JSON. */\nconst SHARE_MAX_INFLATED_BYTES = 1024 * 1024;\n\nconst SHARE_PREFIX = \"#c=\";\n\nfunction toBase64Url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nfunction fromBase64Url(text: string): Uint8Array {\n const b64 = text.replace(/-/g, \"+\").replace(/_/g, \"/\");\n // Browser atob() is strict about padding — restore what the encoder\n // stripped. (Node's atob tolerates missing padding, which is why the\n // round-trip test alone did not catch this.)\n const padded = b64 + \"=\".repeat((4 - (b64.length % 4)) % 4);\n const bin = atob(padded);\n const bytes = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);\n return bytes;\n}\n\n/** Inflate with a hard output cap — bounded memory on hostile payloads. */\nfunction inflateBounded(bytes: Uint8Array): Uint8Array {\n const chunks: Uint8Array[] = [];\n let total = 0;\n let overflow = false;\n const inflate = new Inflate((chunk) => {\n total += chunk.length;\n if (total > SHARE_MAX_INFLATED_BYTES) {\n overflow = true;\n return;\n }\n chunks.push(chunk);\n });\n inflate.push(bytes, true);\n if (overflow) {\n throw new PlaygroundError(\n \"PAYLOAD_TOO_LARGE\",\n `share link inflates past ${SHARE_MAX_INFLATED_BYTES} bytes`,\n );\n }\n const out = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nconst SEVERITIES: readonly string[] = [\n \"info\",\n \"low\",\n \"medium\",\n \"high\",\n \"critical\",\n];\n\nconst FINDING_STRING_FIELDS = [\"rule\", \"file\", \"message\"] as const;\nconst FINDING_NUMBER_FIELDS = [\"startLine\", \"endLine\"] as const;\n\n/** Structural check — a shared finding reaches the HTML report, so every\n * field that gets rendered must be the expected scalar. */\nfunction isFindingShape(v: unknown): v is Finding {\n if (typeof v !== \"object\" || v === null) return false;\n const f = v as Record<string, unknown>;\n if (!FINDING_STRING_FIELDS.every((k) => typeof f[k] === \"string\")) {\n return false;\n }\n if (!SEVERITIES.includes(f.severity as Severity)) return false;\n return FINDING_NUMBER_FIELDS.every(\n (k) => typeof f[k] === \"number\" && Number.isFinite(f[k]),\n );\n}\n\n/** Encode a run into a share link. `base` is the page URL the link targets\n * (`location.origin + location.pathname` at runtime). */\nexport function encodeShareLink(run: ShareableRun, base = \"\"): string {\n const payload = toBase64Url(\n deflateSync(new TextEncoder().encode(JSON.stringify(run))),\n );\n return `${base}${SHARE_PREFIX}${payload}`;\n}\n\n/** Extract the payload tail from a `#c=` fragment or a bare payload. */\nfunction shareFragment(hash: string): string {\n const marker = hash.indexOf(SHARE_PREFIX);\n if (marker >= 0) return hash.slice(marker + SHARE_PREFIX.length);\n return hash.startsWith(\"#\") ? hash.slice(1) : hash;\n}\n\n/** Shape assertion — anything else is a corrupt or hostile payload. */\nfunction assertRunShape(parsed: unknown): asserts parsed is ShareableRun {\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n (parsed as ShareableRun).schema !== \"sverka.playground/v1\" ||\n typeof (parsed as ShareableRun).code !== \"string\"\n ) {\n throw new Error(\"payload is not a sverka.playground/v1 run\");\n }\n const findings = (parsed as ShareableRun).findings;\n if (findings === undefined) return;\n if (!Array.isArray(findings) || !findings.every(isFindingShape)) {\n throw new Error(\"payload findings failed shape validation\");\n }\n}\n\n/** Decode a `#c=` fragment back into a ShareableRun. Corrupt, oversized, or\n * shape-invalid payloads throw PlaygroundError — callers fall back to the\n * default template. */\nexport function decodeShareLink(hash: string): ShareableRun {\n try {\n const json = new TextDecoder().decode(\n inflateBounded(fromBase64Url(shareFragment(hash))),\n );\n const parsed: unknown = JSON.parse(json);\n assertRunShape(parsed);\n return parsed;\n } catch (e) {\n if (e instanceof PlaygroundError) throw e;\n throw new PlaygroundError(\n \"DECODE_FAILED\",\n `share link could not be decoded: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n}\n","// @sverka/playground — shared mini-scanner primitives.\n// Char-class checks instead of regexes (a pattern evaluated against user\n// source is ReDoS surface); every helper is string/comment/regex aware so\n// quoted or literal braces never count as syntax. All scans are linear.\n\n// --- char classes --------------------------------------------------------\n\n/** Non-ASCII whitespace code points beyond the Latin-1 range. */\nconst WS_POINTS = new Set([\n 0xa0, 0x1680, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000, 0xfeff,\n]);\n\n/** Whitespace per the JS grammar: \\t \\n \\v \\f \\r space, NBSP, the unicode\n * space separators (0x2000–0x200a), and ZWNBSP. */\nexport function isWsChar(c: string | undefined): boolean {\n if (c === undefined) return false;\n const n = c.codePointAt(0) ?? 0;\n if ((n >= 9 && n <= 13) || n === 32) return true;\n return (n >= 0x2000 && n <= 0x200a) || WS_POINTS.has(n);\n}\n\n/** ASCII letter-or-digit — digits and both letter cases. */\nfunction isAsciiLetterOrDigit(n: number): boolean {\n return (n >= 48 && n <= 57) || (n >= 65 && n <= 90) || (n >= 97 && n <= 122);\n}\n\n/** Identifier-part chars, approximated: ASCII ident chars plus any\n * non-ASCII char that isn't JS whitespace — `export` + a non-ASCII\n * letter keeps its boundary. Over-approximation only ever suppresses\n * a false keyword match — it never corrupts code. */\nexport function isIdentChar(c: string | undefined): boolean {\n if (c === undefined) return false;\n const n = c.codePointAt(0) ?? 0;\n if (n >= 0x80) return !isWsChar(c);\n return isAsciiLetterOrDigit(n) || c === \"_\" || c === \"$\";\n}\n\nexport function isQuote(c: string | undefined): boolean {\n return c === '\"' || c === \"'\" || c === \"`\";\n}\n\n// --- skips ---------------------------------------------------------------\n\nexport function skipIdent(code: string, i: number): number {\n while (i < code.length && isIdentChar(code[i])) i++;\n return i;\n}\n\n/** Index of the `\\n` ending the `//` comment at `at` (code.length if none). */\nexport function lineCommentEnd(code: string, at: number): number {\n const end = code.indexOf(\"\\n\", at + 2);\n return end === -1 ? code.length : end;\n}\n\n/** Index just past the closing star-slash ending the block comment at\n * `at` — the literal marker is never written inside this doc comment\n * or it would terminate the comment itself. */\nexport function blockCommentEnd(code: string, at: number): number {\n const end = code.indexOf(\"*/\", at + 2);\n return end === -1 ? code.length : end + 2;\n}\n\n/** True when `at` starts a `//` or `/*` comment. */\nexport function commentAt(code: string, at: number): boolean {\n return code[at] === \"/\" && (code[at + 1] === \"/\" || code[at + 1] === \"*\");\n}\n\n/** End index of the string literal starting at `at` (quote char).\n * Template literals may nest `${}` expressions — tracked by brace depth. */\nexport function scanString(code: string, at: number): number {\n const quote = code[at];\n let i = at + 1;\n while (i < code.length) {\n const c = code[i];\n if (c === \"\\\\\") {\n i += 2;\n continue;\n }\n if (c === quote) return i + 1;\n if (quote === \"`\" && c === \"$\" && code[i + 1] === \"{\") {\n i = scanTemplateExpr(code, i + 1);\n continue;\n }\n i++;\n }\n return i;\n}\n\n/** End index of a regex literal starting at `at` (`/`): closes at the\n * first unescaped `/` outside a `[]` class, then consumes flags. Stops at\n * a newline or EOF when unterminated — the evaluator reports the syntax. */\nexport function scanRegex(code: string, at: number): number {\n let i = at + 1;\n let inClass = false;\n while (i < code.length) {\n const c = code[i];\n if (c === \"\\\\\") {\n i += 2;\n continue;\n }\n if (c === \"[\") inClass = true;\n else if (c === \"]\") inClass = false;\n else if (c === \"/\" && !inClass) return skipIdent(code, i + 1);\n else if (c === \"\\n\") return i;\n i++;\n }\n return i;\n}\n\n/** End index of the opaque region at `i`, or i+1 for ordinary chars —\n * plus whether the region was a string literal (an operand). */\nexport function scanOpaqueEnd(\n code: string,\n i: number,\n): { end: number; isString: boolean } {\n const c = code[i];\n if (isQuote(c)) return { end: scanString(code, i), isString: true };\n if (c === \"/\" && code[i + 1] === \"/\")\n return { end: lineCommentEnd(code, i), isString: false };\n if (c === \"/\" && code[i + 1] === \"*\")\n return { end: blockCommentEnd(code, i), isString: false };\n return { end: i + 1, isString: false };\n}\n\n/** Keywords and contextual keywords after which `/` opens a regex rather\n * than divides. Everything else — plain identifiers plus literals such\n * as `this`/`true` — ends an operand, making `/` a division. */\nconst NON_OPERAND_WORDS = new Set([\n \"await\",\n \"break\",\n \"case\",\n \"catch\",\n \"class\",\n \"const\",\n \"continue\",\n \"debugger\",\n \"default\",\n \"delete\",\n \"do\",\n \"else\",\n \"enum\",\n \"export\",\n \"extends\",\n \"finally\",\n \"for\",\n \"function\",\n \"if\",\n \"implements\",\n \"import\",\n \"in\",\n \"instanceof\",\n \"interface\",\n \"let\",\n \"new\",\n \"package\",\n \"private\",\n \"protected\",\n \"public\",\n \"return\",\n \"static\",\n \"switch\",\n \"throw\",\n \"try\",\n \"typeof\",\n \"var\",\n \"void\",\n \"while\",\n \"with\",\n \"yield\",\n \"as\",\n \"async\",\n \"from\",\n \"get\",\n \"of\",\n \"set\",\n \"type\",\n \"declare\",\n]);\n\n/** True when `word` ends an operand — a following `/` is division. */\nexport function operandAfterWord(word: string): boolean {\n return !NON_OPERAND_WORDS.has(word);\n}\n\ninterface TokenStep {\n /** Index just past the consumed token. */\n end: number;\n /** Whether the token ends an operand — a following `/` divides. */\n operandEnd: boolean;\n /** +1 for `{`, −1 for `}`, 0 otherwise. */\n brace: -1 | 0 | 1;\n}\n\n/** Consume one token at `i` for the brace-matching scans: opaque regions\n * and regex literals advance wholesale, identifiers update operand\n * state from the keyword table, braces report a depth delta. */\nfunction scanToken(code: string, i: number, operandEnd: boolean): TokenStep {\n const opaque = scanOpaqueEnd(code, i);\n if (opaque.end > i + 1) {\n return {\n end: opaque.end,\n operandEnd: opaque.isString || operandEnd,\n brace: 0,\n };\n }\n const ch = code[i];\n if (ch === \"/\" && !operandEnd) {\n return { end: scanRegex(code, i), operandEnd: true, brace: 0 };\n }\n if (isWsChar(ch)) return { end: i + 1, operandEnd, brace: 0 };\n if (isIdentChar(ch)) {\n const wend = skipIdent(code, i);\n return {\n end: wend,\n operandEnd: operandAfterWord(code.slice(i, wend)),\n brace: 0,\n };\n }\n if (ch === \"{\") return { end: i + 1, operandEnd: false, brace: 1 };\n if (ch === \"}\") return { end: i + 1, operandEnd: true, brace: -1 };\n return { end: i + 1, operandEnd: false, brace: 0 };\n}\n\n/**\n * End index of the `${` expression whose `{` is at `at`. Strings,\n * comments, nested templates, and regex literals inside the expression\n * are all skipped correctly: `` `${ x[\"`\"] }` `` and `` `${ /\"/ }` ``\n * must not treat literal text as syntax.\n */\nexport function scanTemplateExpr(code: string, at: number): number {\n let depth = 1;\n let i = at + 1;\n let operandEnd = false;\n while (i < code.length && depth > 0) {\n const step = scanToken(code, i, operandEnd);\n depth += step.brace;\n operandEnd = step.operandEnd;\n i = step.end;\n }\n return i;\n}\n\n/**\n * Index of the `}` matching the `{` at `open`; -1 when unbalanced.\n * Operand tracking decides whether a `/` divides or opens a regex —\n * braces inside `/[{]/` never reach the depth counter.\n */\nexport function matchBrace(code: string, open: number): number {\n let depth = 0;\n let i = open;\n let operandEnd = false;\n while (i < code.length) {\n const step = scanToken(code, i, operandEnd);\n depth += step.brace;\n operandEnd = step.operandEnd;\n i = step.end;\n if (step.brace === -1 && depth === 0) return i - 1;\n }\n return -1;\n}\n\n/** `word` starting at `at` with an identifier boundary on the right. */\nexport function keywordAt(code: string, at: number, word: string): boolean {\n return code.startsWith(word, at) && !isIdentChar(code[at + word.length]);\n}\n\n/** Skip whitespace and comments (including newlines). */\nexport function skipTrivia(code: string, at: number): number {\n let i = at;\n for (;;) {\n const c = code[i];\n if (isWsChar(c)) {\n i++;\n continue;\n }\n if (c === \"/\" && code[i + 1] === \"/\") {\n i = lineCommentEnd(code, i);\n continue;\n }\n if (c === \"/\" && code[i + 1] === \"*\") {\n i = blockCommentEnd(code, i);\n continue;\n }\n return i;\n }\n}\n","// @sverka/playground — shared engine plumbing. Browser-safe.\n// Code template, user-code evaluation, and a timeout-wrapped run used by\n// both the full playground app and the embeddable mountRunner.\n\nimport { Project, Pipeline, FunctionStep, Entry } from \"./pipeline.js\";\nimport { runPipeline } from \"./runner.js\";\nimport {\n commentAt,\n isIdentChar,\n isWsChar,\n keywordAt,\n matchBrace,\n operandAfterWord,\n scanOpaqueEnd,\n scanRegex,\n scanString,\n skipIdent,\n skipTrivia,\n} from \"./scanner.js\";\n\n/** Default template shown in the editor. */\nexport const DEFAULT_CODE = [\n 'import { Project, Pipeline, FunctionStep, Entry } from \"@sverka/playground\";',\n \"\",\n 'const proj = new Project(\"demo\");',\n 'const checks = new Pipeline(proj, \"checks\");',\n \"\",\n 'new FunctionStep(checks, \"lint\", {',\n \" fn: () => [\",\n ' { rule: \"no-unused-vars\", file: \"src/index.ts\", line: 5, severity: \"high\", message: \"Variable \\'x\\' is declared but never used\" },',\n ' { rule: \"no-console\", file: \"src/utils.ts\", line: 12, severity: \"medium\", message: \"Unexpected console.log statement\" },',\n \" ],\",\n \"});\",\n \"\",\n 'new FunctionStep(checks, \"typecheck\", {',\n \" fn: () => [\",\n ' { rule: \"ts2322\", file: \"src/types.ts\", line: 8, severity: \"critical\", message: \"Type \\'string\\' is not assignable to type \\'number\\'\" },',\n \" ],\",\n \"});\",\n \"\",\n 'new FunctionStep(checks, \"test\", {',\n \" fn: () => [\",\n ' { rule: \"assertion-failed\", file: \"test/index.test.ts\", line: 23, severity: \"high\", message: \"Expected 5 but got 3\" },',\n ' { rule: \"assertion-failed\", file: \"test/index.test.ts\", line: 45, severity: \"low\", message: \"Expected \\'hello\\' but got \\'world\\'\" },',\n \" ],\",\n \"});\",\n \"\",\n 'new Entry(checks, \"on-push\", { trigger: { kind: \"push\" }, roots: [\"test\"] });',\n \"\",\n \"export default proj;\",\n \"\",\n].join(\"\\n\");\n\n/** Escape HTML special characters to prevent XSS. */\nexport function escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#39;\");\n}\n\n/** +1/−1 for bracket chars, 0 otherwise. */\nfunction bracketDelta(c: string | undefined): number {\n if (c === \"{\" || c === \"(\" || c === \"[\") return 1;\n if (c === \"}\" || c === \")\" || c === \"]\") return -1;\n return 0;\n}\n\n/** Chars that end an import statement at depth ≤0: `;` (consumed) or a\n * newline after the module-specifier string (left in place). */\nfunction stmtTerminator(\n c: string | undefined,\n depth: number,\n specSeen: boolean,\n): \"past\" | \"at\" | null {\n if (c === \";\" && depth <= 0) return \"past\";\n if (c === \"\\n\" && depth <= 0 && specSeen) return \"at\";\n return null;\n}\n\n/** End index of the statement starting at `at`: first top-level `;`, or\n * the first newline after the module-specifier string (covers multiline\n * `import {…}\\n from \"x\"` and `import \"x\"` without a semicolon). */\nfunction scanStatementEnd(code: string, at: number): number {\n let i = at;\n let depth = 0;\n let specSeen = false;\n while (i < code.length) {\n const opaque = scanOpaqueEnd(code, i);\n if (opaque.end > i + 1) {\n specSeen = specSeen || opaque.isString;\n i = opaque.end;\n continue;\n }\n const term = stmtTerminator(code[i], depth, specSeen);\n if (term === \"past\") return i + 1;\n if (term === \"at\") return i;\n depth += bracketDelta(code[i]);\n i++;\n }\n return i;\n}\n\n/** Same-line statement tail: horizontal whitespace then an optional `;`.\n * Never eats the newline — the following line is the next statement. */\nfunction scanStmtTail(code: string, at: number): number {\n let i = at;\n while (i < code.length && (code[i] === \" \" || code[i] === \"\\t\")) i++;\n if (code[i] === \";\") i++;\n return i;\n}\n\n/** End index of the `from \"…\"` clause starting after `from`, or `at` when\n * the clause is malformed — let the evaluator complain. */\nfunction scanFromClause(code: string, next: number): number {\n const specAt = skipTrivia(code, next + 4);\n const q = code[specAt];\n if (q === '\"' || q === \"'\") {\n return scanStmtTail(code, scanString(code, specAt));\n }\n return next;\n}\n\n/** End index of `export * [as ns] [from \"…\"]` — the `*` is at `at`. */\nfunction scanExportStar(code: string, at: number): number {\n let i = skipTrivia(code, at + 1);\n if (keywordAt(code, i, \"as\")) {\n i = skipTrivia(code, i + 2);\n i = skipIdent(code, i);\n }\n const next = skipTrivia(code, i);\n if (keywordAt(code, next, \"from\")) return scanFromClause(code, next);\n return scanStmtTail(code, i);\n}\n\n/**\n * End index of `export {…}` / `export * [as ns] [from \"…\"]` starting at\n * the `{` or `*`. A bare list ends at its `;` or newline — unlike an\n * import, a newline after the closing brace must end the statement or\n * the next line would be eaten with it.\n */\nfunction scanExportListEnd(code: string, at: number): number {\n if (code[at] === \"*\") return scanExportStar(code, at);\n const close = matchBrace(code, at);\n const i = close < 0 ? code.length : close + 1;\n const next = skipTrivia(code, i);\n if (keywordAt(code, next, \"from\")) return scanFromClause(code, next);\n return scanStmtTail(code, i);\n}\n\ninterface ScanState {\n i: number;\n stmtStart: boolean;\n exportDefaultDone: boolean;\n /** `block` vs `expr` — separates block braces from object literals, so\n * `{ export: 1 }` is a key, not a statement. */\n stack: (\"block\" | \"expr\")[];\n /** True when the previous token ended an operand — `/` then divides.\n * Regex literals never update the delimiter stack, so `/[{]/` can't\n * leave an unclosed `{` that keeps `export default` alive to eval. */\n operandEnd: boolean;\n}\n\nfunction atStmtLevel(st: ScanState): boolean {\n return st.stack.length === 0 || st.stack.at(-1) === \"block\";\n}\n\n/** Copy a string literal or comment verbatim into `out`. */\nfunction copyOpaque(code: string, st: ScanState, out: string[]): boolean {\n const opaque = scanOpaqueEnd(code, st.i);\n if (opaque.end <= st.i + 1) return false;\n out.push(code.slice(st.i, opaque.end));\n if (opaque.isString) {\n st.stmtStart = false;\n st.operandEnd = true;\n }\n st.i = opaque.end;\n return true;\n}\n\n/** Handle `{}`/`()`/`[]` — pushes onto the block/expr stack and sets the\n * operand context: `}` closing an object literal ends a value, `}`\n * closing a block ends a statement. */\nfunction applyBracket(code: string, st: ScanState, out: string[]): boolean {\n const c = code[st.i];\n if (c === \"{\") {\n // A `{` at statement position opens a block; anywhere else it's an\n // object literal. Inside a block the next token is a statement;\n // inside an object it's a key — `export:` there is not a keyword.\n st.stack.push(st.stmtStart ? \"block\" : \"expr\");\n st.operandEnd = false;\n } else if (c === \"}\") {\n const popped = st.stack.pop();\n st.stmtStart = popped === \"block\";\n st.operandEnd = popped === \"expr\";\n } else if (c === \"(\" || c === \"[\") {\n st.stack.push(\"expr\");\n st.stmtStart = false;\n st.operandEnd = false;\n } else if (c === \")\" || c === \"]\") {\n st.stack.pop();\n st.stmtStart = false;\n st.operandEnd = true;\n } else {\n return false;\n }\n out.push(c ?? \"\");\n st.i++;\n return true;\n}\n\n/** Handle `\\n`, `;`, and whitespace — statement boundaries at statement\n * level only (a `;` inside `for (;;)` doesn't count). A newline never\n * resets operand context: `foo\\n/bar/` divides, per ASI rules. */\nfunction applyTerminator(code: string, st: ScanState, out: string[]): boolean {\n const c = code[st.i];\n if (c !== \";\" && c !== \"\\n\" && !isWsChar(c)) return false;\n if (c === \";\") {\n if (atStmtLevel(st)) st.stmtStart = true;\n st.operandEnd = false;\n } else if (c === \"\\n\" && atStmtLevel(st)) {\n st.stmtStart = true;\n }\n out.push(c ?? \"\");\n st.i++;\n return true;\n}\n\n/** Handle `/` — a regex literal when an operand is expected, a division\n * operator when one just ended. Comments never reach here (copyOpaque\n * already consumed them). */\nfunction applySlash(code: string, st: ScanState, out: string[]): boolean {\n if (code[st.i] !== \"/\" || commentAt(code, st.i)) return false;\n if (st.operandEnd) {\n out.push(\"/\");\n st.i++;\n st.operandEnd = false;\n } else {\n const end = scanRegex(code, st.i);\n out.push(code.slice(st.i, end));\n st.i = end;\n st.operandEnd = true;\n }\n st.stmtStart = false;\n return true;\n}\n\n/** Handle an `import` keyword at statement start. Dynamic `import (…)`\n * and `import.meta` pass through verbatim — whitespace and comments may\n * sit between the keyword and the paren. */\nfunction tryImport(code: string, st: ScanState, out: string[]): boolean {\n if (!st.stmtStart || !keywordAt(code, st.i, \"import\")) return false;\n const nc = code[skipTrivia(code, st.i + 6)];\n if (nc === \"(\" || nc === \".\") {\n out.push(\"import\");\n st.i += 6;\n st.stmtStart = false;\n st.operandEnd = true;\n return true;\n }\n st.i = scanStatementEnd(code, st.i + 6);\n st.stmtStart = false;\n st.operandEnd = false;\n return true;\n}\n\n/** Handle `export default` (→ `return`), `export {…}`/`export *` (removed)\n * and `export <decl>` (keyword dropped — the eval context is a function\n * body, where `export const` would be a syntax error). */\nfunction tryExport(code: string, st: ScanState, out: string[]): boolean {\n if (!st.stmtStart || !keywordAt(code, st.i, \"export\")) return false;\n const j = skipTrivia(code, st.i + 6);\n if (\n !st.exportDefaultDone &&\n code.startsWith(\"default\", j) &&\n !isIdentChar(code[j + 7])\n ) {\n // `return` must touch the expression — a newline between `default`\n // and the value would trigger ASI and return undefined.\n out.push(\"return \");\n st.exportDefaultDone = true;\n st.i = skipTrivia(code, j + 7);\n st.stmtStart = false;\n st.operandEnd = false;\n return true;\n }\n if (code[j] === \"{\" || code[j] === \"*\") {\n st.i = scanExportListEnd(code, j);\n st.stmtStart = false;\n st.operandEnd = false;\n return true;\n }\n st.i = j;\n st.stmtStart = false;\n st.operandEnd = false;\n return true;\n}\n\n/** Emit an identifier run — keyword table decides whether `/` after it\n * divides (`foo /x/`) or opens a regex (`return /x/`). */\nfunction tryWord(code: string, st: ScanState, out: string[]): boolean {\n if (!isIdentChar(code[st.i])) return false;\n const wend = skipIdent(code, st.i);\n out.push(code.slice(st.i, wend));\n st.operandEnd = operandAfterWord(code.slice(st.i, wend));\n st.stmtStart = false;\n st.i = wend;\n return true;\n}\n\n/** Char handlers tried in order — first true wins. Any char left over is\n * copied verbatim as a non-operand punctuation token. */\nconst HANDLERS: ((code: string, st: ScanState, out: string[]) => boolean)[] = [\n copyOpaque,\n applyBracket,\n applyTerminator,\n applySlash,\n tryImport,\n tryExport,\n tryWord,\n];\n\nfunction advanceScan(code: string, st: ScanState, out: string[]): void {\n for (const handle of HANDLERS) {\n if (handle(code, st, out)) return;\n }\n st.stmtStart = false;\n st.operandEnd = false;\n out.push(code[st.i] ?? \"\");\n st.i++;\n}\n\n/**\n * Strip import/export statements from user code for eval — a linear\n * single-pass scan, so multiline imports and comments/strings are handled\n * and pathological spacing cannot cause quadratic backtracking.\n *\n * `import …` statements are removed whole. `export default` becomes\n * `return`, `export {…}`/`export *` are removed, and `export` before a\n * declaration is dropped (the eval context is a function body, where\n * `export const` would be a syntax error).\n *\n * Note: TypeScript-specific syntax (type annotations, interfaces, enums)\n * is not stripped — the playground uses Monaco's TypeScript language mode\n * for editing, but evaluation is plain JavaScript. Users should write\n * JS-compatible code or use the `as any` escape hatch sparingly.\n */\nexport function preprocessCode(code: string): string {\n const out: string[] = [];\n const st: ScanState = {\n i: 0,\n stmtStart: true,\n exportDefaultDone: false,\n stack: [],\n operandEnd: false,\n };\n while (st.i < code.length) {\n if (code[st.i] === undefined) break;\n advanceScan(code, st, out);\n }\n return out.join(\"\");\n}\n\n/**\n * Evaluate user code and return the Project.\n *\n * SECURITY: This uses `new Function()` to execute user-provided code.\n * This is intentional — the playground is a local development tool where\n * the user writes and runs their own pipeline code. The code runs in the\n * browser's main page context (not a sandboxed iframe or worker) because\n * the playground needs to support synchronous FunctionStep execution and\n * direct access to the pipeline constructs. This is the same trust model\n * as a local REPL or `node -e`.\n */\nexport function evaluateUserCode(code: string): Project {\n const processed = preprocessCode(code);\n // Dynamic code execution is intentional for the playground sandbox.\n // SonarCloud S1523: safe — user code runs in the browser sandbox with the\n // same trust model as a local REPL or `node -e` (see comment above).\n const params = [\"Project\", \"Pipeline\", \"FunctionStep\", \"Entry\", processed];\n // NOSONAR suppresses only issues on the marker's own line — it must\n // trail the `new Function` callee, not sit inside the argument list.\n const fn = new Function(...params); // NOSONAR — intentional dynamic evaluation in sandbox\n const result = fn(Project, Pipeline, FunctionStep, Entry); // NOSONAR\n if (!(result instanceof Project)) {\n throw new TypeError(\"Code must export a Project instance\");\n }\n return result as Project;\n}\n\n/**\n * Run a pipeline with a timeout. If a step returns a never-resolving\n * promise, the timeout ensures the UI recovers instead of staying\n * stuck in \"Running\" state forever.\n */\nexport async function runPipelineWithTimeout(\n project: Project,\n timeoutMs: number,\n): Promise<Awaited<ReturnType<typeof runPipeline>>> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n runPipeline(project),\n new Promise<never>((_, reject) => {\n timer = setTimeout(\n () => reject(new Error(`Pipeline timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n }),\n ]);\n } finally {\n // Without this, every completed run leaves a live timer that rejects\n // into an unhandled rejection timeoutMs later.\n if (timer !== undefined) clearTimeout(timer);\n }\n}\n","// @sverka/playground — mountRunner embed API (Spec 53).\n// Docs pages mount a self-contained runner: code (editable or readonly),\n// a Run button, status line, and a findings iframe. Zero backend — the\n// runner evaluates pipeline code in-page under the same trust model as\n// the full playground (see engine.ts).\n\nimport { generateSarifHtml } from \"@sverka/sarif-viewer-web/html-generator\";\nimport type { PipelineResult } from \"./types.js\";\nimport {\n DEFAULT_CODE,\n escapeHtml,\n evaluateUserCode,\n runPipelineWithTimeout,\n} from \"./engine.js\";\n\nexport interface MountRunnerOptions {\n /** Initial editor source. Defaults to the playground template. */\n readonly code?: string;\n /** Render the source as a fixed listing instead of an editable field. */\n readonly readonly?: boolean;\n /** Run `code` immediately on mount. Defaults to false — embed code may\n * come from a share link or CMS, so mounting never executes it unless\n * the embedder opts in or the user clicks Run. */\n readonly autoRun?: boolean;\n /** Called once per completed run. */\n readonly onRun?: (result: PipelineResult) => void;\n}\n\nexport interface MountedRunner {\n /** Remove the runner's DOM and listeners. */\n dispose(): void;\n}\n\nconst STYLE = `\n font-family: monospace; font-size: 12px;\n background: #0d1117; color: #c9d1d9;\n border: 1px solid #30363d; border-radius: 6px;\n display: flex; flex-direction: column; overflow: hidden;\n`;\n\nfunction createHeader(doc: Document): {\n header: HTMLElement;\n runBtn: HTMLButtonElement;\n status: HTMLSpanElement;\n} {\n const header = doc.createElement(\"div\");\n header.setAttribute(\n \"style\",\n \"display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.6rem;background:#161b22;border-bottom:1px solid #30363d;\",\n );\n const runBtn = doc.createElement(\"button\");\n runBtn.textContent = \"Run\";\n runBtn.setAttribute(\n \"style\",\n \"padding:0.2rem 0.8rem;border:1px solid #30363d;border-radius:6px;background:#1f6feb;color:#fff;cursor:pointer;font-size:0.8rem;\",\n );\n const status = doc.createElement(\"span\");\n status.setAttribute(\"style\", \"font-size:0.75rem;color:#8b949e;\");\n status.textContent = \"Ready\";\n header.append(runBtn, status);\n return { header, runBtn, status };\n}\n\nfunction createCodeEl(\n doc: Document,\n code: string,\n readonly: boolean,\n): HTMLTextAreaElement | HTMLElement {\n if (readonly) {\n const pre = doc.createElement(\"pre\");\n pre.setAttribute(\n \"style\",\n \"margin:0;padding:0.75rem;overflow:auto;max-height:16rem;white-space:pre;\",\n );\n pre.textContent = code;\n return pre;\n }\n const textarea = doc.createElement(\"textarea\");\n textarea.value = code;\n textarea.setAttribute(\n \"style\",\n \"flex:0 0 12rem;width:100%;background:#0d1117;color:#c9d1d9;border:none;border-bottom:1px solid #30363d;padding:0.75rem;resize:vertical;outline:none;font:inherit;\",\n );\n return textarea;\n}\n\nfunction createFrame(doc: Document): HTMLIFrameElement {\n const frame = doc.createElement(\"iframe\");\n // Shared findings render via srcdoc — sandboxed onto an opaque origin so a\n // crafted payload can never touch the host page; allow-scripts keeps the\n // report's own viewer interactivity (search/filter/sort) working.\n frame.setAttribute(\"sandbox\", \"allow-scripts\");\n frame.setAttribute(\n \"style\",\n \"flex:1;min-height:10rem;width:100%;border:none;background:#0d1117;\",\n );\n return frame;\n}\n\nconst BODY_DARK =\n '<body style=\"background:#0d1117;font-family:monospace;padding:1rem;';\n\n/** Failure banner HTML — string concat keeps user data flowing through\n * escapeHtml explicitly, never through template interpolation. */\nfunction failureBanner(result: PipelineResult): string {\n const failed = result.steps.filter((s) => s.status === \"failure\");\n const lines =\n failed.map((s) => `${s.stepId}: ${s.error ?? \"failed\"}`).join(\"\\n\") ||\n `${result.steps.length} steps failed`;\n return [\n '<div style=\"background:#3d1d1f;color:#f85149;padding:0.6rem 1rem;font-family:monospace;border-bottom:1px solid #30363d;\">',\n \"<strong>Run failed</strong>\",\n '<pre style=\"margin:0.4rem 0 0;white-space:pre-wrap;\">',\n escapeHtml(lines),\n \"</pre></div>\",\n ].join(\"\");\n}\n\n/** Document shown when the run failed — partial findings still render\n * underneath the banner so finished steps aren't hidden by the error. */\nfunction failureDoc(result: PipelineResult, banner: string): string {\n if (result.findings.length === 0) {\n return (\n \"<!DOCTYPE html><html>\" +\n BODY_DARK +\n 'color:#f85149;\">' +\n banner +\n \"</body></html>\"\n );\n }\n const sarif = generateSarifHtml(result.findings);\n return sarif.includes(\"<body>\")\n ? sarif.replace(\"<body>\", \"<body>\" + banner)\n : banner + sarif;\n}\n\n/** Document shown for a clean run with zero findings. */\nfunction successDoc(result: PipelineResult): string {\n return [\n \"<!DOCTYPE html><html>\",\n BODY_DARK,\n 'color:#3fb950;\">',\n \"<h3>No findings — all checks passed</h3><p>\",\n escapeHtml(String(result.steps.length)),\n \" steps in \",\n escapeHtml(String(result.totalDurationMs)),\n \"ms</p></body></html>\",\n ].join(\"\");\n}\n\n/** Document shown when the runner itself threw (eval or pipeline error). */\nfunction errorDoc(e: unknown): string {\n const msg = e instanceof Error ? e.message : String(e);\n return [\n \"<!DOCTYPE html><html>\",\n BODY_DARK,\n 'color:#f85149;\">',\n \"<h3>Error</h3><pre>\",\n escapeHtml(msg),\n \"</pre></body></html>\",\n ].join(\"\");\n}\n\nfunction statusLine(result: PipelineResult): string {\n if (result.success)\n return `${result.findings.length} findings · ${result.totalDurationMs}ms`;\n const failures = result.steps.filter((s) => s.status === \"failure\").length;\n return `${failures} step(s) failed · ${result.findings.length} findings`;\n}\n\n/** Render a completed run into the findings frame. */\nfunction renderResult(\n result: PipelineResult,\n showFindings: (html: string) => void,\n): void {\n if (!result.success) {\n showFindings(failureDoc(result, failureBanner(result)));\n return;\n }\n if (result.findings.length === 0) {\n showFindings(successDoc(result));\n return;\n }\n showFindings(generateSarifHtml(result.findings));\n}\n\n/**\n * Mount a self-contained runner inside `el`. `dispose()` tears the mount\n * down. With `autoRun: true` the runner executes `code` once on mount;\n * otherwise it waits for the Run button — embeds never auto-execute code\n * the embedder didn't explicitly allow.\n */\nexport function mountRunner(\n el: HTMLElement,\n opts: MountRunnerOptions = {},\n): MountedRunner {\n const doc = el.ownerDocument;\n const code = opts.code ?? DEFAULT_CODE;\n\n const root = doc.createElement(\"div\");\n root.setAttribute(\"style\", STYLE);\n const { header, runBtn, status } = createHeader(doc);\n const codeEl = createCodeEl(doc, code, opts.readonly === true);\n const frame = createFrame(doc);\n root.append(header, codeEl, frame);\n el.appendChild(root);\n\n // Branch on the option, not instanceof — a container owned by another\n // window would produce a cross-realm textarea that fails the check.\n const getCode = (): string =>\n opts.readonly === true ? code : (codeEl as HTMLTextAreaElement).value;\n\n const run = async (): Promise<void> => {\n runBtn.disabled = true;\n status.textContent = \"Running...\";\n let result: PipelineResult;\n try {\n const project = evaluateUserCode(getCode());\n result = await runPipelineWithTimeout(project, 30_000);\n renderResult(result, (html) => {\n frame.srcdoc = html;\n });\n status.textContent = statusLine(result);\n } catch (e) {\n frame.srcdoc = errorDoc(e);\n status.textContent = \"Error\";\n runBtn.disabled = false;\n return;\n }\n runBtn.disabled = false;\n // Outside the try — a throwing callback must not corrupt the findings\n // view or get misreported as a runner failure.\n opts.onRun?.(result);\n };\n\n const onClick = (): void => {\n void run();\n };\n runBtn.addEventListener(\"click\", onClick);\n if (opts.autoRun === true) void run();\n\n return {\n dispose() {\n runBtn.removeEventListener(\"click\", onClick);\n root.remove();\n },\n };\n}\n"],"mappings":"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"}
package/index.html CHANGED
@@ -124,6 +124,7 @@
124
124
  <h1>Sverka Playground</h1>
125
125
  <div class="actions">
126
126
  <span class="status" id="status">Ready</span>
127
+ <button id="share-btn" title="Copy share link">Share</button>
127
128
  <button id="run-btn">Run</button>
128
129
  </div>
129
130
  </header>
@@ -135,7 +136,11 @@
135
136
  <div class="splitter" id="splitter"></div>
136
137
  <div class="findings-panel">
137
138
  <div class="panel-header">Findings</div>
138
- <iframe id="findings-frame" title="Findings"></iframe>
139
+ <iframe
140
+ id="findings-frame"
141
+ title="Findings"
142
+ sandbox="allow-scripts"
143
+ ></iframe>
139
144
  </div>
140
145
  </div>
141
146
  <!-- Monaco editor loaded from CDN at runtime (see app.ts).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sverka/playground",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Sverka Playground — browser sandbox for building and running check pipelines",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,12 +37,14 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@sverka/sarif-viewer-web": "^0.2.10"
40
+ "@sverka/sarif-viewer-web": "^0.2.12",
41
+ "fflate": "0.8.3"
41
42
  },
42
43
  "devDependencies": {
44
+ "jsdom": "^30.1.2",
43
45
  "tsdown": "^0.23.0",
44
46
  "typescript": "^5.8.0",
45
- "vitest": "^5.0.0",
46
- "vite": "^8.3.1"
47
+ "vite": "^8.3.1",
48
+ "vitest": "^5.0.0"
47
49
  }
48
50
  }