@vitronai/themis 0.1.4 → 0.1.6

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/src/generate.js CHANGED
@@ -1,16 +1,16 @@
1
1
  const crypto = require('crypto');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const Module = require('module');
5
4
  const { execFileSync } = require('child_process');
6
5
  const ts = require('typescript');
6
+ const { ARTIFACT_RELATIVE_PATHS } = require('./artifact-paths');
7
7
 
8
8
  const GENERATED_MARKER = '// Generated by Themis code scan. Do not edit manually.';
9
9
  const GENERATED_HELPER_NAME = '_themis.contract-runtime.js';
10
- const GENERATED_MAP_ARTIFACT = path.join('.themis', 'generate-map.json');
11
- const GENERATED_RESULT_ARTIFACT = path.join('.themis', 'generate-last.json');
12
- const GENERATED_HANDOFF_ARTIFACT = path.join('.themis', 'generate-handoff.json');
13
- const GENERATED_BACKLOG_ARTIFACT = path.join('.themis', 'generate-backlog.json');
10
+ const GENERATED_MAP_ARTIFACT = ARTIFACT_RELATIVE_PATHS.generateMap;
11
+ const GENERATED_RESULT_ARTIFACT = ARTIFACT_RELATIVE_PATHS.generateResult;
12
+ const GENERATED_HANDOFF_ARTIFACT = ARTIFACT_RELATIVE_PATHS.generateHandoff;
13
+ const GENERATED_BACKLOG_ARTIFACT = ARTIFACT_RELATIVE_PATHS.generateBacklog;
14
14
  const PROJECT_PROVIDER_CANDIDATES = ['themis.generate.js', 'themis.generate.cjs'];
15
15
  const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
16
16
  const TEST_FILE_PATTERN = /\.(test|spec)\.(js|jsx|ts|tsx)$/;
@@ -41,1182 +41,15 @@ const SCAFFOLD_HINT_META = Object.freeze({
41
41
  version: 1
42
42
  });
43
43
 
