@epikodelabs/testify 1.0.16

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/bin/jasmine ADDED
@@ -0,0 +1,1053 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as ___createRequire } from "module";
3
+ const require2 = ___createRequire(import.meta.url);
4
+ const ___fileURLToPath = require2("url").fileURLToPath;
5
+ const ___path = require2("path");
6
+ const __filename = ___fileURLToPath(import.meta.url);
7
+ const __dirname = ___path.dirname(__filename);
8
+ import * as fs from "fs";
9
+ import fs__default from "fs";
10
+ import path from "path";
11
+ import { createRequire } from "module";
12
+ import { pathToFileURL } from "url";
13
+ import "util";
14
+ const scriptRel = "modulepreload";
15
+ const assetsURL = function(dep) {
16
+ return "/" + dep;
17
+ };
18
+ const seen = {};
19
+ const __vitePreload = function preload(baseModule, deps, importerUrl) {
20
+ let promise = Promise.resolve();
21
+ if (deps && deps.length > 0) {
22
+ let allSettled = function(promises$2) {
23
+ return Promise.all(promises$2.map((p) => Promise.resolve(p).then((value$1) => ({
24
+ status: "fulfilled",
25
+ value: value$1
26
+ }), (reason) => ({
27
+ status: "rejected",
28
+ reason
29
+ }))));
30
+ };
31
+ document.getElementsByTagName("link");
32
+ const cspNonceMeta = document.querySelector("meta[property=csp-nonce]");
33
+ const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce");
34
+ promise = allSettled(deps.map((dep) => {
35
+ dep = assetsURL(dep);
36
+ if (dep in seen) return;
37
+ seen[dep] = true;
38
+ const isCss = dep.endsWith(".css");
39
+ const cssSelector = isCss ? '[rel="stylesheet"]' : "";
40
+ if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) return;
41
+ const link = document.createElement("link");
42
+ link.rel = isCss ? "stylesheet" : scriptRel;
43
+ if (!isCss) link.as = "script";
44
+ link.crossOrigin = "";
45
+ link.href = dep;
46
+ if (cspNonce) link.setAttribute("nonce", cspNonce);
47
+ document.head.appendChild(link);
48
+ if (isCss) return new Promise((res, rej) => {
49
+ link.addEventListener("load", res);
50
+ link.addEventListener("error", () => rej(/* @__PURE__ */ new Error(`Unable to preload CSS for ${dep}`)));
51
+ });
52
+ }));
53
+ }
54
+ function handlePreloadError(err$2) {
55
+ const e$1 = new Event("vite:preloadError", { cancelable: true });
56
+ e$1.payload = err$2;
57
+ window.dispatchEvent(e$1);
58
+ if (!e$1.defaultPrevented) throw err$2;
59
+ }
60
+ return promise.then((res) => {
61
+ for (const item of res || []) {
62
+ if (item.status !== "rejected") continue;
63
+ handlePreloadError(item.reason);
64
+ }
65
+ return baseModule().catch(handlePreloadError);
66
+ });
67
+ };
68
+ const MAX_WIDTH = 63;
69
+ const ANSI_FULL_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
70
+ function wrapLine(text, width, indentation = 0, mode = "char") {
71
+ text = text.replace(/\r?\n/g, " ").replace(/[\uFEFF\xA0\t]/g, " ").replace(/\s{2,}/g, " ").trim();
72
+ const indent = " ".repeat(indentation);
73
+ const indentWidth = indent.length;
74
+ if (width <= indentWidth) width = indentWidth + 1;
75
+ const availableWidth = width - indentWidth;
76
+ return mode === "char" ? wrapByChar(text, availableWidth, indent) : wrapByWord(text, availableWidth, indent);
77
+ }
78
+ function wrapByChar(text, available, indent) {
79
+ const lines = [];
80
+ let buf = "";
81
+ let vis = 0;
82
+ const tokens = text.split(ANSI_FULL_REGEX);
83
+ for (const token of tokens) {
84
+ if (ANSI_FULL_REGEX.test(token)) {
85
+ buf += token;
86
+ continue;
87
+ }
88
+ for (const ch of [...token]) {
89
+ if (vis >= available) {
90
+ lines.push(indent + buf);
91
+ buf = "";
92
+ vis = 0;
93
+ }
94
+ buf += ch;
95
+ vis++;
96
+ }
97
+ }
98
+ if (buf) lines.push(indent + buf);
99
+ return lines;
100
+ }
101
+ function wrapByWord(text, available, indent) {
102
+ const lines = [];
103
+ let buf = "";
104
+ let vis = 0;
105
+ let word = "";
106
+ let wordVis = 0;
107
+ const flushWord = () => {
108
+ if (!word) return;
109
+ if (wordVis > available) {
110
+ for (const ch of [...word]) {
111
+ if (vis >= available) {
112
+ lines.push(indent + buf.trimEnd());
113
+ buf = "";
114
+ vis = 0;
115
+ }
116
+ buf += ch;
117
+ vis++;
118
+ }
119
+ } else {
120
+ if (vis + wordVis > available && vis > 0) {
121
+ lines.push(indent + buf.trimEnd());
122
+ buf = "";
123
+ vis = 0;
124
+ }
125
+ buf += word;
126
+ vis += wordVis;
127
+ }
128
+ word = "";
129
+ wordVis = 0;
130
+ };
131
+ for (const ch of [...text]) {
132
+ if (/\s/.test(ch)) {
133
+ flushWord();
134
+ if (vis < available && vis > 0) {
135
+ buf += " ";
136
+ vis++;
137
+ }
138
+ } else {
139
+ word += ch;
140
+ wordVis++;
141
+ }
142
+ }
143
+ flushWord();
144
+ if (buf) lines.push(indent + buf.trimEnd());
145
+ return lines;
146
+ }
147
+ const colors = {
148
+ reset: "\x1B[0m",
149
+ bold: "\x1B[1m",
150
+ brightRed: "\x1B[91m",
151
+ brightGreen: "\x1B[92m"
152
+ };
153
+ class Logger {
154
+ previousLines = [];
155
+ showPrompt = true;
156
+ prompt;
157
+ errorPrompt;
158
+ constructor(options = {}) {
159
+ this.prompt = `${colors.bold}${options.promptColor ?? colors.brightGreen}> ${colors.reset}`;
160
+ this.errorPrompt = `${options.errorPromptColor ?? colors.brightRed}> ${colors.reset}`;
161
+ }
162
+ // ─── Utilities ───────────────────────────────────────────
163
+ visibleWidth(str) {
164
+ return [...str.replace(ANSI_FULL_REGEX, "")].length;
165
+ }
166
+ clearLine() {
167
+ process.stdout.write("\r\x1B[K");
168
+ }
169
+ writeLine(line, color = "") {
170
+ this.clearLine();
171
+ process.stdout.write(color + line + colors.reset);
172
+ if (this.visibleWidth(line) >= MAX_WIDTH) {
173
+ process.stdout.write("\n");
174
+ }
175
+ }
176
+ addLine(text, opts = {}) {
177
+ this.previousLines.push({
178
+ text,
179
+ isRaw: opts.isRaw,
180
+ hasPrompt: opts.hasPrompt ?? this.showPrompt
181
+ });
182
+ if (this.previousLines.length > 200) {
183
+ this.previousLines.splice(0, 100);
184
+ }
185
+ }
186
+ // ─── REFORMAT (RESTORED) ─────────────────────────────────
187
+ reformat(text, opts) {
188
+ const { width, align = "left", padChar = " " } = opts;
189
+ const normalized = text.replace(/\r?\n/g, " ").replace(/[\uFEFF\xA0\t]/g, " ").replace(/\s{2,}/g, " ").trim();
190
+ const lines = [];
191
+ let buf = "";
192
+ let vis = 0;
193
+ const tokens = normalized.split(
194
+ /(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\)))/g
195
+ );
196
+ for (const token of tokens) {
197
+ if (ANSI_FULL_REGEX.test(token)) {
198
+ buf += token;
199
+ continue;
200
+ }
201
+ for (const ch of [...token]) {
202
+ if (vis >= width) {
203
+ lines.push(this.applyPadding(buf, vis, width, align, padChar));
204
+ buf = "";
205
+ vis = 0;
206
+ }
207
+ buf += ch;
208
+ vis++;
209
+ }
210
+ }
211
+ if (buf) {
212
+ lines.push(this.applyPadding(buf, vis, width, align, padChar));
213
+ }
214
+ return lines;
215
+ }
216
+ applyPadding(text, visible, width, align, padChar) {
217
+ const pad = Math.max(0, width - visible);
218
+ switch (align) {
219
+ case "right":
220
+ return padChar.repeat(pad) + text;
221
+ case "center": {
222
+ const left = Math.floor(pad / 2);
223
+ const right = pad - left;
224
+ return padChar.repeat(left) + text + padChar.repeat(right);
225
+ }
226
+ default:
227
+ return text + padChar.repeat(pad);
228
+ }
229
+ }
230
+ // ─── Printing ────────────────────────────────────────────
231
+ print(msg) {
232
+ const lines = wrapLine(
233
+ this.showPrompt ? this.prompt + msg : msg,
234
+ MAX_WIDTH,
235
+ 0,
236
+ "word"
237
+ );
238
+ for (let i = 0; i < lines.length; i++) {
239
+ this.writeLine(lines[i]);
240
+ if (i < lines.length - 1) process.stdout.write("\n");
241
+ this.addLine(lines[i]);
242
+ }
243
+ this.showPrompt = false;
244
+ return true;
245
+ }
246
+ println(msg = "") {
247
+ if (msg) this.print(msg);
248
+ process.stdout.write("\n");
249
+ this.addLine("");
250
+ this.showPrompt = true;
251
+ return true;
252
+ }
253
+ printRaw(line) {
254
+ process.stdout.write(line);
255
+ this.addLine(line, { isRaw: true });
256
+ return true;
257
+ }
258
+ printlnRaw(line = "") {
259
+ this.printRaw(line);
260
+ process.stdout.write("\n");
261
+ this.addLine("", { isRaw: true });
262
+ return true;
263
+ }
264
+ error(msg) {
265
+ const lines = wrapLine(
266
+ this.showPrompt ? this.errorPrompt + msg : msg,
267
+ MAX_WIDTH,
268
+ 0,
269
+ "word"
270
+ );
271
+ for (let i = 0; i < lines.length; i++) {
272
+ this.writeLine(lines[i], colors.brightRed);
273
+ if (i < lines.length - 1) process.stdout.write("\n");
274
+ this.addLine(lines[i]);
275
+ }
276
+ process.stdout.write("\n");
277
+ this.showPrompt = true;
278
+ return true;
279
+ }
280
+ // ─── History ─────────────────────────────────────────────
281
+ clearHistory() {
282
+ this.previousLines = [];
283
+ }
284
+ getHistory() {
285
+ return [...this.previousLines];
286
+ }
287
+ }
288
+ const logger = new Logger();
289
+ class JasmineConsoleReporter {
290
+ print = (message) => process.stdout.write(message);
291
+ showColors = true;
292
+ specCount = 0;
293
+ executableSpecCount = 0;
294
+ failureCount = 0;
295
+ failedSpecs = [];
296
+ pendingSpecs = [];
297
+ alwaysListPendingSpecs = true;
298
+ ansi = {
299
+ green: "\x1B[32m",
300
+ red: "\x1B[31m",
301
+ yellow: "\x1B[33m",
302
+ none: "\x1B[0m"
303
+ };
304
+ failedSuites = [];
305
+ stackFilter = (stack) => stack;
306
+ randomSeedReproductionCmd(seed) {
307
+ return "jasmine --random=true --seed=" + seed;
308
+ }
309
+ /**
310
+ * Configures the reporter.
311
+ */
312
+ setOptions(options) {
313
+ if (options.print) {
314
+ this.print = options.print;
315
+ }
316
+ this.showColors = options.showColors || false;
317
+ if (options.stackFilter) {
318
+ this.stackFilter = options.stackFilter;
319
+ }
320
+ if (options.randomSeedReproductionCmd) {
321
+ this.randomSeedReproductionCmd = options.randomSeedReproductionCmd;
322
+ }
323
+ if (options.alwaysListPendingSpecs !== void 0) {
324
+ this.alwaysListPendingSpecs = options.alwaysListPendingSpecs;
325
+ }
326
+ }
327
+ jasmineStarted(options) {
328
+ this.specCount = 0;
329
+ this.executableSpecCount = 0;
330
+ this.failureCount = 0;
331
+ if (options?.order?.random) {
332
+ this.print("Randomized with seed " + options.order.seed);
333
+ this.printNewline();
334
+ }
335
+ this.print("Started");
336
+ this.printNewline();
337
+ }
338
+ jasmineDone(result) {
339
+ if (result.failedExpectations) {
340
+ this.failureCount += result.failedExpectations.length;
341
+ }
342
+ this.printNewline();
343
+ this.printNewline();
344
+ if (this.failedSpecs.length > 0) {
345
+ this.print("Failures:");
346
+ }
347
+ for (let i = 0; i < this.failedSpecs.length; i++) {
348
+ this.specFailureDetails(this.failedSpecs[i], i + 1);
349
+ }
350
+ for (let i = 0; i < this.failedSuites.length; i++) {
351
+ this.suiteFailureDetails(this.failedSuites[i]);
352
+ }
353
+ if (result.failedExpectations?.length > 0) {
354
+ this.suiteFailureDetails({
355
+ fullName: "top suite",
356
+ failedExpectations: result.failedExpectations
357
+ });
358
+ }
359
+ if (this.alwaysListPendingSpecs || result.overallStatus === "passed") {
360
+ if (this.pendingSpecs.length > 0) {
361
+ this.print("Pending:");
362
+ }
363
+ for (let i = 0; i < this.pendingSpecs.length; i++) {
364
+ this.pendingSpecDetails(this.pendingSpecs[i], i + 1);
365
+ }
366
+ }
367
+ if (this.specCount > 0) {
368
+ this.printNewline();
369
+ if (this.executableSpecCount !== this.specCount) {
370
+ this.print(
371
+ "Ran " + this.executableSpecCount + " of " + this.specCount + this.plural(" spec", this.specCount)
372
+ );
373
+ this.printNewline();
374
+ }
375
+ let specCounts = this.executableSpecCount + " " + this.plural("spec", this.executableSpecCount) + ", " + this.failureCount + " " + this.plural("failure", this.failureCount);
376
+ if (this.pendingSpecs.length) {
377
+ specCounts += ", " + this.pendingSpecs.length + " pending " + this.plural("spec", this.pendingSpecs.length);
378
+ }
379
+ this.print(specCounts);
380
+ } else {
381
+ this.print("No specs found");
382
+ }
383
+ this.printNewline();
384
+ const seconds = result ? result.totalTime / 1e3 : 0;
385
+ this.print("Finished in " + seconds + " " + this.plural("second", seconds));
386
+ this.printNewline();
387
+ if (result && result.overallStatus === "incomplete") {
388
+ this.print("Incomplete: " + result.incompleteReason);
389
+ this.printNewline();
390
+ }
391
+ if (result.order?.random) {
392
+ this.print("Randomized with seed " + result.order.seed);
393
+ this.print(" (" + this.randomSeedReproductionCmd(result.order.seed) + ")");
394
+ this.printNewline();
395
+ }
396
+ }
397
+ specDone(result) {
398
+ this.specCount++;
399
+ if (result.status == "pending") {
400
+ this.pendingSpecs.push(result);
401
+ this.executableSpecCount++;
402
+ this.print(this.colored("yellow", "*"));
403
+ return;
404
+ }
405
+ if (result.status == "passed") {
406
+ this.executableSpecCount++;
407
+ this.print(this.colored("green", "."));
408
+ return;
409
+ }
410
+ if (result.status == "failed") {
411
+ this.failureCount++;
412
+ this.failedSpecs.push(result);
413
+ this.executableSpecCount++;
414
+ this.print(this.colored("red", "F"));
415
+ }
416
+ }
417
+ suiteDone(result) {
418
+ if (result.failedExpectations && result.failedExpectations.length > 0) {
419
+ this.failureCount++;
420
+ this.failedSuites.push(result);
421
+ }
422
+ }
423
+ reporterCapabilities = { parallel: true };
424
+ printNewline() {
425
+ this.print("\n");
426
+ }
427
+ colored(color, str) {
428
+ return this.showColors ? this.ansi[color] + str + this.ansi.none : str;
429
+ }
430
+ plural(str, count) {
431
+ return count == 1 ? str : str + "s";
432
+ }
433
+ repeat(thing, times) {
434
+ return Array.from({ length: times }, () => thing);
435
+ }
436
+ indent(str, spaces) {
437
+ const lines = (str || "").split("\n");
438
+ return lines.map((line) => this.repeat(" ", spaces).join("") + line).join("\n");
439
+ }
440
+ specFailureDetails(result, failedSpecNumber) {
441
+ this.printNewline();
442
+ this.print(failedSpecNumber + ") ");
443
+ this.print(result.fullName);
444
+ this.printFailedExpectations(result);
445
+ if (result.debugLogs?.length) {
446
+ this.printNewline();
447
+ this.print(this.indent("Debug logs:", 2));
448
+ this.printNewline();
449
+ for (const entry of result.debugLogs) {
450
+ this.print(this.indent(`${entry.timestamp}ms: ${entry.message}`, 4));
451
+ this.printNewline();
452
+ }
453
+ }
454
+ }
455
+ suiteFailureDetails(result) {
456
+ this.printNewline();
457
+ this.print("Suite error: " + result.fullName);
458
+ this.printFailedExpectations(result);
459
+ }
460
+ printFailedExpectations(result) {
461
+ for (let i = 0; i < result.failedExpectations.length; i++) {
462
+ const failedExpectation = result.failedExpectations[i];
463
+ this.printNewline();
464
+ this.print(this.indent("Message:", 2));
465
+ this.printNewline();
466
+ this.print(this.colored("red", this.indent(failedExpectation.message, 4)));
467
+ this.printNewline();
468
+ this.print(this.indent("Stack:", 2));
469
+ this.printNewline();
470
+ this.print(this.indent(this.stackFilter(failedExpectation.stack), 4));
471
+ }
472
+ if (result.failedExpectations.length === 0 && Array.isArray(result.passedExpectations) && result.passedExpectations.length === 0) {
473
+ this.printNewline();
474
+ this.print(this.indent("Message:", 2));
475
+ this.printNewline();
476
+ this.print(this.colored("red", this.indent("Spec has no expectations", 4)));
477
+ }
478
+ this.printNewline();
479
+ }
480
+ pendingSpecDetails(result, pendingSpecNumber) {
481
+ this.printNewline();
482
+ this.printNewline();
483
+ this.print(pendingSpecNumber + ") ");
484
+ this.print(result.fullName);
485
+ this.printNewline();
486
+ let pendingReason = "No reason given";
487
+ if (result.pendingReason && result.pendingReason !== "") {
488
+ pendingReason = result.pendingReason;
489
+ }
490
+ this.print(this.indent(this.colored("yellow", pendingReason), 2));
491
+ this.printNewline();
492
+ }
493
+ }
494
+ class AwaitableJasmineConsoleReporter extends JasmineConsoleReporter {
495
+ resolveComplete;
496
+ complete;
497
+ constructor() {
498
+ super();
499
+ this.complete = new Promise((resolve) => {
500
+ this.resolveComplete = resolve;
501
+ });
502
+ }
503
+ jasmineDone(result) {
504
+ super.jasmineDone(result);
505
+ this.resolveComplete?.(result);
506
+ }
507
+ }
508
+ class JSONCleaner {
509
+ options;
510
+ constructor(options = {}) {
511
+ this.options = {
512
+ removeComments: options.removeComments !== false,
513
+ removeTrailingCommas: options.removeTrailingCommas !== false,
514
+ normalizeWhitespace: options.normalizeWhitespace === true,
515
+ allowSingleQuotes: options.allowSingleQuotes === true,
516
+ preserveNewlines: options.preserveNewlines !== false,
517
+ strict: options.strict === true
518
+ };
519
+ }
520
+ /**
521
+ * Clean JSON string by removing comments and fixing common issues
522
+ */
523
+ clean(jsonString) {
524
+ if (typeof jsonString !== "string") {
525
+ throw new Error("Input must be a string");
526
+ }
527
+ let result = jsonString;
528
+ if (this.options.removeComments) {
529
+ result = this.stripComments(result);
530
+ }
531
+ if (this.options.removeTrailingCommas) {
532
+ result = this.removeTrailingCommas(result);
533
+ }
534
+ if (this.options.allowSingleQuotes) {
535
+ result = this.normalizeSingleQuotes(result);
536
+ }
537
+ if (this.options.normalizeWhitespace) {
538
+ result = this.normalizeWhitespace(result);
539
+ }
540
+ if (this.options.strict) {
541
+ try {
542
+ JSON.parse(result);
543
+ } catch (error) {
544
+ const message = error instanceof Error ? error.message : String(error);
545
+ throw new Error(`Invalid JSON after cleaning: ${message}`);
546
+ }
547
+ }
548
+ return result;
549
+ }
550
+ /**
551
+ * Strip single-line and multi-line comments
552
+ * More robust than simple regex - handles edge cases
553
+ */
554
+ stripComments(str) {
555
+ let result = "";
556
+ let i = 0;
557
+ const state = {
558
+ inString: false,
559
+ stringChar: null,
560
+ escapeNext: false
561
+ };
562
+ while (i < str.length) {
563
+ const char = str[i];
564
+ const nextChar = str[i + 1];
565
+ if (state.escapeNext) {
566
+ result += char;
567
+ state.escapeNext = false;
568
+ i++;
569
+ continue;
570
+ }
571
+ if (char === "\\" && state.inString) {
572
+ result += char;
573
+ state.escapeNext = true;
574
+ i++;
575
+ continue;
576
+ }
577
+ if ((char === '"' || char === "'") && !state.escapeNext) {
578
+ if (!state.inString) {
579
+ state.inString = true;
580
+ state.stringChar = char;
581
+ result += char;
582
+ } else if (char === state.stringChar) {
583
+ state.inString = false;
584
+ state.stringChar = null;
585
+ result += char;
586
+ } else {
587
+ result += char;
588
+ }
589
+ i++;
590
+ continue;
591
+ }
592
+ if (!state.inString) {
593
+ if (char === "/" && nextChar === "/") {
594
+ i += 2;
595
+ while (i < str.length && str[i] !== "\n" && str[i] !== "\r") {
596
+ i++;
597
+ }
598
+ if (i < str.length && this.options.preserveNewlines) {
599
+ result += str[i];
600
+ }
601
+ i++;
602
+ continue;
603
+ }
604
+ if (char === "/" && nextChar === "*") {
605
+ i += 2;
606
+ let foundEnd = false;
607
+ let newlineCount = 0;
608
+ while (i < str.length - 1) {
609
+ if (str[i] === "\n") newlineCount++;
610
+ if (str[i] === "*" && str[i + 1] === "/") {
611
+ i += 2;
612
+ foundEnd = true;
613
+ break;
614
+ }
615
+ i++;
616
+ }
617
+ if (!foundEnd) {
618
+ throw new Error("Unclosed multi-line comment");
619
+ }
620
+ if (this.options.preserveNewlines && newlineCount > 0) {
621
+ result += "\n".repeat(Math.min(newlineCount, 2));
622
+ }
623
+ continue;
624
+ }
625
+ }
626
+ result += char;
627
+ i++;
628
+ }
629
+ if (state.inString) {
630
+ throw new Error("Unclosed string in JSON");
631
+ }
632
+ return result;
633
+ }
634
+ /**
635
+ * Remove trailing commas before closing brackets/braces
636
+ */
637
+ removeTrailingCommas(str) {
638
+ let result = "";
639
+ let i = 0;
640
+ const state = {
641
+ inString: false,
642
+ stringChar: null,
643
+ escapeNext: false
644
+ };
645
+ while (i < str.length) {
646
+ const char = str[i];
647
+ if (state.escapeNext) {
648
+ result += char;
649
+ state.escapeNext = false;
650
+ i++;
651
+ continue;
652
+ }
653
+ if (char === "\\" && state.inString) {
654
+ result += char;
655
+ state.escapeNext = true;
656
+ i++;
657
+ continue;
658
+ }
659
+ if ((char === '"' || char === "'") && !state.escapeNext) {
660
+ if (!state.inString) {
661
+ state.inString = true;
662
+ state.stringChar = char;
663
+ } else if (char === state.stringChar) {
664
+ state.inString = false;
665
+ state.stringChar = null;
666
+ }
667
+ result += char;
668
+ i++;
669
+ continue;
670
+ }
671
+ if (!state.inString && char === ",") {
672
+ let j = i + 1;
673
+ while (j < str.length && /\s/.test(str[j])) {
674
+ j++;
675
+ }
676
+ if (j < str.length && (str[j] === "]" || str[j] === "}")) {
677
+ i++;
678
+ continue;
679
+ }
680
+ }
681
+ result += char;
682
+ i++;
683
+ }
684
+ return result;
685
+ }
686
+ /**
687
+ * Convert single quotes to double quotes (outside of strings)
688
+ */
689
+ normalizeSingleQuotes(str) {
690
+ let result = "";
691
+ let i = 0;
692
+ const state = {
693
+ inString: false,
694
+ stringChar: null,
695
+ escapeNext: false
696
+ };
697
+ while (i < str.length) {
698
+ const char = str[i];
699
+ if (state.escapeNext) {
700
+ result += char;
701
+ state.escapeNext = false;
702
+ i++;
703
+ continue;
704
+ }
705
+ if (char === "\\" && state.inString) {
706
+ result += char;
707
+ state.escapeNext = true;
708
+ i++;
709
+ continue;
710
+ }
711
+ if ((char === '"' || char === "'") && !state.escapeNext) {
712
+ if (!state.inString) {
713
+ state.inString = true;
714
+ state.stringChar = char;
715
+ result += '"';
716
+ } else if (char === state.stringChar) {
717
+ state.inString = false;
718
+ state.stringChar = null;
719
+ result += '"';
720
+ } else {
721
+ if (char === '"') {
722
+ result += '\\"';
723
+ } else {
724
+ result += char;
725
+ }
726
+ }
727
+ i++;
728
+ continue;
729
+ }
730
+ result += char;
731
+ i++;
732
+ }
733
+ return result;
734
+ }
735
+ /**
736
+ * Normalize whitespace
737
+ */
738
+ normalizeWhitespace(str) {
739
+ return str.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
740
+ }
741
+ /**
742
+ * Parse JSON string with automatic cleaning
743
+ */
744
+ parse(jsonString) {
745
+ const cleaned = this.clean(jsonString);
746
+ return JSON.parse(cleaned);
747
+ }
748
+ /**
749
+ * Read and clean JSON file
750
+ */
751
+ readFile(filePath, encoding = "utf8") {
752
+ const content = fs.readFileSync(filePath, encoding);
753
+ return this.clean(content);
754
+ }
755
+ /**
756
+ * Read, clean, and parse JSON file
757
+ */
758
+ parseFile(filePath, encoding = "utf8") {
759
+ const cleaned = this.readFile(filePath, encoding);
760
+ return JSON.parse(cleaned);
761
+ }
762
+ /**
763
+ * Clean and write JSON file
764
+ */
765
+ writeFile(filePath, jsonString, encoding = "utf8") {
766
+ const cleaned = this.clean(jsonString);
767
+ fs.writeFileSync(filePath, cleaned, encoding);
768
+ }
769
+ /**
770
+ * Clean and prettify JSON file
771
+ */
772
+ prettifyFile(inputPath, outputPath = null, indent = 2) {
773
+ const parsed = this.parseFile(inputPath);
774
+ const prettified = JSON.stringify(parsed, null, indent);
775
+ const targetPath = outputPath || inputPath;
776
+ fs.writeFileSync(targetPath, prettified, "utf8");
777
+ }
778
+ }
779
+ const norm = (p) => p.replace(/\\/g, "/");
780
+ const packageRoot = norm(path.resolve(__dirname, ".."));
781
+ const packageRequire = createRequire(path.join(packageRoot, "package.json"));
782
+ const vscodeLaunchConfigName = "Debug current spec (jasmine)";
783
+ function getRuntimeEnv() {
784
+ const runtimeProcess = globalThis.process;
785
+ return runtimeProcess?.env ?? {};
786
+ }
787
+ function isRunningInVsCode() {
788
+ const env = getRuntimeEnv();
789
+ return env.TERM_PROGRAM === "vscode" || typeof env.VSCODE_PID === "string" || typeof env.VSCODE_CWD === "string" || typeof env.VSCODE_INSPECTOR_OPTIONS === "string";
790
+ }
791
+ function printHelp() {
792
+ logger.println("jasmine: run a single Jasmine spec in Node");
793
+ logger.println("");
794
+ logger.println("Usage:");
795
+ logger.println(" npx jasmine --spec <path-to-spec>");
796
+ logger.println(" npx jasmine init");
797
+ logger.println("");
798
+ logger.println("Commands:");
799
+ logger.println(" init Create/update .vscode/launch.json (VS Code debug; requires VS Code)");
800
+ logger.println("");
801
+ logger.println("Options:");
802
+ logger.println(" --spec <path> Path to a single spec file");
803
+ logger.println(" --random Randomize spec order");
804
+ logger.println(" --seed <number> Seed used for randomization");
805
+ logger.println(" --stop-on-fail Stop on first expectation failure");
806
+ logger.println(" --help Show this help");
807
+ logger.println("");
808
+ logger.println("TypeScript + tsconfig paths (recommended):");
809
+ logger.println(
810
+ " node --loader @epikodelabs/testify/esm-loader.mjs ./node_modules/@epikodelabs/testify/bin/jasmine --spec <file>"
811
+ );
812
+ logger.println("");
813
+ logger.println("VS Code debug config name:");
814
+ logger.println(` ${vscodeLaunchConfigName}`);
815
+ }
816
+ function parseArgs(argv) {
817
+ const args = argv.slice(2);
818
+ const get = (flag) => {
819
+ const index = args.indexOf(flag);
820
+ if (index === -1) return void 0;
821
+ return args[index + 1];
822
+ };
823
+ const help = args.includes("--help") || args.includes("-h");
824
+ const command = args[0];
825
+ const initLaunchConfig = args.includes("--init-launch-config") || command === "init";
826
+ const specRaw = get("--spec");
827
+ if (help) {
828
+ return {
829
+ spec: specRaw ? norm(path.resolve(process.cwd(), specRaw)) : "",
830
+ random: args.includes("--random"),
831
+ stopOnFail: args.includes("--stop-on-fail"),
832
+ seed: get("--seed") ? Number(get("--seed")) : void 0,
833
+ help: true,
834
+ initLaunchConfig
835
+ };
836
+ }
837
+ if (command && !command.startsWith("-") && command !== "init") {
838
+ logger.error(`ERROR: Unknown command: ${command}`);
839
+ logger.println("");
840
+ printHelp();
841
+ process.exit(1);
842
+ }
843
+ if (initLaunchConfig) {
844
+ return {
845
+ spec: specRaw ? norm(path.resolve(process.cwd(), specRaw)) : "",
846
+ random: args.includes("--random"),
847
+ stopOnFail: args.includes("--stop-on-fail"),
848
+ seed: get("--seed") ? Number(get("--seed")) : void 0,
849
+ help: false,
850
+ initLaunchConfig: true
851
+ };
852
+ }
853
+ if (!specRaw) {
854
+ logger.error("ERROR: Missing required --spec <path>");
855
+ logger.println("");
856
+ printHelp();
857
+ process.exit(1);
858
+ }
859
+ const spec = norm(path.resolve(process.cwd(), specRaw));
860
+ if (!fs__default.existsSync(spec)) {
861
+ logger.error(`ERROR: Spec file not found: ${spec}`);
862
+ process.exit(1);
863
+ }
864
+ const seedRaw = get("--seed");
865
+ return {
866
+ spec,
867
+ random: args.includes("--random"),
868
+ stopOnFail: args.includes("--stop-on-fail"),
869
+ seed: seedRaw ? Number(seedRaw) : void 0,
870
+ help: false,
871
+ initLaunchConfig: false
872
+ };
873
+ }
874
+ function normalizeCliArgs(args) {
875
+ const normalized = [...args];
876
+ for (let i = 0; i < normalized.length; i += 1) {
877
+ if (normalized[i] === "--spec" && typeof normalized[i + 1] === "string") {
878
+ normalized[i + 1] = norm(normalized[i + 1]);
879
+ i += 1;
880
+ }
881
+ }
882
+ return normalized;
883
+ }
884
+ function isTypeScriptLike(filePath) {
885
+ const ext = path.extname(filePath).toLowerCase();
886
+ return ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts";
887
+ }
888
+ function hasEsmLoader() {
889
+ const fromNodeOptions = (getRuntimeEnv().NODE_OPTIONS ?? "").split(/\s+/g).filter(Boolean);
890
+ const argv = [...process.execArgv, ...fromNodeOptions];
891
+ return argv.includes("--loader") || argv.some((a) => a.startsWith("--loader="));
892
+ }
893
+ function findNearestTsconfig(startDir) {
894
+ let current = norm(path.resolve(startDir));
895
+ while (true) {
896
+ const candidate = norm(path.join(current, "tsconfig.json"));
897
+ if (fs__default.existsSync(candidate)) return candidate;
898
+ const parent = path.dirname(current);
899
+ if (parent === current) return null;
900
+ current = parent;
901
+ }
902
+ }
903
+ function getDefaultVsCodeLaunchConfiguration() {
904
+ return {
905
+ type: "node",
906
+ request: "launch",
907
+ name: vscodeLaunchConfigName,
908
+ runtimeExecutable: "node",
909
+ runtimeArgs: ["--loader", "@epikodelabs/testify/esm-loader.mjs", "--enable-source-maps"],
910
+ program: "${workspaceFolder}/node_modules/@epikodelabs/testify/bin/jasmine",
911
+ args: ["--spec", "${file}"],
912
+ env: {
913
+ TS_NODE_PROJECT: "${workspaceFolder}/tsconfig.json"
914
+ },
915
+ cwd: "${workspaceFolder}",
916
+ console: "integratedTerminal",
917
+ skipFiles: ["<node_internals>/**"]
918
+ };
919
+ }
920
+ function initVsCodeLaunchConfig() {
921
+ const vscodeDir = norm(path.resolve(process.cwd(), ".vscode"));
922
+ const launchJsonPath = norm(path.join(vscodeDir, "launch.json"));
923
+ const config = getDefaultVsCodeLaunchConfiguration();
924
+ fs__default.mkdirSync(vscodeDir, { recursive: true });
925
+ if (!fs__default.existsSync(launchJsonPath)) {
926
+ const launchJson = { version: "0.2.0", configurations: [config] };
927
+ fs__default.writeFileSync(launchJsonPath, `${JSON.stringify(launchJson, null, 2)}
928
+ `);
929
+ logger.println(`Created VS Code launch config at ${launchJsonPath}`);
930
+ logger.println(`Added configuration: ${vscodeLaunchConfigName}`);
931
+ return;
932
+ }
933
+ const raw = fs__default.readFileSync(launchJsonPath, "utf-8");
934
+ let parsed;
935
+ try {
936
+ parsed = JSON.parse(raw);
937
+ } catch {
938
+ try {
939
+ parsed = new JSONCleaner().parse(raw);
940
+ } catch (error) {
941
+ logger.error(`ERROR: Failed to parse existing VS Code launch config: ${launchJsonPath}`);
942
+ logger.error(String(error));
943
+ logger.println("");
944
+ logger.println("Add this configuration manually:");
945
+ logger.println(`${JSON.stringify(getDefaultVsCodeLaunchConfiguration(), null, 2)}`);
946
+ process.exit(1);
947
+ }
948
+ }
949
+ if (!parsed || typeof parsed !== "object") parsed = {};
950
+ if (!Array.isArray(parsed.configurations)) parsed.configurations = [];
951
+ const programSuffix = "/bin/jasmine";
952
+ const alreadyHasConfig = parsed.configurations.some((c) => {
953
+ if (!c || typeof c !== "object") return false;
954
+ if (c.name === vscodeLaunchConfigName) return true;
955
+ const program = typeof c.program === "string" ? c.program.replace(/\\/g, "/") : "";
956
+ const args = Array.isArray(c.args) ? c.args : [];
957
+ return program.endsWith(programSuffix) && args.includes("--spec");
958
+ });
959
+ if (alreadyHasConfig) {
960
+ logger.println(`VS Code launch config already contains: ${vscodeLaunchConfigName}`);
961
+ return;
962
+ }
963
+ parsed.version ??= "0.2.0";
964
+ parsed.configurations.unshift(config);
965
+ fs__default.writeFileSync(launchJsonPath, `${JSON.stringify(parsed, null, 2)}
966
+ `);
967
+ logger.println(`Updated VS Code launch config at ${launchJsonPath}`);
968
+ logger.println(`Added configuration: ${vscodeLaunchConfigName}`);
969
+ }
970
+ async function respawnWithLoader(args) {
971
+ const { spawn } = await __vitePreload(async () => {
972
+ const { spawn: spawn2 } = await import("child_process");
973
+ return { spawn: spawn2 };
974
+ }, true ? [] : void 0);
975
+ const tsconfig = findNearestTsconfig(path.dirname(args.spec));
976
+ const env = { ...getRuntimeEnv() };
977
+ if (tsconfig) env.TS_NODE_PROJECT = tsconfig;
978
+ env.TS_NODE_TRANSPILE_ONLY ??= "true";
979
+ const loaderPath = norm(path.join(packageRoot, "esm-loader.mjs"));
980
+ const loaderSpecifier = fs__default.existsSync(loaderPath) ? pathToFileURL(loaderPath).href : "@epikodelabs/testify/esm-loader.mjs";
981
+ const child = spawn(
982
+ process.execPath,
983
+ [
984
+ "--loader",
985
+ loaderSpecifier,
986
+ "--enable-source-maps",
987
+ process.argv[1],
988
+ ...normalizeCliArgs(process.argv.slice(2))
989
+ ],
990
+ { stdio: "inherit", env, cwd: process.cwd() }
991
+ );
992
+ child.on("exit", (code) => process.exit(code ?? 1));
993
+ while (true) {
994
+ await new Promise((r) => setTimeout(r, 1e6));
995
+ }
996
+ }
997
+ async function loadJasmine() {
998
+ const jasmineCorePath = norm(packageRequire.resolve("jasmine-core/lib/jasmine-core/jasmine.js"));
999
+ const jasmineCore = await import(pathToFileURL(jasmineCorePath).href);
1000
+ const jasmineRequire = jasmineCore.default;
1001
+ const jasmineInstance = jasmineRequire.core(jasmineRequire);
1002
+ const jasmineEnv = jasmineInstance.getEnv();
1003
+ Object.assign(globalThis, jasmineRequire.interface(jasmineInstance, jasmineEnv));
1004
+ globalThis.jasmine = {
1005
+ ...globalThis.jasmine ?? {},
1006
+ ...jasmineInstance
1007
+ };
1008
+ return { jasmineEnv, jasmineInstance };
1009
+ }
1010
+ async function main() {
1011
+ const args = parseArgs(process.argv);
1012
+ if (args.help) {
1013
+ printHelp();
1014
+ process.exit(0);
1015
+ }
1016
+ if (args.initLaunchConfig) {
1017
+ if (!isRunningInVsCode()) {
1018
+ logger.error("ERROR: `npx jasmine init` is only supported when run from VS Code.");
1019
+ logger.println("");
1020
+ logger.println("Open VS Code, then run this from the integrated terminal (Terminal -> New Terminal).");
1021
+ process.exit(1);
1022
+ }
1023
+ initVsCodeLaunchConfig();
1024
+ process.exit(0);
1025
+ }
1026
+ if (isTypeScriptLike(args.spec) && !hasEsmLoader() && !process.execArgv.join(" ").includes("--inspect")) {
1027
+ await respawnWithLoader(args);
1028
+ }
1029
+ const { jasmineEnv } = await loadJasmine();
1030
+ process.on("unhandledRejection", (error) => {
1031
+ logger.error(`ERROR: Unhandled rejection: ${error}`);
1032
+ process.exit(1);
1033
+ });
1034
+ process.on("uncaughtException", (error) => {
1035
+ logger.error(`ERROR: Uncaught exception: ${error}`);
1036
+ process.exit(1);
1037
+ });
1038
+ jasmineEnv.configure({
1039
+ random: args.random,
1040
+ stopSpecOnExpectationFailure: args.stopOnFail,
1041
+ seed: args.seed
1042
+ });
1043
+ const reporter = new AwaitableJasmineConsoleReporter();
1044
+ jasmineEnv.addReporter(reporter);
1045
+ await import(pathToFileURL(args.spec).href);
1046
+ await jasmineEnv.execute();
1047
+ const exitCode = (await reporter.complete)?.overallStatus === "passed" ? 0 : 1;
1048
+ process.exit(exitCode);
1049
+ }
1050
+ main().catch((error) => {
1051
+ logger.error(`ERROR: Failed to run jasmine: ${error.stack ?? error}`);
1052
+ process.exit(1);
1053
+ });