@sverka/playground 0.2.11 → 0.2.13

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.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Inflate, deflateSync } from "fflate";
2
+ import { generateSarifHtml } from "@sverka/sarif-viewer-web/html-generator";
2
3
  //#region src/pipeline.ts
3
4
  /** Base class for all constructs. */
4
5
  var Construct = class {
@@ -269,6 +270,1158 @@ function decodeShareLink(hash) {
269
270
  }
270
271
  }
271
272
  //#endregion
272
- export { Entry, FunctionStep, Pipeline, PlaygroundError, Project, SHARE_PAYLOAD_WARN_BYTES, decodeShareLink, encodeShareLink, runPipeline };
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
+ /** True when `at` opens a string literal or a comment. */
331
+ function isOpaqueStart(code, at) {
332
+ return isQuote(code[at]) || commentAt(code, at);
333
+ }
334
+ /** End index of the string literal starting at `at` (quote char).
335
+ * Template literals may nest `${}` expressions — tracked by brace depth. */
336
+ function scanString(code, at) {
337
+ const quote = code[at];
338
+ let i = at + 1;
339
+ while (i < code.length) {
340
+ const c = code[i];
341
+ if (c === "\\") {
342
+ i += 2;
343
+ continue;
344
+ }
345
+ if (c === quote) return i + 1;
346
+ if (quote === "`" && c === "$" && code[i + 1] === "{") {
347
+ i = scanTemplateExpr(code, i + 1);
348
+ continue;
349
+ }
350
+ i++;
351
+ }
352
+ return i;
353
+ }
354
+ /** End index of a regex literal starting at `at` (`/`): closes at the
355
+ * first unescaped `/` outside a `[]` class, then consumes flags. Stops at
356
+ * a newline or EOF when unterminated — the evaluator reports the syntax. */
357
+ function scanRegex(code, at) {
358
+ let i = at + 1;
359
+ let inClass = false;
360
+ while (i < code.length) {
361
+ const c = code[i];
362
+ if (c === "\\") {
363
+ i += 2;
364
+ continue;
365
+ }
366
+ if (c === "[") inClass = true;
367
+ else if (c === "]") inClass = false;
368
+ else if (c === "/" && !inClass) return skipIdent(code, i + 1);
369
+ else if (c === "\n") return i;
370
+ i++;
371
+ }
372
+ return i;
373
+ }
374
+ /** End index of the opaque region at `i`, or i+1 for ordinary chars —
375
+ * plus whether the region was a string literal (an operand). */
376
+ function scanOpaqueEnd(code, i) {
377
+ const c = code[i];
378
+ if (isQuote(c)) return {
379
+ end: scanString(code, i),
380
+ isString: true
381
+ };
382
+ if (c === "/" && code[i + 1] === "/") return {
383
+ end: lineCommentEnd(code, i),
384
+ isString: false
385
+ };
386
+ if (c === "/" && code[i + 1] === "*") return {
387
+ end: blockCommentEnd(code, i),
388
+ isString: false
389
+ };
390
+ return {
391
+ end: i + 1,
392
+ isString: false
393
+ };
394
+ }
395
+ /** Keywords and contextual keywords after which `/` opens a regex rather
396
+ * than divides. Everything else — plain identifiers plus literals such
397
+ * as `this`/`true` — ends an operand, making `/` a division. */
398
+ const NON_OPERAND_WORDS = /* @__PURE__ */ new Set([
399
+ "await",
400
+ "break",
401
+ "case",
402
+ "catch",
403
+ "class",
404
+ "const",
405
+ "continue",
406
+ "debugger",
407
+ "default",
408
+ "delete",
409
+ "do",
410
+ "else",
411
+ "enum",
412
+ "export",
413
+ "extends",
414
+ "finally",
415
+ "for",
416
+ "function",
417
+ "if",
418
+ "implements",
419
+ "import",
420
+ "in",
421
+ "instanceof",
422
+ "interface",
423
+ "let",
424
+ "new",
425
+ "package",
426
+ "private",
427
+ "protected",
428
+ "public",
429
+ "return",
430
+ "static",
431
+ "switch",
432
+ "throw",
433
+ "try",
434
+ "typeof",
435
+ "var",
436
+ "void",
437
+ "while",
438
+ "with",
439
+ "yield",
440
+ "as",
441
+ "async",
442
+ "from",
443
+ "get",
444
+ "of",
445
+ "set",
446
+ "type",
447
+ "declare"
448
+ ]);
449
+ /** True when `word` ends an operand — a following `/` is division. */
450
+ function operandAfterWord(word) {
451
+ return !NON_OPERAND_WORDS.has(word);
452
+ }
453
+ /** Consume one token at `i` for the brace-matching scans: opaque regions
454
+ * and regex literals advance wholesale, identifiers update operand
455
+ * state from the keyword table, braces report a depth delta. */
456
+ function scanToken(code, i, operandEnd) {
457
+ const opaque = scanOpaqueEnd(code, i);
458
+ if (opaque.end > i + 1) return {
459
+ end: opaque.end,
460
+ operandEnd: opaque.isString || operandEnd,
461
+ brace: 0
462
+ };
463
+ const ch = code[i];
464
+ if (ch === "/" && !operandEnd) return {
465
+ end: scanRegex(code, i),
466
+ operandEnd: true,
467
+ brace: 0
468
+ };
469
+ if (isWsChar(ch)) return {
470
+ end: i + 1,
471
+ operandEnd,
472
+ brace: 0
473
+ };
474
+ if (isIdentChar(ch)) {
475
+ const wend = skipIdent(code, i);
476
+ return {
477
+ end: wend,
478
+ operandEnd: operandAfterWord(code.slice(i, wend)),
479
+ brace: 0
480
+ };
481
+ }
482
+ if (ch === "{") return {
483
+ end: i + 1,
484
+ operandEnd: false,
485
+ brace: 1
486
+ };
487
+ if (ch === "}") return {
488
+ end: i + 1,
489
+ operandEnd: true,
490
+ brace: -1
491
+ };
492
+ return {
493
+ end: i + 1,
494
+ operandEnd: false,
495
+ brace: 0
496
+ };
497
+ }
498
+ /**
499
+ * Scan one operand-context token — an opaque span, a regex-or-division,
500
+ * or an identifier run — returning where it ends and whether it leaves
501
+ * an operand behind. Comments are transparent (operand context passes
502
+ * through unchanged). Returns null for punctuation/whitespace the
503
+ * caller's own loop owns.
504
+ */
505
+ function scanOperand(code, i, operandEnd) {
506
+ const opaque = scanOpaqueEnd(code, i);
507
+ if (opaque.end > i + 1) return {
508
+ end: opaque.end,
509
+ operandEnd: operandEnd || opaque.isString
510
+ };
511
+ const c = code[i];
512
+ if (c === "/") return operandEnd ? {
513
+ end: i + 1,
514
+ operandEnd: false
515
+ } : {
516
+ end: scanRegex(code, i),
517
+ operandEnd: true
518
+ };
519
+ if (isIdentChar(c)) {
520
+ const wend = skipIdent(code, i);
521
+ return {
522
+ end: wend,
523
+ operandEnd: operandAfterWord(code.slice(i, wend))
524
+ };
525
+ }
526
+ return null;
527
+ }
528
+ /**
529
+ * End index of the `${` expression whose `{` is at `at`. Strings,
530
+ * comments, nested templates, and regex literals inside the expression
531
+ * are all skipped correctly: `` `${ x["`"] }` `` and `` `${ /"/ }` ``
532
+ * must not treat literal text as syntax.
533
+ */
534
+ function scanTemplateExpr(code, at) {
535
+ let depth = 1;
536
+ let i = at + 1;
537
+ let operandEnd = false;
538
+ while (i < code.length && depth > 0) {
539
+ const step = scanToken(code, i, operandEnd);
540
+ depth += step.brace;
541
+ operandEnd = step.operandEnd;
542
+ i = step.end;
543
+ }
544
+ return i;
545
+ }
546
+ /**
547
+ * Index of the `}` matching the `{` at `open`; -1 when unbalanced.
548
+ * Operand tracking decides whether a `/` divides or opens a regex —
549
+ * braces inside `/[{]/` never reach the depth counter.
550
+ */
551
+ function matchBrace(code, open) {
552
+ let depth = 0;
553
+ let i = open;
554
+ let operandEnd = false;
555
+ while (i < code.length) {
556
+ const step = scanToken(code, i, operandEnd);
557
+ depth += step.brace;
558
+ operandEnd = step.operandEnd;
559
+ i = step.end;
560
+ if (step.brace === -1 && depth === 0) return i - 1;
561
+ }
562
+ return -1;
563
+ }
564
+ /** `word` starting at `at` with an identifier boundary on the right. */
565
+ function keywordAt(code, at, word) {
566
+ return code.startsWith(word, at) && !isIdentChar(code[at + word.length]);
567
+ }
568
+ /** Skip whitespace and comments (including newlines). */
569
+ function skipTrivia(code, at) {
570
+ let i = at;
571
+ for (;;) {
572
+ const c = code[i];
573
+ if (isWsChar(c)) {
574
+ i++;
575
+ continue;
576
+ }
577
+ if (c === "/" && code[i + 1] === "/") {
578
+ i = lineCommentEnd(code, i);
579
+ continue;
580
+ }
581
+ if (c === "/" && code[i + 1] === "*") {
582
+ i = blockCommentEnd(code, i);
583
+ continue;
584
+ }
585
+ return i;
586
+ }
587
+ }
588
+ //#endregion
589
+ //#region src/engine.ts
590
+ /** Default template shown in the editor. */
591
+ const DEFAULT_CODE = [
592
+ "import { Project, Pipeline, FunctionStep, Entry } from \"@sverka/playground\";",
593
+ "",
594
+ "const proj = new Project(\"demo\");",
595
+ "const checks = new Pipeline(proj, \"checks\");",
596
+ "",
597
+ "new FunctionStep(checks, \"lint\", {",
598
+ " fn: () => [",
599
+ " { rule: \"no-unused-vars\", file: \"src/index.ts\", line: 5, severity: \"high\", message: \"Variable 'x' is declared but never used\" },",
600
+ " { rule: \"no-console\", file: \"src/utils.ts\", line: 12, severity: \"medium\", message: \"Unexpected console.log statement\" },",
601
+ " ],",
602
+ "});",
603
+ "",
604
+ "new FunctionStep(checks, \"typecheck\", {",
605
+ " fn: () => [",
606
+ " { rule: \"ts2322\", file: \"src/types.ts\", line: 8, severity: \"critical\", message: \"Type 'string' is not assignable to type 'number'\" },",
607
+ " ],",
608
+ "});",
609
+ "",
610
+ "new FunctionStep(checks, \"test\", {",
611
+ " fn: () => [",
612
+ " { rule: \"assertion-failed\", file: \"test/index.test.ts\", line: 23, severity: \"high\", message: \"Expected 5 but got 3\" },",
613
+ " { rule: \"assertion-failed\", file: \"test/index.test.ts\", line: 45, severity: \"low\", message: \"Expected 'hello' but got 'world'\" },",
614
+ " ],",
615
+ "});",
616
+ "",
617
+ "new Entry(checks, \"on-push\", { trigger: { kind: \"push\" }, roots: [\"test\"] });",
618
+ "",
619
+ "export default proj;",
620
+ ""
621
+ ].join("\n");
622
+ /** Escape HTML special characters to prevent XSS. */
623
+ function escapeHtml(text) {
624
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
625
+ }
626
+ /** +1/−1 for bracket chars, 0 otherwise. */
627
+ function bracketDelta(c) {
628
+ if (c === "{" || c === "(" || c === "[") return 1;
629
+ if (c === "}" || c === ")" || c === "]") return -1;
630
+ return 0;
631
+ }
632
+ /** Chars that end an import statement at depth ≤0: `;` (consumed) or a
633
+ * newline after the module-specifier string (left in place). */
634
+ function stmtTerminator(c, depth, specSeen) {
635
+ if (c === ";" && depth <= 0) return "past";
636
+ if (c === "\n" && depth <= 0 && specSeen) return "at";
637
+ return null;
638
+ }
639
+ /** End index of the statement starting at `at`: first top-level `;`, or
640
+ * the first newline after the module-specifier string (covers multiline
641
+ * `import {…}\n from "x"` and `import "x"` without a semicolon). */
642
+ function scanStatementEnd(code, at) {
643
+ let i = at;
644
+ let depth = 0;
645
+ let specSeen = false;
646
+ while (i < code.length) {
647
+ const opaque = scanOpaqueEnd(code, i);
648
+ if (opaque.end > i + 1) {
649
+ specSeen = specSeen || opaque.isString;
650
+ i = opaque.end;
651
+ continue;
652
+ }
653
+ const term = stmtTerminator(code[i], depth, specSeen);
654
+ if (term === "past") return i + 1;
655
+ if (term === "at") return i;
656
+ depth += bracketDelta(code[i]);
657
+ i++;
658
+ }
659
+ return i;
660
+ }
661
+ /** Same-line statement tail: horizontal whitespace then an optional `;`.
662
+ * Never eats the newline — the following line is the next statement. */
663
+ function scanStmtTail(code, at) {
664
+ let i = at;
665
+ while (i < code.length && (code[i] === " " || code[i] === " ")) i++;
666
+ if (code[i] === ";") i++;
667
+ return i;
668
+ }
669
+ /** End index of the `from "…"` clause starting after `from`, or `at` when
670
+ * the clause is malformed — let the evaluator complain. */
671
+ function scanFromClause(code, next) {
672
+ const specAt = skipTrivia(code, next + 4);
673
+ const q = code[specAt];
674
+ if (q === "\"" || q === "'") return scanStmtTail(code, scanString(code, specAt));
675
+ return next;
676
+ }
677
+ /** End index of `export * [as ns] [from "…"]` — the `*` is at `at`. */
678
+ function scanExportStar(code, at) {
679
+ let i = skipTrivia(code, at + 1);
680
+ if (keywordAt(code, i, "as")) {
681
+ i = skipTrivia(code, i + 2);
682
+ i = skipIdent(code, i);
683
+ }
684
+ const next = skipTrivia(code, i);
685
+ if (keywordAt(code, next, "from")) return scanFromClause(code, next);
686
+ return scanStmtTail(code, i);
687
+ }
688
+ /**
689
+ * End index of `export {…}` / `export * [as ns] [from "…"]` starting at
690
+ * the `{` or `*`. A bare list ends at its `;` or newline — unlike an
691
+ * import, a newline after the closing brace must end the statement or
692
+ * the next line would be eaten with it.
693
+ */
694
+ function scanExportListEnd(code, at) {
695
+ if (code[at] === "*") return scanExportStar(code, at);
696
+ const close = matchBrace(code, at);
697
+ const i = close < 0 ? code.length : close + 1;
698
+ const next = skipTrivia(code, i);
699
+ if (keywordAt(code, next, "from")) return scanFromClause(code, next);
700
+ return scanStmtTail(code, i);
701
+ }
702
+ function atStmtLevel(st) {
703
+ return st.stack.length === 0 || st.stack.at(-1) === "block";
704
+ }
705
+ /** Copy a string literal or comment verbatim into `out`. */
706
+ function copyOpaque(code, st, out) {
707
+ const opaque = scanOpaqueEnd(code, st.i);
708
+ if (opaque.end <= st.i + 1) return false;
709
+ out.push(code.slice(st.i, opaque.end));
710
+ if (opaque.isString) {
711
+ st.stmtStart = false;
712
+ st.operandEnd = true;
713
+ }
714
+ st.i = opaque.end;
715
+ return true;
716
+ }
717
+ /** Handle `{}`/`()`/`[]` — pushes onto the block/expr stack and sets the
718
+ * operand context: `}` closing an object literal ends a value, `}`
719
+ * closing a block ends a statement. */
720
+ function applyBracket(code, st, out) {
721
+ const c = code[st.i];
722
+ if (c === "{") {
723
+ st.stack.push(st.stmtStart ? "block" : "expr");
724
+ st.operandEnd = false;
725
+ } else if (c === "}") {
726
+ const popped = st.stack.pop();
727
+ st.stmtStart = popped === "block";
728
+ st.operandEnd = popped === "expr";
729
+ } else if (c === "(" || c === "[") {
730
+ st.stack.push("expr");
731
+ st.stmtStart = false;
732
+ st.operandEnd = false;
733
+ } else if (c === ")" || c === "]") {
734
+ st.stack.pop();
735
+ st.stmtStart = false;
736
+ st.operandEnd = true;
737
+ } else return false;
738
+ out.push(c ?? "");
739
+ st.i++;
740
+ return true;
741
+ }
742
+ /** Handle `\n`, `;`, and whitespace — statement boundaries at statement
743
+ * level only (a `;` inside `for (;;)` doesn't count). A newline never
744
+ * resets operand context: `foo\n/bar/` divides, per ASI rules. */
745
+ function applyTerminator(code, st, out) {
746
+ const c = code[st.i];
747
+ if (c !== ";" && c !== "\n" && !isWsChar(c)) return false;
748
+ if (c === ";") {
749
+ if (atStmtLevel(st)) st.stmtStart = true;
750
+ st.operandEnd = false;
751
+ } else if (c === "\n" && atStmtLevel(st)) st.stmtStart = true;
752
+ out.push(c ?? "");
753
+ st.i++;
754
+ return true;
755
+ }
756
+ /** Handle `/` — a regex literal when an operand is expected, a division
757
+ * operator when one just ended. Comments never reach here (copyOpaque
758
+ * already consumed them). */
759
+ function applySlash(code, st, out) {
760
+ if (code[st.i] !== "/" || commentAt(code, st.i)) return false;
761
+ if (st.operandEnd) {
762
+ out.push("/");
763
+ st.i++;
764
+ st.operandEnd = false;
765
+ } else {
766
+ const end = scanRegex(code, st.i);
767
+ out.push(code.slice(st.i, end));
768
+ st.i = end;
769
+ st.operandEnd = true;
770
+ }
771
+ st.stmtStart = false;
772
+ return true;
773
+ }
774
+ /** Handle an `import` keyword at statement start. Dynamic `import (…)`
775
+ * and `import.meta` pass through verbatim — whitespace and comments may
776
+ * sit between the keyword and the paren. */
777
+ function tryImport(code, st, out) {
778
+ if (!st.stmtStart || !keywordAt(code, st.i, "import")) return false;
779
+ const nc = code[skipTrivia(code, st.i + 6)];
780
+ if (nc === "(" || nc === ".") {
781
+ out.push("import");
782
+ st.i += 6;
783
+ st.stmtStart = false;
784
+ st.operandEnd = true;
785
+ return true;
786
+ }
787
+ st.i = scanStatementEnd(code, st.i + 6);
788
+ st.stmtStart = false;
789
+ st.operandEnd = false;
790
+ return true;
791
+ }
792
+ /** Handle `export default` (→ `return`), `export {…}`/`export *` (removed)
793
+ * and `export <decl>` (keyword dropped — the eval context is a function
794
+ * body, where `export const` would be a syntax error). */
795
+ function tryExport(code, st, out) {
796
+ if (!st.stmtStart || !keywordAt(code, st.i, "export")) return false;
797
+ const j = skipTrivia(code, st.i + 6);
798
+ if (!st.exportDefaultDone && code.startsWith("default", j) && !isIdentChar(code[j + 7])) {
799
+ out.push("return ");
800
+ st.exportDefaultDone = true;
801
+ st.i = skipTrivia(code, j + 7);
802
+ st.stmtStart = false;
803
+ st.operandEnd = false;
804
+ return true;
805
+ }
806
+ if (code[j] === "{" || code[j] === "*") {
807
+ st.i = scanExportListEnd(code, j);
808
+ st.stmtStart = false;
809
+ st.operandEnd = false;
810
+ return true;
811
+ }
812
+ st.i = j;
813
+ st.stmtStart = false;
814
+ st.operandEnd = false;
815
+ return true;
816
+ }
817
+ /** Emit an identifier run — keyword table decides whether `/` after it
818
+ * divides (`foo /x/`) or opens a regex (`return /x/`). */
819
+ function tryWord(code, st, out) {
820
+ if (!isIdentChar(code[st.i])) return false;
821
+ const wend = skipIdent(code, st.i);
822
+ out.push(code.slice(st.i, wend));
823
+ st.operandEnd = operandAfterWord(code.slice(st.i, wend));
824
+ st.stmtStart = false;
825
+ st.i = wend;
826
+ return true;
827
+ }
828
+ /** Char handlers tried in order — first true wins. Any char left over is
829
+ * copied verbatim as a non-operand punctuation token. */
830
+ const HANDLERS = [
831
+ copyOpaque,
832
+ applyBracket,
833
+ applyTerminator,
834
+ applySlash,
835
+ tryImport,
836
+ tryExport,
837
+ tryWord
838
+ ];
839
+ function advanceScan(code, st, out) {
840
+ for (const handle of HANDLERS) if (handle(code, st, out)) return;
841
+ st.stmtStart = false;
842
+ st.operandEnd = false;
843
+ out.push(code[st.i] ?? "");
844
+ st.i++;
845
+ }
846
+ /**
847
+ * Strip import/export statements from user code for eval — a linear
848
+ * single-pass scan, so multiline imports and comments/strings are handled
849
+ * and pathological spacing cannot cause quadratic backtracking.
850
+ *
851
+ * `import …` statements are removed whole. `export default` becomes
852
+ * `return`, `export {…}`/`export *` are removed, and `export` before a
853
+ * declaration is dropped (the eval context is a function body, where
854
+ * `export const` would be a syntax error).
855
+ *
856
+ * Note: TypeScript-specific syntax (type annotations, interfaces, enums)
857
+ * is not stripped — the playground uses Monaco's TypeScript language mode
858
+ * for editing, but evaluation is plain JavaScript. Users should write
859
+ * JS-compatible code or use the `as any` escape hatch sparingly.
860
+ */
861
+ function preprocessCode(code) {
862
+ const out = [];
863
+ const st = {
864
+ i: 0,
865
+ stmtStart: true,
866
+ exportDefaultDone: false,
867
+ stack: [],
868
+ operandEnd: false
869
+ };
870
+ while (st.i < code.length) {
871
+ if (code[st.i] === void 0) break;
872
+ advanceScan(code, st, out);
873
+ }
874
+ return out.join("");
875
+ }
876
+ /**
877
+ * Evaluate user code and return the Project.
878
+ *
879
+ * SECURITY: This uses `new Function()` to execute user-provided code.
880
+ * This is intentional — the playground is a local development tool where
881
+ * the user writes and runs their own pipeline code. The code runs in the
882
+ * browser's main page context (not a sandboxed iframe or worker) because
883
+ * the playground needs to support synchronous FunctionStep execution and
884
+ * direct access to the pipeline constructs. This is the same trust model
885
+ * as a local REPL or `node -e`.
886
+ */
887
+ function evaluateUserCode(code) {
888
+ const params = [
889
+ "Project",
890
+ "Pipeline",
891
+ "FunctionStep",
892
+ "Entry",
893
+ preprocessCode(code)
894
+ ];
895
+ const result = new Function(...params)(Project, Pipeline, FunctionStep, Entry);
896
+ if (!(result instanceof Project)) throw new TypeError("Code must export a Project instance");
897
+ return result;
898
+ }
899
+ /**
900
+ * Run a pipeline with a timeout. If a step returns a never-resolving
901
+ * promise, the timeout ensures the UI recovers instead of staying
902
+ * stuck in "Running" state forever.
903
+ */
904
+ async function runPipelineWithTimeout(project, timeoutMs) {
905
+ let timer;
906
+ try {
907
+ return await Promise.race([runPipeline(project), new Promise((_, reject) => {
908
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Pipeline timed out after ${timeoutMs}ms`)), timeoutMs);
909
+ })]);
910
+ } finally {
911
+ if (timer !== void 0) clearTimeout(timer);
912
+ }
913
+ }
914
+ //#endregion
915
+ //#region src/embed.ts
916
+ const STYLE = `
917
+ font-family: monospace; font-size: 12px;
918
+ background: #0d1117; color: #c9d1d9;
919
+ border: 1px solid #30363d; border-radius: 6px;
920
+ display: flex; flex-direction: column; overflow: hidden;
921
+ `;
922
+ function createHeader(doc) {
923
+ const header = doc.createElement("div");
924
+ header.setAttribute("style", "display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.6rem;background:#161b22;border-bottom:1px solid #30363d;");
925
+ const runBtn = doc.createElement("button");
926
+ runBtn.textContent = "Run";
927
+ 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;");
928
+ const status = doc.createElement("span");
929
+ status.setAttribute("style", "font-size:0.75rem;color:#8b949e;");
930
+ status.textContent = "Ready";
931
+ header.append(runBtn, status);
932
+ return {
933
+ header,
934
+ runBtn,
935
+ status
936
+ };
937
+ }
938
+ function createCodeEl(doc, code, readonly) {
939
+ if (readonly) {
940
+ const pre = doc.createElement("pre");
941
+ pre.setAttribute("style", "margin:0;padding:0.75rem;overflow:auto;max-height:16rem;white-space:pre;");
942
+ pre.textContent = code;
943
+ return pre;
944
+ }
945
+ const textarea = doc.createElement("textarea");
946
+ textarea.value = code;
947
+ 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;");
948
+ return textarea;
949
+ }
950
+ function createFrame(doc) {
951
+ const frame = doc.createElement("iframe");
952
+ frame.setAttribute("sandbox", "allow-scripts");
953
+ frame.setAttribute("style", "flex:1;min-height:10rem;width:100%;border:none;background:#0d1117;");
954
+ return frame;
955
+ }
956
+ const BODY_DARK = "<body style=\"background:#0d1117;font-family:monospace;padding:1rem;";
957
+ /** Failure banner HTML — string concat keeps user data flowing through
958
+ * escapeHtml explicitly, never through template interpolation. */
959
+ function failureBanner(result) {
960
+ return [
961
+ "<div style=\"background:#3d1d1f;color:#f85149;padding:0.6rem 1rem;font-family:monospace;border-bottom:1px solid #30363d;\">",
962
+ "<strong>Run failed</strong>",
963
+ "<pre style=\"margin:0.4rem 0 0;white-space:pre-wrap;\">",
964
+ escapeHtml(result.steps.filter((s) => s.status === "failure").map((s) => `${s.stepId}: ${s.error ?? "failed"}`).join("\n") || `${result.steps.length} steps failed`),
965
+ "</pre></div>"
966
+ ].join("");
967
+ }
968
+ /** Document shown when the run failed — partial findings still render
969
+ * underneath the banner so finished steps aren't hidden by the error. */
970
+ function failureDoc(result, banner) {
971
+ if (result.findings.length === 0) return "<!DOCTYPE html><html>" + BODY_DARK + "color:#f85149;\">" + banner + "</body></html>";
972
+ const sarif = generateSarifHtml(result.findings);
973
+ return sarif.includes("<body>") ? sarif.replace("<body>", "<body>" + banner) : banner + sarif;
974
+ }
975
+ /** Document shown for a clean run with zero findings. */
976
+ function successDoc(result) {
977
+ return [
978
+ "<!DOCTYPE html><html>",
979
+ BODY_DARK,
980
+ "color:#3fb950;\">",
981
+ "<h3>No findings — all checks passed</h3><p>",
982
+ escapeHtml(String(result.steps.length)),
983
+ " steps in ",
984
+ escapeHtml(String(result.totalDurationMs)),
985
+ "ms</p></body></html>"
986
+ ].join("");
987
+ }
988
+ /** Document shown when the runner itself threw (eval or pipeline error). */
989
+ function errorDoc(e) {
990
+ const msg = e instanceof Error ? e.message : String(e);
991
+ return [
992
+ "<!DOCTYPE html><html>",
993
+ BODY_DARK,
994
+ "color:#f85149;\">",
995
+ "<h3>Error</h3><pre>",
996
+ escapeHtml(msg),
997
+ "</pre></body></html>"
998
+ ].join("");
999
+ }
1000
+ function statusLine(result) {
1001
+ if (result.success) return `${result.findings.length} findings · ${result.totalDurationMs}ms`;
1002
+ return `${result.steps.filter((s) => s.status === "failure").length} step(s) failed · ${result.findings.length} findings`;
1003
+ }
1004
+ /** Render a completed run into the findings frame. */
1005
+ function renderResult(result, showFindings) {
1006
+ if (!result.success) {
1007
+ showFindings(failureDoc(result, failureBanner(result)));
1008
+ return;
1009
+ }
1010
+ if (result.findings.length === 0) {
1011
+ showFindings(successDoc(result));
1012
+ return;
1013
+ }
1014
+ showFindings(generateSarifHtml(result.findings));
1015
+ }
1016
+ /**
1017
+ * Mount a self-contained runner inside `el`. `dispose()` tears the mount
1018
+ * down. With `autoRun: true` the runner executes `code` once on mount;
1019
+ * otherwise it waits for the Run button — embeds never auto-execute code
1020
+ * the embedder didn't explicitly allow.
1021
+ */
1022
+ function mountRunner(el, opts = {}) {
1023
+ const doc = el.ownerDocument;
1024
+ const code = opts.code ?? DEFAULT_CODE;
1025
+ const root = doc.createElement("div");
1026
+ root.setAttribute("style", STYLE);
1027
+ const { header, runBtn, status } = createHeader(doc);
1028
+ const codeEl = createCodeEl(doc, code, opts.readonly === true);
1029
+ const frame = createFrame(doc);
1030
+ root.append(header, codeEl, frame);
1031
+ el.appendChild(root);
1032
+ const getCode = () => opts.readonly === true ? code : codeEl.value;
1033
+ const run = async () => {
1034
+ runBtn.disabled = true;
1035
+ status.textContent = "Running...";
1036
+ let result;
1037
+ try {
1038
+ result = await runPipelineWithTimeout(evaluateUserCode(getCode()), 3e4);
1039
+ renderResult(result, (html) => {
1040
+ frame.srcdoc = html;
1041
+ });
1042
+ status.textContent = statusLine(result);
1043
+ } catch (e) {
1044
+ frame.srcdoc = errorDoc(e);
1045
+ status.textContent = "Error";
1046
+ runBtn.disabled = false;
1047
+ return;
1048
+ }
1049
+ runBtn.disabled = false;
1050
+ opts.onRun?.(result);
1051
+ };
1052
+ const onClick = () => {
1053
+ run();
1054
+ };
1055
+ runBtn.addEventListener("click", onClick);
1056
+ if (opts.autoRun === true) run();
1057
+ return { dispose() {
1058
+ runBtn.removeEventListener("click", onClick);
1059
+ root.remove();
1060
+ } };
1061
+ }
1062
+ //#endregion
1063
+ //#region src/transpile.ts
1064
+ function isOpenBracket(c) {
1065
+ return c === "{" || c === "[" || c === "(";
1066
+ }
1067
+ function isCloseBracket(c) {
1068
+ return c === "}" || c === "]" || c === ")";
1069
+ }
1070
+ function skipWs(code, i) {
1071
+ while (i < code.length && isWsChar(code[i])) i++;
1072
+ return i;
1073
+ }
1074
+ /** One token of an operand-tracking scan — identifiers (with `word`),
1075
+ * opaque spans, and regex-vs-division resolve through the shared
1076
+ * scanner; whitespace passes through. Null when `i` sits on
1077
+ * punctuation the caller's own depth/branch logic owns. */
1078
+ function propToken(s, i, operandEnd) {
1079
+ if (isWsChar(s[i])) return {
1080
+ end: i + 1,
1081
+ operandEnd
1082
+ };
1083
+ const op = scanOperand(s, i, operandEnd);
1084
+ if (op === null) return null;
1085
+ if (isIdentChar(s[i])) return {
1086
+ ...op,
1087
+ word: s.slice(i, op.end)
1088
+ };
1089
+ return op;
1090
+ }
1091
+ /** Apply a bracket/other punctuation char to depth + operand state. */
1092
+ function applyPunctuation(c, st) {
1093
+ if (isOpenBracket(c)) {
1094
+ st.depth++;
1095
+ st.operandEnd = false;
1096
+ } else if (isCloseBracket(c)) {
1097
+ st.depth--;
1098
+ st.operandEnd = true;
1099
+ } else st.operandEnd = false;
1100
+ }
1101
+ /**
1102
+ * Index of the `:` following a depth-0 `key` in a props object body, or -1.
1103
+ * A `dependencies` declared inside `fn` never counts — only top-level keys.
1104
+ */
1105
+ function findPropKey(props, key) {
1106
+ const st = {
1107
+ depth: 0,
1108
+ operandEnd: false
1109
+ };
1110
+ let i = 0;
1111
+ while (i < props.length) {
1112
+ const tok = propToken(props, i, st.operandEnd);
1113
+ if (tok === null) {
1114
+ applyPunctuation(props[i], st);
1115
+ i++;
1116
+ continue;
1117
+ }
1118
+ if (st.depth === 0 && tok.word === key) {
1119
+ const hit = skipWs(props, tok.end);
1120
+ if (props[hit] === ":") return hit;
1121
+ }
1122
+ st.operandEnd = tok.operandEnd;
1123
+ i = tok.end;
1124
+ }
1125
+ return -1;
1126
+ }
1127
+ /** True when the comment at `k` is a top-level tail comment — `deps: [] //
1128
+ * note` — and therefore not part of the value. Emitting it would comment
1129
+ * out the generated `}` after `dependsOn: <value>`. */
1130
+ function isTailComment(props, k, after) {
1131
+ const ahead = skipWs(props, after);
1132
+ return ahead >= props.length || props[ahead] === ",";
1133
+ }
1134
+ /** End index of a prop value starting at `j` — the next top-level comma
1135
+ * or the end of the props body, with strings/comments/brackets balanced. */
1136
+ function propValueEnd(props, j) {
1137
+ const st = {
1138
+ depth: 0,
1139
+ operandEnd: false
1140
+ };
1141
+ let k = j;
1142
+ while (k < props.length) {
1143
+ if (isOpaqueStart(props, k)) {
1144
+ const after = scanOpaqueEnd(props, k).end;
1145
+ if (st.depth === 0 && isTailComment(props, k, after)) return k;
1146
+ k = after;
1147
+ continue;
1148
+ }
1149
+ const tok = propToken(props, k, st.operandEnd);
1150
+ if (tok !== null) {
1151
+ st.operandEnd = tok.operandEnd;
1152
+ k = tok.end;
1153
+ continue;
1154
+ }
1155
+ if (props[k] === "," && st.depth === 0) return k;
1156
+ applyPunctuation(props[k], st);
1157
+ k++;
1158
+ }
1159
+ return k;
1160
+ }
1161
+ /**
1162
+ * Extract a top-level `key: <value>` span from a props object body.
1163
+ * Only depth-0 keys match — a `dependencies` declared inside `fn` never
1164
+ * counts. The value runs to the next top-level `,`, with strings,
1165
+ * comments, and nested brackets all balanced.
1166
+ */
1167
+ function extractProp(props, key) {
1168
+ const colon = findPropKey(props, key);
1169
+ if (colon < 0) return void 0;
1170
+ const j = skipWs(props, colon + 1);
1171
+ return props.slice(j, propValueEnd(props, j)).trim();
1172
+ }
1173
+ /** Consume trailing spaces/tabs plus one optional `;` — never the newline.
1174
+ * Eating it would fuse the emitted import with the next line
1175
+ * (`from "@sverka/workflow"const x = …`). */
1176
+ function stmtEndPos(source, i) {
1177
+ let end = i;
1178
+ while (source[end] === " " || source[end] === " ") end++;
1179
+ if (source[end] === ";") end++;
1180
+ return end;
1181
+ }
1182
+ /** Parse one import specifier: `Name` or `Name as alias`. */
1183
+ function parseImportSpec(spec) {
1184
+ const a = skipWs(spec, 0);
1185
+ const wend = skipIdent(spec, a);
1186
+ const orig = spec.slice(a, wend);
1187
+ const asPos = skipWs(spec, wend);
1188
+ if (!spec.startsWith("as", asPos) || isIdentChar(spec[asPos + 2])) return {
1189
+ orig,
1190
+ local: orig
1191
+ };
1192
+ const b = skipWs(spec, asPos + 2);
1193
+ const local = spec.slice(b, skipIdent(spec, b));
1194
+ return local === "" ? {
1195
+ orig,
1196
+ local: orig
1197
+ } : {
1198
+ orig,
1199
+ local
1200
+ };
1201
+ }
1202
+ /** Map `{…}` import specifiers; FunctionStep becomes ShellStep (aliases
1203
+ * survive: `FunctionStep as Fn` → `ShellStep as Fn`). */
1204
+ function mapImportSpecifiers(inner) {
1205
+ const mapped = [];
1206
+ const fnStepLocals = [];
1207
+ for (const spec of inner.split(",")) {
1208
+ const trimmed = spec.trim();
1209
+ if (trimmed === "") continue;
1210
+ const { orig, local } = parseImportSpec(trimmed);
1211
+ if (orig === "FunctionStep") {
1212
+ fnStepLocals.push(local);
1213
+ mapped.push(orig === local ? "ShellStep" : `ShellStep as ${local}`);
1214
+ } else mapped.push(trimmed);
1215
+ }
1216
+ return {
1217
+ mapped: mapped.join(", "),
1218
+ fnStepLocals
1219
+ };
1220
+ }
1221
+ /** Parse `import {…} from "@sverka/playground"` at `i`, or null when the
1222
+ * statement has another shape/module. Caller guarantees "import" at `i`. */
1223
+ function parsePlaygroundImport(source, i) {
1224
+ const j = skipWs(source, i + 6);
1225
+ if (source[j] !== "{") return null;
1226
+ const namesEnd = matchBrace(source, j);
1227
+ if (namesEnd < 0) return null;
1228
+ const fromKw = skipWs(source, namesEnd + 1);
1229
+ if (!keywordAt(source, fromKw, "from")) return null;
1230
+ const qPos = skipWs(source, fromKw + 4);
1231
+ const q = source[qPos];
1232
+ if (q !== "\"" && q !== "'") return null;
1233
+ const specEnd = scanString(source, qPos);
1234
+ if (source.slice(qPos + 1, specEnd - 1) !== "@sverka/playground") return null;
1235
+ const { mapped, fnStepLocals } = mapImportSpecifiers(source.slice(j + 1, namesEnd));
1236
+ return {
1237
+ stmtEnd: stmtEndPos(source, specEnd),
1238
+ mapped,
1239
+ fnStepLocals
1240
+ };
1241
+ }
1242
+ /**
1243
+ * Rewrite `import {…} from "@sverka/playground"` statements linearly.
1244
+ *
1245
+ * Returns the rewritten source plus every local name bound to
1246
+ * FunctionStep, so constructor calls under an alias are transpiled too.
1247
+ */
1248
+ function rewritePlaygroundImports(source) {
1249
+ const fnStepNames = [];
1250
+ let out = "";
1251
+ let cursor = 0;
1252
+ let i = 0;
1253
+ while (i < source.length) {
1254
+ const end = scanOpaqueEnd(source, i).end;
1255
+ if (end > i + 1) {
1256
+ i = end;
1257
+ continue;
1258
+ }
1259
+ if (source.startsWith("import", i) && !isIdentChar(source[i + 6])) {
1260
+ const stmt = parsePlaygroundImport(source, i);
1261
+ if (stmt !== null) {
1262
+ out += source.slice(cursor, i);
1263
+ out += `import { ${stmt.mapped} } from "@sverka/workflow"`;
1264
+ if (source[stmt.stmtEnd - 1] === ";") out += ";";
1265
+ fnStepNames.push(...stmt.fnStepLocals);
1266
+ cursor = stmt.stmtEnd;
1267
+ i = stmt.stmtEnd;
1268
+ continue;
1269
+ }
1270
+ }
1271
+ i++;
1272
+ }
1273
+ return {
1274
+ code: out + source.slice(cursor),
1275
+ fnStepNames
1276
+ };
1277
+ }
1278
+ /** Index of `(` after `new <name>` where <name> ∈ names, or null. */
1279
+ function stepCallParen(code, afterNew, names) {
1280
+ const j = skipWs(code, afterNew);
1281
+ for (const name of names) {
1282
+ const next = code[j + name.length];
1283
+ if (code.startsWith(name, j) && !isIdentChar(next)) {
1284
+ const paren = skipWs(code, j + name.length);
1285
+ if (code[paren] === "(") return paren;
1286
+ }
1287
+ }
1288
+ return null;
1289
+ }
1290
+ /** Operand context after punctuation — closers end a value, everything
1291
+ * else (openers, operators, separators) expects one. */
1292
+ function punctuationOperandEnd(c) {
1293
+ return c === ")" || c === "]" || c === "}";
1294
+ }
1295
+ /**
1296
+ * Locate the next `new <name>(` call where <name> ∈ names. Whole-word
1297
+ * scanning makes boundary checks free — `myNew` or `newer` never match.
1298
+ * Opaque regions and regex literals are skipped inline, so a
1299
+ * `new FunctionStep(` inside a comment, string, or `/new Fn(/` pattern
1300
+ * is documentation, not a call.
1301
+ */
1302
+ function findStepCall(code, names, from) {
1303
+ let operandEnd = false;
1304
+ let i = from;
1305
+ while (i < code.length) {
1306
+ const tok = propToken(code, i, operandEnd);
1307
+ if (tok !== null) {
1308
+ if (tok.word === "new") {
1309
+ const paren = stepCallParen(code, tok.end, names);
1310
+ if (paren !== null) return {
1311
+ start: i,
1312
+ paren
1313
+ };
1314
+ }
1315
+ operandEnd = tok.operandEnd;
1316
+ i = tok.end;
1317
+ continue;
1318
+ }
1319
+ operandEnd = punctuationOperandEnd(code[i]);
1320
+ i++;
1321
+ }
1322
+ return null;
1323
+ }
1324
+ /** End of the scope expression — the first top-level comma after `from`,
1325
+ * or -1 when the argument list closes/ends first. */
1326
+ function scanScopeEnd(code, from) {
1327
+ const st = {
1328
+ depth: 0,
1329
+ operandEnd: false
1330
+ };
1331
+ let k = from;
1332
+ while (k < code.length) {
1333
+ const tok = propToken(code, k, st.operandEnd);
1334
+ if (tok !== null) {
1335
+ st.operandEnd = tok.operandEnd;
1336
+ k = tok.end;
1337
+ continue;
1338
+ }
1339
+ const c = code[k];
1340
+ if (c === "," && st.depth === 0) return k;
1341
+ if (isCloseBracket(c) && st.depth === 0) return -1;
1342
+ applyPunctuation(c, st);
1343
+ k++;
1344
+ }
1345
+ return -1;
1346
+ }
1347
+ /** Parse `(scope, "id", {` after the `(` at `paren`; null when the call
1348
+ * shape differs. A template id throws immediately — never half-converts. */
1349
+ function parseStepArgs(code, paren) {
1350
+ const scopeStart = skipWs(code, paren + 1);
1351
+ const comma = scanScopeEnd(code, scopeStart);
1352
+ if (comma < 0) return null;
1353
+ const qPos = skipWs(code, comma + 1);
1354
+ const q = code[qPos];
1355
+ if (q === "`") throw new PlaygroundError("TRANSPILE_FAILED", "cannot parse FunctionStep — template-literal step id");
1356
+ if (q !== "\"" && q !== "'") return null;
1357
+ const idEnd = scanString(code, qPos);
1358
+ const afterId = skipWs(code, idEnd);
1359
+ if (code[afterId] !== ",") return null;
1360
+ const propsOpen = skipWs(code, afterId + 1);
1361
+ if (code[propsOpen] !== "{") return null;
1362
+ return {
1363
+ scope: code.slice(scopeStart, comma).trim(),
1364
+ id: code.slice(qPos + 1, idEnd - 1),
1365
+ propsOpen
1366
+ };
1367
+ }
1368
+ /** Emit the ShellStep placeholder replacing one FunctionStep call. */
1369
+ function shellStepCall(args, dependencies) {
1370
+ return `new ShellStep(${args.scope}, "${args.id}", { command: "echo 'TODO: port '${args.id}' — replace with the real shell command'"` + (dependencies !== void 0 ? ", dependsOn: " + dependencies : "") + " }";
1371
+ }
1372
+ /** Rewrite every `new Fn(scope, "id", {…})` call under the given local
1373
+ * names into ShellStep placeholders; `dependencies` → `dependsOn`. */
1374
+ function rewriteStepCalls(code, names) {
1375
+ let result = "";
1376
+ let cursor = 0;
1377
+ for (;;) {
1378
+ const call = findStepCall(code, names, cursor);
1379
+ if (call === null) break;
1380
+ const args = parseStepArgs(code, call.paren);
1381
+ if (args === null) {
1382
+ result += code.slice(cursor, call.start + 3);
1383
+ cursor = call.start + 3;
1384
+ continue;
1385
+ }
1386
+ const propsClose = matchBrace(code, args.propsOpen);
1387
+ if (propsClose < 0) throw new PlaygroundError("TRANSPILE_FAILED", `cannot parse FunctionStep "${args.id}" — unbalanced props object`);
1388
+ const dependencies = extractProp(code.slice(args.propsOpen + 1, propsClose), "dependencies");
1389
+ result += code.slice(cursor, call.start) + shellStepCall(args, dependencies);
1390
+ cursor = propsClose + 1;
1391
+ }
1392
+ return result + code.slice(cursor);
1393
+ }
1394
+ /** A surviving step call means a shape we could not convert — computed
1395
+ * scope/id or a malformed alias call. Fail loud rather than emit a
1396
+ * half-converted file. */
1397
+ function assertNoLeftoverSteps(code, names) {
1398
+ const call = findStepCall(code, names, 0);
1399
+ if (call === null) return;
1400
+ const scopeStart = skipWs(code, call.paren + 1);
1401
+ const scopeEnd = scanScopeEnd(code, scopeStart);
1402
+ throw new PlaygroundError("TRANSPILE_FAILED", `cannot parse FunctionStep with scope '${scopeEnd < 0 ? "?" : code.slice(scopeStart, scopeEnd).trim() || "?"}' — unsupported expression`);
1403
+ }
1404
+ /**
1405
+ * Transpile playground source into `sverka.config.ts`.
1406
+ *
1407
+ * Rewrites the `@sverka/playground` import to `@sverka/workflow` and each
1408
+ * `new FunctionStep(scope, "id", { fn, dependencies })` into a ShellStep
1409
+ * whose command echoes a placeholder — the function body has no shell
1410
+ * equivalent.
1411
+ * `dependencies` maps onto `dependsOn`. Aliased FunctionStep imports
1412
+ * (`FunctionStep as Fn`) are followed too.
1413
+ *
1414
+ * Throws `PlaygroundError(TRANSPILE_FAILED)` naming the offending step when
1415
+ * a FunctionStep cannot be parsed — a half-converted file is never emitted.
1416
+ */
1417
+ function toSverkaConfig(source) {
1418
+ const { code, fnStepNames } = rewritePlaygroundImports(source);
1419
+ if (fnStepNames.length === 0) fnStepNames.push("FunctionStep");
1420
+ const out = rewriteStepCalls(code, fnStepNames);
1421
+ assertNoLeftoverSteps(out, fnStepNames);
1422
+ return out;
1423
+ }
1424
+ //#endregion
1425
+ export { DEFAULT_CODE, Entry, FunctionStep, Pipeline, PlaygroundError, Project, SHARE_PAYLOAD_WARN_BYTES, decodeShareLink, encodeShareLink, evaluateUserCode, mountRunner, runPipeline, runPipelineWithTimeout, toSverkaConfig };
273
1426
 
274
1427
  //# sourceMappingURL=index.mjs.map