44
- const GENERATED_HELPER_SOURCE = `${GENERATED_MARKER}
45
- const crypto = require('crypto');
46
- const fs = require('fs');
47
- const path = require('path');
48
- const Module = require('module');
49
-
50
- function listExportNames(moduleExports) {
51
- const keys = getEnumerableKeys(moduleExports);
52
- const namedKeys = keys.filter((key) => key !== '__esModule' && key !== 'default').sort();
53
- const hasExplicitDefault = keys.includes('default');
54
- const needsImplicitDefault = hasImplicitDefaultExport(moduleExports, namedKeys);
55
- const names = [];
56
-
57
- if (hasExplicitDefault || needsImplicitDefault) {
58
- names.push('default');
59
- }
60
-
61
- names.push(...namedKeys);
62
- return names;
63
- }
64
-
65
- function buildModuleContract(moduleExports) {
66
- const names = listExportNames(moduleExports);
67
- const contract = {};
68
-
69
- for (const name of names) {
70
- contract[name] = normalizeModuleValue(readExportValue(moduleExports, name));
71
- }
72
-
73
- return contract;
74
- }
75
-
76
- function readExportValue(moduleExports, name) {
77
- if (name === 'default') {
78
- if (
79
- moduleExports &&
80
- (typeof moduleExports === 'object' || typeof moduleExports === 'function') &&
81
- Object.prototype.hasOwnProperty.call(moduleExports, 'default')
82
- ) {
83
- return moduleExports.default;
84
- }
85
-
86
- return moduleExports;
87
- }
88
-
89
- return moduleExports[name];
90
- }
91
-
92
- function getEnumerableKeys(value) {
93
- if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
94
- return [];
95
- }
96
-
97
- return Object.keys(value).sort();
98
- }
99
-
100
- function hasImplicitDefaultExport(moduleExports, namedKeys) {
101
- if (moduleExports === null || moduleExports === undefined) {
102
- return true;
103
- }
104
-
105
- if (typeof moduleExports === 'function') {
106
- return true;
107
- }
108
-
109
- if (typeof moduleExports !== 'object') {
110
- return true;
111
- }
112
-
113
- if (Array.isArray(moduleExports)) {
114
- return true;
115
- }
116
-
117
- return namedKeys.length === 0;
118
- }
119
-
120
- function normalizeModuleValue(value) {
121
- if (value === null) {
122
- return { kind: 'null' };
123
- }
124
-
125
- const primitiveType = typeof value;
126
- if (primitiveType === 'undefined') {
127
- return { kind: 'undefined' };
128
- }
129
- if (primitiveType === 'string' || primitiveType === 'number' || primitiveType === 'boolean') {
130
- return { kind: primitiveType, value };
131
- }
132
- if (primitiveType === 'bigint') {
133
- return { kind: 'bigint', value: String(value) };
134
- }
135
- if (primitiveType === 'symbol') {
136
- return { kind: 'symbol', value: String(value) };
137
- }
138
- if (primitiveType === 'function') {
139
- return normalizeFunction(value);
140
- }
141
-
142
- if (Array.isArray(value)) {
143
- return {
144
- kind: 'array',
145
- length: value.length,
146
- itemTypes: [...new Set(value.map(classifyValue))].sort()
147
- };
148
- }
149
-
150
- if (value instanceof Date) {
151
- return { kind: 'date' };
152
- }
153
- if (value instanceof RegExp) {
154
- return { kind: 'regexp', source: value.source, flags: value.flags };
155
- }
156
- if (value instanceof Map) {
157
- return { kind: 'map', size: value.size };
158
- }
159
- if (value instanceof Set) {
160
- return { kind: 'set', size: value.size };
161
- }
162
- if (isPlainObject(value)) {
163
- return { kind: 'object', keys: Object.keys(value).sort() };
164
- }
165
-
166
- return {
167
- kind: 'instance',
168
- constructor: getConstructorName(value),
169
- keys: Object.keys(value).sort()
170
- };
171
- }
172
-
173
- function normalizeBehaviorValue(value, seen = new Set()) {
174
- if (value === null || value === undefined) {
175
- return value;
176
- }
177
-
178
- const primitiveType = typeof value;
179
- if (primitiveType === 'string' || primitiveType === 'number' || primitiveType === 'boolean') {
180
- return value;
181
- }
182
- if (primitiveType === 'bigint') {
183
- return { kind: 'bigint', value: String(value) };
184
- }
185
- if (primitiveType === 'symbol') {
186
- return { kind: 'symbol', value: String(value) };
187
- }
188
- if (primitiveType === 'function') {
189
- return normalizeFunction(value);
190
- }
191
-
192
- if (seen.has(value)) {
193
- return { kind: 'circular' };
194
- }
195
- seen.add(value);
196
-
197
- try {
198
- if (isReactLikeElement(value)) {
199
- return {
200
- kind: 'element',
201
- type: normalizeElementType(value.type),
202
- key: value.key === undefined ? null : value.key,
203
- props: normalizeBehaviorValue(value.props || {}, seen)
204
- };
205
- }
206
-
207
- if (Array.isArray(value)) {
208
- return value.map((item) => normalizeBehaviorValue(item, seen));
209
- }
210
-
211
- if (value instanceof Date) {
212
- return { kind: 'date', value: value.toISOString() };
213
- }
214
- if (value instanceof RegExp) {
215
- return { kind: 'regexp', source: value.source, flags: value.flags };
216
- }
217
- if (value instanceof Map) {
218
- return {
219
- kind: 'map',
220
- entries: [...value.entries()].map(([key, entryValue]) => ([
221
- normalizeBehaviorValue(key, seen),
222
- normalizeBehaviorValue(entryValue, seen)
223
- ]))
224
- };
225
- }
226
- if (value instanceof Set) {
227
- return {
228
- kind: 'set',
229
- values: [...value.values()].map((entryValue) => normalizeBehaviorValue(entryValue, seen))
230
- };
231
- }
232
- if (isPlainObject(value)) {
233
- const normalized = {};
234
- for (const key of Object.keys(value).sort()) {
235
- normalized[key] = normalizeBehaviorValue(value[key], seen);
236
- }
237
- return normalized;
238
- }
239
-
240
- return {
241
- kind: 'instance',
242
- constructor: getConstructorName(value),
243
- keys: normalizeBehaviorValue(Object.keys(value).sort(), seen)
244
- };
245
- } finally {
246
- seen.delete(value);
247
- }
248
- }
249
-
250
- function clonePlainData(value) {
251
- if (value === undefined) {
252
- return undefined;
253
- }
254
- return JSON.parse(JSON.stringify(value));
255
- }
256
-
257
- async function normalizeRouteResult(value) {
258
- if (typeof Response !== 'undefined' && value instanceof Response) {
259
- const headers = {};
260
- for (const [key, headerValue] of value.headers.entries()) {
261
- headers[key] = headerValue;
262
- }
263
-
264
- const text = await value.clone().text();
265
- const body = tryParseJson(text);
266
-
267
- return {
268
- kind: 'response',
269
- status: value.status,
270
- statusText: value.statusText,
271
- redirected: value.redirected,
272
- headers,
273
- body: body === null ? text : body
274
- };
275
- }
276
-
277
- return normalizeBehaviorValue(value);
278
- }
279
-
280
- function createRequestFromSpec(spec) {
281
- const requestSpec = spec || {};
282
- const headers = { ...(requestSpec.headers || {}) };
283
- let body = undefined;
284
-
285
- if (requestSpec.json !== undefined) {
286
- if (!Object.prototype.hasOwnProperty.call(headers, 'content-type')) {
287
- headers['content-type'] = 'application/json';
288
- }
289
- body = JSON.stringify(requestSpec.json);
290
- } else if (requestSpec.body !== undefined) {
291
- body = requestSpec.body;
292
- }
293
-
294
- return new Request(requestSpec.url, {
295
- method: requestSpec.method || 'GET',
296
- headers,
297
- body
298
- });
299
- }
300
-
301
- function assertSourceFreshness(sourceFile, expectedHash, sourceLabel, regenerateCommand) {
302
- const currentHash = hashSourceFile(sourceFile);
303
- if (currentHash === expectedHash) {
304
- return;
305
- }
306
-
307
- throw new Error(
308
- 'Themis generated test is stale for ' + sourceLabel +
309
- '. Run: ' + regenerateCommand +
310
- '. Expected source hash ' + expectedHash +
311
- ' but found ' + currentHash + '.'
312
- );
313
- }
314
-
315
- function hashSourceFile(sourceFile) {
316
- return crypto.createHash('sha1').update(fs.readFileSync(sourceFile, 'utf8')).digest('hex');
317
- }
318
-
319
- function tryParseJson(value) {
320
- if (!value) {
321
- return null;
322
- }
323
-
324
- try {
325
- return JSON.parse(value);
326
- } catch (error) {
327
- return null;
328
- }
329
- }
330
-
331
- function normalizeElementType(type) {
332
- if (typeof type === 'string') {
333
- return type;
334
- }
335
- if (typeof type === 'symbol') {
336
- return String(type);
337
- }
338
- if (typeof type === 'function') {
339
- return type.name || '(anonymous)';
340
- }
341
- return normalizeBehaviorValue(type);
342
- }
343
-
344
- function loadModuleWithReactHarness(sourceFile, run) {
345
- const harness = { stateRecords: [], stateSlots: [], cursor: 0 };
346
-
347
- return withPatchedModule(sourceFile, 'react', (actualReact) => {
348
- const base = actualReact && (typeof actualReact === 'object' || typeof actualReact === 'function')
349
- ? actualReact
350
- : {};
351
-
352
- function beginRender() {
353
- harness.cursor = 0;
354
- }
355
-
356
- function useState(initialValue) {
357
- const slotIndex = harness.cursor;
358
- harness.cursor += 1;
359
- let slot = harness.stateSlots[slotIndex];
360
-
361
- if (!slot) {
362
- const startingValue = typeof initialValue === 'function' ? initialValue() : initialValue;
363
- slot = {
364
- currentValue: startingValue,
365
- record: {
366
- initial: normalizeBehaviorValue(startingValue),
367
- updates: []
368
- }
369
- };
370
- harness.stateSlots[slotIndex] = slot;
371
- harness.stateRecords.push(slot.record);
372
- }
373
-
374
- function setValue(nextValue) {
375
- slot.currentValue = typeof nextValue === 'function' ? nextValue(slot.currentValue) : nextValue;
376
- slot.record.updates.push(normalizeBehaviorValue(slot.currentValue));
377
- return slot.currentValue;
378
- }
379
-
380
- return [slot.currentValue, setValue];
381
- }
382
-
383
- return {
384
- ...base,
385
- useState,
386
- __THEMIS_BEGIN_RENDER__: beginRender
387
- };
388
- }, () => {
389
- const resolvedSource = require.resolve(sourceFile);
390
- delete require.cache[resolvedSource];
391
- const moduleExports = require(sourceFile);
392
- return run({
393
- moduleExports,
394
- stateRecords: harness.stateRecords,
395
- beginRender() {
396
- harness.cursor = 0;
397
- }
398
- });
399
- });
400
- }
401
-
402
- function withPatchedModule(sourceFile, request, buildExports, run) {
403
- let resolvedRequest;
404
- try {
405
- resolvedRequest = require.resolve(request, { paths: [path.dirname(sourceFile)] });
406
- } catch (error) {
407
- return run();
408
- }
409
-
410
- const previousLoad = Module._load;
411
- const hadExisting = Object.prototype.hasOwnProperty.call(require.cache, resolvedRequest);
412
- const previousCacheEntry = require.cache[resolvedRequest];
413
-
414
- delete require.cache[resolvedRequest];
415
- const actualExports = previousLoad.call(Module, resolvedRequest, null, false);
416
- const patchedExports = buildExports(actualExports);
417
- delete require.cache[resolvedRequest];
418
-
419
- Module._load = function themisPatchedModuleLoad(targetRequest, parent, isMain) {
420
- const resolvedTarget = Module._resolveFilename(targetRequest, parent, isMain);
421
- if (resolvedTarget === resolvedRequest) {
422
- return patchedExports;
423
- }
424
- return previousLoad.call(this, targetRequest, parent, isMain);
425
- };
426
-
427
- try {
428
- return run();
429
- } finally {
430
- Module._load = previousLoad;
431
- if (hadExisting) {
432
- require.cache[resolvedRequest] = previousCacheEntry;
433
- } else {
434
- delete require.cache[resolvedRequest];
435
- }
436
- }
437
- }
438
-
439
- async function runComponentInteractionContract(sourceFile, exportName, props, interactionPlan = [], options = {}) {
440
- const wrapRender = options && typeof options.wrapRender === 'function' ? options.wrapRender : null;
441
- return loadModuleWithReactHarness(sourceFile, async ({ moduleExports, stateRecords, beginRender } = {}) => {
442
- const component = readExportValue(moduleExports || require(sourceFile), exportName);
443
- function render() {
444
- beginRender();
445
- const rendered = component(props);
446
- return wrapRender ? wrapRender(rendered) : rendered;
447
- }
448
-
449
- let rendered = render();
450
- const availableInteractions = collectElementInteractions(rendered);
451
- const interactions = [];
452
-
453
- for (const interaction of resolvePlannedElementInteractions(availableInteractions, interactionPlan)) {
454
- const beforeState = normalizeBehaviorValue(stateRecords);
455
- const beforeRendered = normalizeBehaviorValue(rendered);
456
- const result = interaction.invoke();
457
- rendered = render();
458
- const immediateRendered = normalizeBehaviorValue(rendered);
459
- const immediateDom = buildDomContractFromElement(rendered);
460
- const settledResult = await settleComponentInteractionResult(result);
461
- rendered = render();
462
- interactions.push({
463
- label: interaction.label,
464
- eventName: interaction.eventName,
465
- elementType: interaction.elementType,
466
- syntheticEvent: interaction.syntheticEvent,
467
- result: normalizeBehaviorValue(settledResult),
468
- beforeState,
469
- afterState: normalizeBehaviorValue(stateRecords),
470
- beforeRendered,
471
- immediateRendered,
472
- afterRendered: normalizeBehaviorValue(rendered),
473
- beforeDom: buildDomContractFromElement(interaction.beforeRenderedElement || beforeRendered),
474
- immediateDom,
475
- afterDom: buildDomContractFromElement(rendered)
476
- });
477
- }
478
-
479
- return {
480
- rendered: normalizeBehaviorValue(rendered),
481
- dom: buildDomContractFromElement(rendered),
482
- state: normalizeBehaviorValue(stateRecords),
483
- plan: normalizeBehaviorValue(interactionPlan),
484
- interactions
485
- };
486
- });
487
- }
488
-
489
- async function runComponentBehaviorFlowContract(sourceFile, exportName, props, flowPlan = [], options = {}) {
490
- const wrapRender = options && typeof options.wrapRender === 'function' ? options.wrapRender : null;
491
- return loadModuleWithReactHarness(sourceFile, async ({ moduleExports, stateRecords, beginRender } = {}) => {
492
- const component = readExportValue(moduleExports || require(sourceFile), exportName);
493
- function render() {
494
- beginRender();
495
- const rendered = component(props);
496
- return wrapRender ? wrapRender(rendered) : rendered;
497
- }
498
-
499
- let rendered = render();
500
- const steps = [];
501
-
502
- for (const step of normalizeComponentFlowPlan(flowPlan)) {
503
- const availableInteractions = collectElementInteractions(rendered);
504
- const interaction = findMatchingElementInteraction(availableInteractions, step);
505
- if (!interaction) {
506
- steps.push({
507
- label: step.label || step.event || 'unresolved-step',
508
- expected: normalizeBehaviorValue(step.expected || {}),
509
- skipped: true,
510
- reason: 'No matching interaction was available for this flow step.',
511
- beforeDom: buildDomContractFromElement(rendered),
512
- beforeState: normalizeBehaviorValue(stateRecords)
513
- });
514
- continue;
515
- }
516
-
517
- const beforeState = normalizeBehaviorValue(stateRecords);
518
- const beforeDom = buildDomContractFromElement(rendered);
519
- const beforeRendered = normalizeBehaviorValue(rendered);
520
- const result = interaction.invoke(step.syntheticEvent);
521
- rendered = render();
522
- const immediateDom = buildDomContractFromElement(rendered);
523
- const immediateState = normalizeBehaviorValue(stateRecords);
524
- const immediateRendered = normalizeBehaviorValue(rendered);
525
- const settledResult = await settleComponentFlowStep(result, step);
526
- rendered = render();
527
- let settledDom = buildDomContractFromElement(rendered);
528
- if (!isSatisfiedFlowExpectation(step.expected, settledDom)) {
529
- const awaited = await awaitFlowExpectation(render, rendered, step);
530
- rendered = awaited.rendered;
531
- settledDom = awaited.dom;
532
- }
533
-
534
- steps.push({
535
- label: step.label || interaction.label,
536
- eventName: interaction.eventName,
537
- elementType: interaction.elementType,
538
- expected: normalizeBehaviorValue(step.expected || {}),
539
- syntheticEvent: normalizeBehaviorValue(step.syntheticEvent),
540
- returnValue: normalizeBehaviorValue(settledResult),
541
- beforeState,
542
- immediateState,
543
- settledState: normalizeBehaviorValue(stateRecords),
544
- beforeRendered,
545
- immediateRendered,
546
- settledRendered: normalizeBehaviorValue(rendered),
547
- beforeDom,
548
- immediateDom,
549
- settledDom
550
- });
551
- }
552
-
553
- return {
554
- rendered: normalizeBehaviorValue(rendered),
555
- dom: buildDomContractFromElement(rendered),
556
- state: normalizeBehaviorValue(stateRecords),
557
- plan: normalizeBehaviorValue(flowPlan),
558
- steps
559
- };
560
- });
561
- }
562
-
563
- function runHookInteractionContract(sourceFile, exportName, args, interactionPlan = []) {
564
- return loadModuleWithReactHarness(sourceFile, ({ moduleExports, stateRecords, beginRender } = {}) => {
565
- const hook = readExportValue(moduleExports || require(sourceFile), exportName);
566
- function render() {
567
- beginRender();
568
- return hook(...args);
569
- }
570
-
571
- let result = render();
572
- const availableInteractions = collectStatefulMethodInteractions(result);
573
- const interactions = resolvePlannedHookInteractions(availableInteractions, interactionPlan).map((interaction) => {
574
- const beforeState = normalizeBehaviorValue(stateRecords);
575
- const beforeResult = normalizeBehaviorValue(result);
576
- const returnValue = interaction.invoke();
577
- result = render();
578
- return {
579
- label: interaction.label,
580
- methodName: interaction.methodName,
581
- returnValue: normalizeBehaviorValue(returnValue),
582
- beforeState,
583
- afterState: normalizeBehaviorValue(stateRecords),
584
- beforeResult,
585
- afterResult: normalizeBehaviorValue(result)
586
- };
587
- });
588
-
589
- return {
590
- result: normalizeBehaviorValue(result),
591
- state: normalizeBehaviorValue(stateRecords),
592
- plan: normalizeBehaviorValue(interactionPlan),
593
- interactions
594
- };
595
- });
596
- }
597
-
598
- function resolvePlannedElementInteractions(interactions, plan) {
599
- if (!Array.isArray(interactions) || interactions.length === 0) {
600
- return [];
601
- }
602
-
603
- const steps = normalizeComponentInteractionPlan(plan);
604
- if (steps.length === 0) {
605
- return interactions.slice(0, 4);
606
- }
607
-
608
- return materializeInteractionSteps(interactions, steps, findMatchingElementInteraction);
609
- }
610
-
611
- function resolvePlannedHookInteractions(interactions, plan) {
612
- if (!Array.isArray(interactions) || interactions.length === 0) {
613
- return [];
614
- }
615
-
616
- const steps = normalizeHookInteractionPlan(plan);
617
- if (steps.length === 0) {
618
- return interactions.slice(0, 6);
619
- }
620
-
621
- return materializeInteractionSteps(interactions, steps, findMatchingHookInteraction);
622
- }
623
-
624
- function materializeInteractionSteps(interactions, steps, matcher) {
625
- const resolved = [];
626
-
627
- for (const step of steps) {
628
- const matched = matcher(interactions, step);
629
- if (!matched) {
630
- continue;
631
- }
632
-
633
- const repeat = Math.max(1, Number(step.repeat || 1));
634
- for (let count = 0; count < repeat; count += 1) {
635
- resolved.push(matched);
636
- }
637
- }
638
-
639
- return resolved;
640
- }
641
-
642
- function normalizeComponentInteractionPlan(plan) {
643
- if (!Array.isArray(plan)) {
644
- return [];
645
- }
646
-
647
- return plan
648
- .map((step) => {
649
- if (typeof step === 'string') {
650
- return { event: step };
651
- }
652
- if (!step || typeof step !== 'object') {
653
- return null;
654
- }
655
- return {
656
- event: typeof step.event === 'string' ? step.event : null,
657
- labelIncludes: typeof step.labelIncludes === 'string' ? step.labelIncludes : null,
658
- elementType: typeof step.elementType === 'string' ? step.elementType : null,
659
- repeat: Number(step.repeat || 1)
660
- };
661
- })
662
- .filter(Boolean);
663
- }
664
-
665
- function normalizeHookInteractionPlan(plan) {
666
- if (!Array.isArray(plan)) {
667
- return [];
668
- }
669
-
670
- return plan
671
- .map((step) => {
672
- if (typeof step === 'string') {
673
- return { method: step };
674
- }
675
- if (!step || typeof step !== 'object') {
676
- return null;
677
- }
678
- return {
679
- method: typeof step.method === 'string' ? step.method : null,
680
- repeat: Number(step.repeat || 1)
681
- };
682
- })
683
- .filter(Boolean);
684
- }
685
-
686
- function normalizeComponentFlowPlan(plan) {
687
- if (!Array.isArray(plan)) {
688
- return [];
689
- }
690
-
691
- return plan
692
- .map((step) => {
693
- if (!step || typeof step !== 'object') {
694
- return null;
695
- }
696
-
697
- const syntheticEvent = buildFlowSyntheticEvent(step);
698
- return {
699
- label: typeof step.label === 'string' ? step.label : null,
700
- event: typeof step.event === 'string' ? step.event : null,
701
- labelIncludes: typeof step.labelIncludes === 'string' ? step.labelIncludes : null,
702
- elementType: typeof step.elementType === 'string' ? step.elementType : null,
703
- syntheticEvent,
704
- awaitResult: step.awaitResult === true,
705
- flushMicrotasks: Number(step.flushMicrotasks || 0),
706
- advanceTimersByTime: Number(step.advanceTimersByTime || 0),
707
- runAllTimers: step.runAllTimers === true,
708
- expected: isPlainObject(step.expected) ? clonePlainData(step.expected) : {}
709
- };
710
- })
711
- .filter((step) => step && step.event);
712
- }
713
-
714
- function findMatchingElementInteraction(interactions, step) {
715
- return interactions.find((interaction) => {
716
- if (step.event && interaction.eventName !== step.event) {
717
- return false;
718
- }
719
- if (step.elementType && interaction.elementType !== step.elementType) {
720
- return false;
721
- }
722
- if (step.labelIncludes && !interaction.label.includes(step.labelIncludes)) {
723
- return false;
724
- }
725
- return true;
726
- }) || null;
727
- }
728
-
729
- function findMatchingHookInteraction(interactions, step) {
730
- return interactions.find((interaction) => interaction.methodName === step.method) || null;
731
- }
732
-
733
- async function settleComponentFlowStep(result, step) {
734
- let settledResult = result;
735
-
736
- if (step.awaitResult && settledResult && typeof settledResult.then === 'function') {
737
- settledResult = await settledResult;
738
- }
739
-
740
- const flushCount = Math.max(0, Number(step.flushMicrotasks || 0));
741
- for (let index = 0; index < flushCount; index += 1) {
742
- await Promise.resolve();
743
- }
744
-
745
- if (step.runAllTimers && typeof globalThis.runAllTimers === 'function') {
746
- globalThis.runAllTimers();
747
- }
748
-
749
- if (Number(step.advanceTimersByTime || 0) > 0 && typeof globalThis.advanceTimersByTime === 'function') {
750
- globalThis.advanceTimersByTime(Number(step.advanceTimersByTime));
751
- }
752
-
753
- if (flushCount > 0) {
754
- for (let index = 0; index < flushCount; index += 1) {
755
- await Promise.resolve();
756
- }
757
- }
758
-
759
- return settledResult;
760
- }
761
-
762
- async function settleComponentInteractionResult(result) {
763
- let settledResult = result;
764
-
765
- if (settledResult && typeof settledResult.then === 'function') {
766
- settledResult = await settledResult;
767
- }
768
-
769
- await flushFlowMicrotasks({ flushMicrotasks: 1 });
770
- return settledResult;
771
- }
772
-
773
- async function awaitFlowExpectation(render, rendered, step, maxAttempts = 4) {
774
- let currentRendered = rendered;
775
- let currentDom = buildDomContractFromElement(currentRendered);
776
-
777
- for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
778
- if (isSatisfiedFlowExpectation(step.expected, currentDom)) {
779
- break;
780
- }
781
-
782
- await flushFlowMicrotasks(step);
783
- currentRendered = render();
784
- currentDom = buildDomContractFromElement(currentRendered);
785
- }
786
-
787
- return {
788
- rendered: currentRendered,
789
- dom: currentDom
790
- };
791
- }
792
-
793
- function isSatisfiedFlowExpectation(expected, dom) {
794
- if (!expected || typeof expected !== 'object') {
795
- return true;
796
- }
797
-
798
- if (typeof expected.beforeTextIncludes === 'string' && !String(dom && dom.textContent || '').includes(expected.beforeTextIncludes)) {
799
- return false;
800
- }
801
-
802
- if (typeof expected.settledTextIncludes === 'string' && !String(dom && dom.textContent || '').includes(expected.settledTextIncludes)) {
803
- return false;
804
- }
805
-
806
- if (typeof expected.textExcludes === 'string' && String(dom && dom.textContent || '').includes(expected.textExcludes)) {
807
- return false;
808
- }
809
-
810
- if (!domContractMatchesAttributes(dom, expected.attributes)) {
811
- return false;
812
- }
813
-
814
- if (!domContractMatchesRoles(dom, expected.rolesInclude)) {
815
- return false;
816
- }
817
-
818
- return true;
819
- }
820
-
821
- function domContractMatchesAttributes(dom, expectedAttributes) {
822
- if (!isPlainObject(expectedAttributes) || Object.keys(expectedAttributes).length === 0) {
823
- return true;
824
- }
825
-
826
- const nodes = dom && Array.isArray(dom.nodes) ? dom.nodes : [];
827
- return nodes.some((node) => {
828
- if (!node || node.kind !== 'element' || !isPlainObject(node.attributes)) {
829
- return false;
830
- }
831
-
832
- return Object.entries(expectedAttributes).every(([key, value]) => {
833
- const actual = Object.prototype.hasOwnProperty.call(node.attributes, key) ? node.attributes[key] : undefined;
834
- return valuesDeepEqual(normalizeBehaviorValue(actual), normalizeBehaviorValue(value));
835
- });
836
- });
837
- }
838
-
839
- function valuesDeepEqual(left, right) {
840
- return JSON.stringify(left) === JSON.stringify(right);
841
- }
842
-
843
- function domContractMatchesRoles(dom, rolesInclude) {
844
- if (rolesInclude === undefined || rolesInclude === null) {
845
- return true;
846
- }
847
-
848
- const expectedRoles = Array.isArray(rolesInclude) ? rolesInclude : [rolesInclude];
849
- if (expectedRoles.length === 0) {
850
- return true;
851
- }
852
-
853
- const roles = dom && Array.isArray(dom.roles) ? dom.roles : [];
854
- return expectedRoles.every((expectedRole) => roles.some((entry) => entry && entry.role === expectedRole));
855
- }
856
-
857
- async function flushFlowMicrotasks(step) {
858
- if (typeof globalThis.flushMicrotasks === 'function') {
859
- await globalThis.flushMicrotasks();
860
- } else {
861
- await Promise.resolve();
862
- await Promise.resolve();
863
- }
864
-
865
- const flushCount = Math.max(0, Number(step && step.flushMicrotasks || 0));
866
- for (let index = 0; index < flushCount; index += 1) {
867
- await Promise.resolve();
868
- }
869
- }
870
-
871
- function collectElementInteractions(node, ancestry = []) {
872
- const interactions = [];
873
- visitElementInteractions(node, ancestry, interactions);
874
- return interactions;
875
- }
876
-
877
- function buildFlowSyntheticEvent(step) {
878
- const payload = isPlainObject(step.target) ? clonePlainData(step.target) : {};
879
- const type = typeof step.event === 'string' ? step.event.replace(/^on/, '').toLowerCase() : 'event';
880
- return {
881
- type,
882
- defaultPrevented: false,
883
- preventDefault() {
884
- this.defaultPrevented = true;
885
- },
886
- target: payload,
887
- currentTarget: {
888
- type: typeof step.elementType === 'string' ? step.elementType : null,
889
- props: {}
890
- }
891
- };
892
- }
893
-
894
- function visitElementInteractions(node, ancestry, interactions) {
895
- if (!isReactLikeElement(node)) {
896
- return;
897
- }
898
-
899
- const elementType = normalizeElementType(node.type);
900
- const currentPath = ancestry.concat([elementType]);
901
- const props = node.props || {};
902
-
903
- for (const eventName of ${JSON.stringify(INTERACTION_EVENT_PRIORITY)}) {
904
- if (typeof props[eventName] !== 'function') {
905
- continue;
906
- }
907
-
908
- const syntheticEvent = buildSyntheticEvent(eventName, node);
909
- interactions.push({
910
- label: currentPath.join(' > ') + '.' + eventName,
911
- eventName,
912
- elementType,
913
- syntheticEvent: normalizeBehaviorValue(syntheticEvent),
914
- beforeRenderedElement: node,
915
- invoke(overrideEvent) {
916
- const eventValue = overrideEvent || syntheticEvent;
917
- return props[eventName](eventValue);
918
- }
919
- });
920
- }
921
-
922
- for (const child of flattenChildren(props.children)) {
923
- visitElementInteractions(child, currentPath, interactions);
924
- }
925
- }
926
-
927
- function buildDomContractFromElement(node) {
928
- const contract = {
929
- textContent: collectElementText(node),
930
- roles: [],
931
- nodes: []
932
- };
933
-
934
- visitDomContract(node, contract, []);
935
- return contract;
936
- }
937
-
938
- function visitDomContract(node, contract, ancestry) {
939
- if (node === null || node === undefined || typeof node === 'boolean') {
940
- return;
941
- }
942
-
943
- if (typeof node === 'string' || typeof node === 'number') {
944
- contract.nodes.push({
945
- kind: 'text',
946
- value: String(node),
947
- path: ancestry.join(' > ')
948
- });
949
- return;
950
- }
951
-
952
- if (Array.isArray(node)) {
953
- for (const child of node) {
954
- visitDomContract(child, contract, ancestry);
955
- }
956
- return;
957
- }
958
-
959
- if (!isReactLikeElement(node)) {
960
- contract.nodes.push({
961
- kind: 'value',
962
- value: normalizeBehaviorValue(node),
963
- path: ancestry.join(' > ')
964
- });
965
- return;
966
- }
967
-
968
- const elementType = normalizeElementType(node.type);
969
- const props = node.props || {};
970
- const currentPath = ancestry.concat([elementType]);
971
- const attributes = collectDomAttributes(props);
972
- const role = inferElementRole(elementType, props);
973
- const textContent = collectElementText(props.children);
974
-
975
- contract.nodes.push({
976
- kind: 'element',
977
- type: elementType,
978
- path: currentPath.join(' > '),
979
- textContent,
980
- attributes
981
- });
982
-
983
- if (role) {
984
- contract.roles.push({
985
- role,
986
- name: props['aria-label'] || textContent,
987
- path: currentPath.join(' > '),
988
- type: elementType,
989
- attributes
990
- });
991
- }
992
-
993
- for (const child of flattenChildren(props.children)) {
994
- visitDomContract(child, contract, currentPath);
995
- }
996
- }
997
-
998
- function collectElementText(node) {
999
- if (node === null || node === undefined || typeof node === 'boolean') {
1000
- return '';
1001
- }
1002
- if (typeof node === 'string' || typeof node === 'number') {
1003
- return String(node);
1004
- }
1005
- if (Array.isArray(node)) {
1006
- return node.map((child) => collectElementText(child)).join('');
1007
- }
1008
- if (!isReactLikeElement(node)) {
1009
- return '';
1010
- }
1011
- return collectElementText((node.props || {}).children);
1012
- }
1013
-
1014
- function collectDomAttributes(props) {
1015
- const attributes = {};
1016
- for (const [key, value] of Object.entries(props || {})) {
1017
- if (key === 'children' || key.startsWith('on') || value === undefined || value === null) {
1018
- continue;
1019
- }
1020
- attributes[key] = normalizeBehaviorValue(value);
1021
- }
1022
- return attributes;
1023
- }
1024
-
1025
- function inferElementRole(type, props) {
1026
- if (props && typeof props.role === 'string') {
1027
- return props.role;
1028
- }
1029
- if (type === 'button') {
1030
- return 'button';
1031
- }
1032
- if (type === 'form') {
1033
- return 'form';
1034
- }
1035
- if (type === 'textarea') {
1036
- return 'textbox';
1037
- }
1038
- if (type === 'input') {
1039
- const inputType = props && typeof props.type === 'string' ? props.type.toLowerCase() : 'text';
1040
- if (inputType === 'checkbox') {
1041
- return 'checkbox';
1042
- }
1043
- if (inputType === 'radio') {
1044
- return 'radio';
1045
- }
1046
- return 'textbox';
1047
- }
1048
- if (type === 'a' && props && props.href) {
1049
- return 'link';
1050
- }
1051
- return null;
1052
- }
1053
-
1054
- function flattenChildren(children) {
1055
- if (children === null || children === undefined) {
1056
- return [];
1057
- }
1058
- if (Array.isArray(children)) {
1059
- const flattened = [];
1060
- for (const child of children) {
1061
- flattened.push(...flattenChildren(child));
1062
- }
1063
- return flattened;
1064
- }
1065
- return [children];
1066
- }
1067
-
1068
- function buildSyntheticEvent(eventName, element) {
1069
- const props = element && element.props ? element.props : {};
1070
- const target = {};
1071
-
1072
- if (eventName === 'onChange' || eventName === 'onInput') {
1073
- if (Object.prototype.hasOwnProperty.call(props, 'checked')) {
1074
- target.checked = !Boolean(props.checked);
1075
- }
1076
- if (Object.prototype.hasOwnProperty.call(props, 'value')) {
1077
- target.value = props.value;
1078
- } else if (!Object.prototype.hasOwnProperty.call(target, 'checked')) {
1079
- target.value = 'themis';
1080
- }
1081
- }
1082
-
1083
- const event = {
1084
- type: eventName.slice(2).toLowerCase(),
1085
- defaultPrevented: false,
1086
- preventDefault() {
1087
- this.defaultPrevented = true;
1088
- },
1089
- target,
1090
- currentTarget: {
1091
- type: normalizeElementType(element && element.type),
1092
- props: normalizeBehaviorValue(props)
1093
- }
1094
- };
1095
-
1096
- return event;
1097
- }
1098
-
1099
- function collectStatefulMethodInteractions(value) {
1100
- if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
1101
- return [];
1102
- }
1103
-
1104
- const methods = Object.keys(value)
1105
- .filter((key) => typeof value[key] === 'function' && value[key].length === 0)
1106
- .sort((left, right) => {
1107
- const leftIndex = ${JSON.stringify(STATEFUL_METHOD_PRIORITY)}.indexOf(left);
1108
- const rightIndex = ${JSON.stringify(STATEFUL_METHOD_PRIORITY)}.indexOf(right);
1109
- if (leftIndex !== -1 || rightIndex !== -1) {
1110
- const safeLeft = leftIndex === -1 ? ${JSON.stringify(STATEFUL_METHOD_PRIORITY)}.length : leftIndex;
1111
- const safeRight = rightIndex === -1 ? ${JSON.stringify(STATEFUL_METHOD_PRIORITY)}.length : rightIndex;
1112
- if (safeLeft !== safeRight) {
1113
- return safeLeft - safeRight;
1114
- }
1115
- }
1116
- return left.localeCompare(right);
1117
- });
1118
-
1119
- return methods.map((methodName) => ({
1120
- label: methodName,
1121
- methodName,
1122
- invoke() {
1123
- return value[methodName]();
1124
- }
1125
- }));
1126
- }
1127
-
1128
- function normalizeFunction(value) {
1129
- const ownKeys = Object.keys(value).sort();
1130
- const prototypeKeys = value.prototype && value.prototype !== Object.prototype
1131
- ? Object.getOwnPropertyNames(value.prototype).filter((key) => key !== 'constructor').sort()
1132
- : [];
1133
-
1134
- return {
1135
- kind: isClassLike(value) ? 'class' : 'function',
1136
- name: value.name || '(anonymous)',
1137
- arity: value.length,
1138
- ownKeys,
1139
- prototypeKeys
1140
- };
1141
- }
1142
-
1143
- function classifyValue(value) {
1144
- if (value === null) {
1145
- return 'null';
1146
- }
1147
- if (Array.isArray(value)) {
1148
- return 'array';
1149
- }
1150
- if (value instanceof Date) {
1151
- return 'date';
1152
- }
1153
- if (value instanceof RegExp) {
1154
- return 'regexp';
1155
- }
1156
- if (value instanceof Map) {
1157
- return 'map';
1158
- }
1159
- if (value instanceof Set) {
1160
- return 'set';
1161
- }
1162
-
1163
- const type = typeof value;
1164
- if (type !== 'object') {
1165
- return type;
1166
- }
1167
-
1168
- if (isPlainObject(value)) {
1169
- return 'object';
1170
- }
1171
-
1172
- return 'instance:' + getConstructorName(value);
1173
- }
1174
-
1175
- function getConstructorName(value) {
1176
- if (!value || !value.constructor || !value.constructor.name) {
1177
- return 'Object';
1178
- }
1179
- return value.constructor.name;
1180
- }
1181
-
1182
- function isPlainObject(value) {
1183
- if (!value || typeof value !== 'object') {
1184
- return false;
1185
- }
1186
-
1187
- const prototype = Object.getPrototypeOf(value);
1188
- return prototype === Object.prototype || prototype === null;
1189
- }
1190
-
1191
- function isClassLike(value) {
1192
- return Function.prototype.toString.call(value).startsWith('class ');
1193
- }
1194
-
1195
- function isReactLikeElement(value) {
1196
- return value && typeof value === 'object' && value.$$typeof === 'react.test.element';
1197
- }
1198
-
1199
- module.exports = {
1200
- listExportNames,
1201
- buildModuleContract,
1202
- readExportValue,
1203
- normalizeBehaviorValue,
1204
- normalizeRouteResult,
1205
- createRequestFromSpec,
1206
- assertSourceFreshness,
1207
- runComponentInteractionContract,
1208
- runComponentBehaviorFlowContract,
1209
- runHookInteractionContract
1210
- };
1211
- `;
1212
-
44
+ const GENERATED_HELPER_MODULE = '@vitronai/themis/contract-runtime';
1213
45
  function generateTestsFromSource(cwd, options = {}) {
1214
46
  const projectRoot = path.resolve(cwd || process.cwd());
1215
47
  const normalizedOptions = normalizeGenerateOptions(projectRoot, options);
1216
48
  const projectProviders = loadProjectProviders(projectRoot);
1217
49
  const scanTarget = resolveScanTarget(projectRoot, normalizedOptions.targetDir || 'src');
1218
50
  const outputDir = path.resolve(projectRoot, normalizedOptions.outputDir || path.join('tests', 'generated'));
1219
- const helperFile = path.join(outputDir, GENERATED_HELPER_NAME);
51
+ const helperFile = GENERATED_HELPER_MODULE;
52
+ const legacyHelperFile = path.join(outputDir, GENERATED_HELPER_NAME);
1220
53
  const mapFile = path.resolve(projectRoot, GENERATED_MAP_ARTIFACT);
1221
54
  const resultArtifactFile = path.resolve(projectRoot, GENERATED_RESULT_ARTIFACT);
1222
55
  const handoffArtifactFile = path.resolve(projectRoot, GENERATED_HANDOFF_ARTIFACT);
@@ -1440,23 +273,16 @@ function generateTestsFromSource(cwd, options = {}) {
1440
273
  }
1441
274
 
1442
275
  let removedFiles = [];
1443
- let helperRemoved = false;
1444
-
1445
276
  if (!normalizedOptions.review) {
1446
277
  const expectedManagedFiles = new Set();
1447
278
 
1448
- if (mergedMapEntries.length > 0 && !normalizedOptions.clean) {
1449
- fs.mkdirSync(outputDir, { recursive: true });
1450
- writeManagedFile(helperFile, GENERATED_HELPER_SOURCE, { force: normalizedOptions.force });
1451
- expectedManagedFiles.add(safeRealpath(helperFile));
1452
- }
1453
-
1454
279
  for (const plan of plans) {
1455
280
  if (plan.action === 'conflict') {
1456
281
  continue;
1457
282
  }
1458
283
 
1459
284
  if (plan.action === 'create' || plan.action === 'update') {
285
+ fs.mkdirSync(outputDir, { recursive: true });
1460
286
  writeManagedFile(plan.outputFile, plan.content, { force: normalizedOptions.force });
1461
287
  }
1462
288
  }
@@ -1472,7 +298,6 @@ function generateTestsFromSource(cwd, options = {}) {
1472
298
  writeGenerateMap(mapFile, projectRoot, scanTarget, outputDir, mergedMapEntries);
1473
299
  } else {
1474
300
  removeGenerateMap(mapFile);
1475
- helperRemoved = removeManagedHelper(helperFile, outputDir);
1476
301
  }
1477
302
  }
1478
303
 
@@ -1527,7 +352,7 @@ function generateTestsFromSource(cwd, options = {}) {
1527
352
  gates: null,
1528
353
  backlog: null,
1529
354
  prompt: '',
1530
- helperRemoved
355
+ helperRemoved: removedFiles.includes(legacyHelperFile)
1531
356
  };
1532
357
 
1533
358
  summary.gates = buildGenerateGateSummary(summary);
@@ -1602,7 +427,7 @@ function buildGeneratePayload(summary, cwd) {
1602
427
  backlog: formatGenerateBacklog(summary.backlog, projectRoot),
1603
428
  artifacts: {
1604
429
  generateMap: formatPathForDisplay(projectRoot, summary.artifacts.generateMap),
1605
- helperFile: formatPathForDisplay(projectRoot, summary.artifacts.helperFile),
430
+ helperFile: formatArtifactReference(projectRoot, summary.artifacts.helperFile),
1606
431
  generateResult: formatPathForDisplay(projectRoot, summary.artifacts.generateResult),
1607
432
  generateHandoff: formatPathForDisplay(projectRoot, summary.artifacts.generateHandoff),
1608
433
  generateBacklog: formatPathForDisplay(projectRoot, summary.artifacts.generateBacklog)
@@ -1712,7 +537,7 @@ function compileOptionalRegex(value, label) {
1712
537
 
1713
538
  try {
1714
539
  return new RegExp(value);
1715
- } catch (error) {
540
+ } catch {
1716
541
  throw new Error(`Invalid ${label} regex: ${value}`);
1717
542
  }
1718
543
  }
@@ -1860,7 +685,7 @@ function collectChangedPaths(projectRoot) {
1860
685
  ['status', '--short', '--untracked-files=all'],
1861
686
  { cwd: projectRoot, encoding: 'utf8' }
1862
687
  );
1863
- } catch (error) {
688
+ } catch {
1864
689
  throw new Error('--changed requires a git worktree.');
1865
690
  }
1866
691
 
@@ -3366,7 +2191,7 @@ function isScaffoldHintPayload(hints) {
3366
2191
  );
3367
2192
  }
3368
2193
 
3369
- function planHintScaffold(analysis, projectRoot) {
2194
+ function planHintScaffold(analysis, _projectRoot) {
3370
2195
  const hintFile = resolveHintFilePath(analysis.file);
3371
2196
  const scaffold = buildHintScaffold(analysis);
3372
2197
  const existingHints = analysis.sidecarHints;
@@ -4009,7 +2834,7 @@ function removeGenerateMap(mapFile) {
4009
2834
  }
4010
2835
  }
4011
2836
 
4012
- function buildSummaryEntries({ plans, removedPlans, projectRoot, review }) {
2837
+ function buildSummaryEntries({ plans, removedPlans, projectRoot: _projectRoot, review }) {
4013
2838
  const entries = [];
4014
2839
 
4015
2840
  for (const plan of plans) {
@@ -4389,12 +3214,20 @@ function resolveScriptKind(filePath) {
4389
3214
  function resolveGeneratedTestPath(outputDir, mirrorBase, sourceFile) {
4390
3215
  const relativeSource = path.relative(mirrorBase, sourceFile);
4391
3216
  const parsed = path.parse(relativeSource);
4392
- return path.join(outputDir, parsed.dir, `${parsed.name}.generated.test.js`);
3217
+ return path.join(outputDir, parsed.dir, `${parsed.name}.generated.test${resolveGeneratedTestExtension(sourceFile)}`);
3218
+ }
3219
+
3220
+ function resolveGeneratedTestExtension(sourceFile) {
3221
+ const extension = path.extname(sourceFile).toLowerCase();
3222
+ if (extension === '.ts' || extension === '.tsx') {
3223
+ return '.ts';
3224
+ }
3225
+ return '.js';
4393
3226
  }
4394
3227
 
4395
3228
  function renderGeneratedTest({ projectRoot, helperFile, outputFile, analysis }) {
4396
3229
  const relativeSourcePath = normalizePath(path.relative(projectRoot, analysis.file));
4397
- const helperImport = normalizeRelativeModule(path.relative(path.dirname(outputFile), helperFile));
3230
+ const helperImport = helperFile;
4398
3231
  const sourceImport = normalizeRelativeModule(path.relative(path.dirname(outputFile), analysis.file));
4399
3232
  const sourceAbsolutePath = normalizePath(path.relative(path.dirname(outputFile), analysis.file));
4400
3233
  const expectedExportContracts = buildExpectedExportContracts(analysis);
@@ -5106,7 +3939,7 @@ function cleanupStaleGeneratedFiles(outputDir, expectedManagedFiles, explicitRem
5106
3939
  fs.rmSync(filePath, { force: true });
5107
3940
  removedFiles.push(filePath);
5108
3941
 
5109
- if (filePath.endsWith('.generated.test.js')) {
3942
+ if (/\.(generated\.test)\.(js|ts)$/.test(filePath)) {
5110
3943
  removeSnapshotFile(filePath);
5111
3944
  }
5112
3945
 
@@ -5142,21 +3975,6 @@ function removeSnapshotFile(testFilePath) {
5142
3975
  }
5143
3976
  }
5144
3977
 
5145
- function removeManagedHelper(helperFile, outputDir) {
5146
- if (!fs.existsSync(helperFile)) {
5147
- return false;
5148
- }
5149
-
5150
- const existing = fs.readFileSync(helperFile, 'utf8');
5151
- if (!existing.startsWith(GENERATED_MARKER)) {
5152
- return false;
5153
- }
5154
-
5155
- fs.rmSync(helperFile, { force: true });
5156
- pruneEmptyDirectories(path.dirname(helperFile), outputDir);
5157
- return true;
5158
- }
5159
-
5160
3978
  function pruneEmptyDirectories(startDir, stopDir) {
5161
3979
  let currentDir = startDir;
5162
3980
  while (isSubPath(currentDir, stopDir) || currentDir === stopDir) {
@@ -5288,7 +4106,7 @@ function isSameOrSubPath(targetPath, parentPath) {
5288
4106
  function safeRealpath(targetPath) {
5289
4107
  try {
5290
4108
  return fs.realpathSync.native(targetPath);
5291
- } catch (error) {
4109
+ } catch {
5292
4110
  return path.resolve(targetPath);
5293
4111
  }
5294
4112
  }
@@ -5298,6 +4116,16 @@ function formatPathForDisplay(projectRoot, targetPath) {
5298
4116
  return relative && !relative.startsWith('..') ? normalizePath(relative) : targetPath;
5299
4117
  }
5300
4118
 
4119
+ function formatArtifactReference(projectRoot, targetPath) {
4120
+ if (typeof targetPath !== 'string') {
4121
+ return targetPath;
4122
+ }
4123
+ if (path.isAbsolute(targetPath)) {
4124
+ return formatPathForDisplay(projectRoot, targetPath);
4125
+ }
4126
+ return normalizePath(targetPath);
4127
+ }
4128
+
5301
4129
  module.exports = {
5302
4130
  generateTestsFromSource,
5303
4131
  buildGeneratePayload,