@kiwa-test/nextjs 1.0.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,11 +65,63 @@ The returned `ServerActionResult` exposes `result` (the resolved value), `error`
65
65
 
66
66
  Throw a `{ [REDIRECT_SYMBOL]: true, url, type }` from your action to signal a redirect. The helper normalizes it into `env.redirect` instead of leaking it as an error. Production code keeps using `redirect()` from `next/navigation` — only the test seam differs.
67
67
 
68
+ ## RSC streaming + Suspense boundary (v1.1+, Issue #558)
69
+
70
+ `setupNextRscEnv` extends the RSC seam to streaming chunks and Suspense boundary transitions — the cases `renderServerComponent` (leaf-level + signal capture) does not model.
71
+
72
+ ```ts
73
+ import { describe, expect, it } from 'vitest';
74
+ import { setupNextRscEnv } from '@kiwa-test/nextjs';
75
+
76
+ async function* streamItems() {
77
+ yield { type: 'div', key: null, props: { children: 'partial: 1 item' } };
78
+ yield { type: 'div', key: null, props: { children: 'partial: 2 items' } };
79
+ yield { type: 'ul', key: null, props: { children: ['a', 'b', 'c'] } };
80
+ }
81
+
82
+ const fallback = { type: 'div', key: null, props: { children: 'loading…' } };
83
+
84
+ describe('streamItems', () => {
85
+ it('captures fallback then resolved subtree in order', async () => {
86
+ const env = await setupNextRscEnv({
87
+ dataSource: streamItems(),
88
+ suspenseFallback: fallback,
89
+ streamingTimeout: 1000,
90
+ });
91
+ expect(env.fallback).toBe(fallback);
92
+ expect(env.chunks).toHaveLength(4); // fallback + 3 yields
93
+ expect(env.chunks[0]).toBe(fallback);
94
+ expect(env.resolved).not.toBeNull();
95
+ expect(env.errorBoundary).toBeNull();
96
+ expect(env.timedOut).toBe(false);
97
+ });
98
+
99
+ it('routes a thrown chunk into errorBoundary', async () => {
100
+ async function* broken() {
101
+ yield fallback;
102
+ throw new Error('stream broken');
103
+ }
104
+ const env = await setupNextRscEnv({ dataSource: broken() });
105
+ expect(env.errorBoundary).not.toBeNull();
106
+ expect((env.errorBoundary?.error as Error).message).toBe('stream broken');
107
+ expect(env.resolved).toBeNull();
108
+ });
109
+ });
110
+ ```
111
+
112
+ | `env` field | Type | Meaning |
113
+ |---|---|---|
114
+ | `chunks` | `RscNode[]` | All chunks in arrival order. `chunks[0]` is the Suspense fallback when one is provided. |
115
+ | `fallback` | `RscNode \| null` | The fallback markup the helper captured before streaming started. |
116
+ | `resolved` | `RscNode \| null` | The last chunk yielded by the source — what a real page settles on. `null` when the source threw or only the fallback was emitted. |
117
+ | `errorBoundary` | `RscErrorBoundarySignal \| null` | Set when component / stream throws or `injectError` is provided. Mirrors what `error.tsx` would see. |
118
+ | `timedOut` | `boolean` | `true` when `streamingTimeout` elapsed before the source completed. |
119
+
68
120
  ## Out of scope (tracked separately)
69
121
 
70
- - **React Server Components (RSC) render assertions** — [#494](https://github.com/cardene777/kiwa/issues/494)
71
- - **`middleware.ts` invocation** — [#495](https://github.com/cardene777/kiwa/issues/495)
72
- - **End-to-end browser flow after the action** — use `/kiwa-e2e` or `/kiwa-play` instead
122
+ - **Real React `renderToReadableStream` rendering / flight payload byte format** — leaf-level coverage lives in `renderServerComponent`, full wire protocol stays out of scope.
123
+ - **Multiple concurrent Suspense boundaries interleaving** — one boundary per `setupNextRscEnv` call.
124
+ - **End-to-end browser flow after the action** — use `/kiwa-e2e` or `/kiwa-play` instead.
73
125
 
74
126
  ## License
75
127
 
package/dist/index.cjs CHANGED
@@ -22,16 +22,44 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  FORBIDDEN_SYMBOL: () => FORBIDDEN_SYMBOL,
24
24
  MIDDLEWARE_ACTION_SYMBOL: () => MIDDLEWARE_ACTION_SYMBOL,
25
+ NEXT_AXIS_TO_EVENTS: () => NEXT_AXIS_TO_EVENTS,
25
26
  NOT_FOUND_SYMBOL: () => NOT_FOUND_SYMBOL,
26
27
  PARALLEL_INTERCEPTION_SYMBOL: () => PARALLEL_INTERCEPTION_SYMBOL,
27
28
  REDIRECT_SYMBOL: () => REDIRECT_SYMBOL,
29
+ RSC_ERROR_BOUNDARY_SYMBOL: () => RSC_ERROR_BOUNDARY_SYMBOL,
28
30
  RSC_REDIRECT_SYMBOL: () => RSC_REDIRECT_SYMBOL,
31
+ assertMode: () => assertMode,
32
+ captureParallelError: () => captureParallelError,
33
+ collectFidelityCoverage: () => collectFidelityCoverage,
34
+ completePartialPrerendering: () => completePartialPrerendering,
29
35
  findAll: () => findAll,
36
+ flushStreamingBoundary: () => flushStreamingBoundary,
37
+ interceptCurrentSegment: () => interceptCurrentSegment,
38
+ interceptParentSegment: () => interceptParentSegment,
39
+ interceptRootCatchall: () => interceptRootCatchall,
30
40
  invokeMiddleware: () => invokeMiddleware,
31
41
  invokeParallelRoutes: () => invokeParallelRoutes,
32
42
  invokeServerAction: () => invokeServerAction,
33
43
  middlewareActions: () => middlewareActions,
44
+ navigateSlot: () => navigateSlot,
45
+ openDynamicHole: () => openDynamicHole,
46
+ openInterceptedModal: () => openInterceptedModal,
47
+ providerEventName: () => providerEventName,
48
+ redirectAction: () => redirectAction,
49
+ renderDefaultSlot: () => renderDefaultSlot,
50
+ renderLoadingState: () => renderLoadingState,
34
51
  renderServerComponent: () => renderServerComponent,
52
+ renderStaticShell: () => renderStaticShell,
53
+ resolveAllModes: () => resolveAllModes,
54
+ resolveMode: () => resolveMode,
55
+ revalidateActionPath: () => revalidateActionPath,
56
+ revalidateActionTag: () => revalidateActionTag,
57
+ setupNextRscEnv: () => setupNextRscEnv,
58
+ startInterceptionRoutes: () => startInterceptionRoutes,
59
+ startParallelRoutesAdvanced: () => startParallelRoutesAdvanced,
60
+ startPartialPrerendering: () => startPartialPrerendering,
61
+ startServerActionAdvanced: () => startServerActionAdvanced,
62
+ submitFormAction: () => submitFormAction,
35
63
  textContent: () => textContent
36
64
  });
37
65
  module.exports = __toCommonJS(index_exports);
@@ -279,20 +307,561 @@ async function invokeParallelRoutes(opts) {
279
307
  }
280
308
  return { tree, slotResults, childrenError, layoutError };
281
309
  }
310
+
311
+ // src/setup-next-rsc-env.ts
312
+ var RSC_ERROR_BOUNDARY_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.rsc.errorBoundary");
313
+ var DEFAULT_STREAMING_TIMEOUT_MS = 5e3;
314
+ function buildErrorBoundary(error) {
315
+ return { [RSC_ERROR_BOUNDARY_SYMBOL]: true, error };
316
+ }
317
+ async function runWithTimeout(promise, timeoutMs) {
318
+ let timer;
319
+ const timeout = new Promise((resolve) => {
320
+ timer = setTimeout(() => resolve({ value: null, timedOut: true }), timeoutMs);
321
+ });
322
+ try {
323
+ const value = await Promise.race([
324
+ promise.then((v) => ({ value: v, timedOut: false })),
325
+ timeout
326
+ ]);
327
+ return value;
328
+ } finally {
329
+ if (timer) clearTimeout(timer);
330
+ }
331
+ }
332
+ async function collectStream(source, fallback) {
333
+ const chunks = [];
334
+ if (typeof fallback !== "undefined" && fallback !== null) {
335
+ chunks.push(fallback);
336
+ }
337
+ try {
338
+ for await (const chunk of source) {
339
+ chunks.push(chunk);
340
+ }
341
+ } catch (err) {
342
+ return { chunks, resolved: null, thrown: err };
343
+ }
344
+ const resolved = chunks.length > 0 ? chunks[chunks.length - 1] ?? null : null;
345
+ const fallbackOnly = chunks.length === 1 && fallback !== null && Object.is(chunks[0], fallback);
346
+ return { chunks, resolved: fallbackOnly ? null : resolved, thrown: void 0 };
347
+ }
348
+ async function* singleChunkSource(component, props) {
349
+ const result = await component(props);
350
+ yield result;
351
+ }
352
+ async function setupNextRscEnv(opts = {}) {
353
+ const timeoutMs = opts.streamingTimeout ?? DEFAULT_STREAMING_TIMEOUT_MS;
354
+ const fallback = opts.suspenseFallback ?? null;
355
+ if (timeoutMs <= 0 && (opts.dataSource || opts.component)) {
356
+ return {
357
+ chunks: fallback !== null ? [fallback] : [],
358
+ fallback,
359
+ resolved: null,
360
+ errorBoundary: null,
361
+ timedOut: true
362
+ };
363
+ }
364
+ if (typeof opts.injectError !== "undefined") {
365
+ return {
366
+ chunks: fallback !== null ? [fallback] : [],
367
+ fallback,
368
+ resolved: null,
369
+ errorBoundary: buildErrorBoundary(opts.injectError),
370
+ timedOut: false
371
+ };
372
+ }
373
+ if (!opts.dataSource && !opts.component) {
374
+ return {
375
+ chunks: fallback !== null ? [fallback] : [],
376
+ fallback,
377
+ resolved: null,
378
+ errorBoundary: null,
379
+ timedOut: false
380
+ };
381
+ }
382
+ const source = opts.dataSource ? opts.dataSource : singleChunkSource(opts.component, opts.props ?? {});
383
+ const { value, timedOut } = await runWithTimeout(collectStream(source, fallback), timeoutMs);
384
+ if (timedOut || value === null) {
385
+ return {
386
+ chunks: fallback !== null ? [fallback] : [],
387
+ fallback,
388
+ resolved: null,
389
+ errorBoundary: null,
390
+ timedOut: true
391
+ };
392
+ }
393
+ const { chunks, resolved, thrown } = value;
394
+ if (typeof thrown !== "undefined") {
395
+ return {
396
+ chunks,
397
+ fallback,
398
+ resolved: null,
399
+ errorBoundary: buildErrorBoundary(thrown),
400
+ timedOut: false
401
+ };
402
+ }
403
+ return {
404
+ chunks,
405
+ fallback,
406
+ resolved,
407
+ errorBoundary: null,
408
+ timedOut: false
409
+ };
410
+ }
411
+
412
+ // src/semantics/types.ts
413
+ var dialect = {
414
+ "app-router": {
415
+ "action.form_submitted": "app.server-action.form.submit",
416
+ "action.revalidate_path": "app.cache.revalidatePath",
417
+ "action.revalidate_tag": "app.cache.revalidateTag",
418
+ "action.redirected": "app.navigation.redirect",
419
+ "ppr.static_shell_rendered": "app.ppr.static-shell",
420
+ "ppr.dynamic_hole_opened": "app.ppr.dynamic-hole",
421
+ "ppr.streaming_boundary_flushed": "app.ppr.stream-boundary",
422
+ "ppr.completed": "app.ppr.complete",
423
+ "intercept.current_segment": "app.intercept.current",
424
+ "intercept.parent_segment": "app.intercept.parent",
425
+ "intercept.root_catchall": "app.intercept.root",
426
+ "intercept.modal_opened": "app.intercept.modal",
427
+ "parallel.default_rendered": "app.parallel.default",
428
+ "parallel.loading_rendered": "app.parallel.loading",
429
+ "parallel.error_boundary_captured": "app.parallel.error",
430
+ "parallel.slot_navigated": "app.parallel.navigate"
431
+ },
432
+ "pages-router": {
433
+ "action.form_submitted": "pages.api.form.submit",
434
+ "action.revalidate_path": "pages.isr.revalidate",
435
+ "action.revalidate_tag": "pages.cache.tag.noop",
436
+ "action.redirected": "pages.router.redirect",
437
+ "ppr.static_shell_rendered": "pages.ssg.shell",
438
+ "ppr.dynamic_hole_opened": "pages.ssr.dynamic-hole",
439
+ "ppr.streaming_boundary_flushed": "pages.stream.boundary",
440
+ "ppr.completed": "pages.render.complete",
441
+ "intercept.current_segment": "pages.intercept.current",
442
+ "intercept.parent_segment": "pages.intercept.parent",
443
+ "intercept.root_catchall": "pages.intercept.root",
444
+ "intercept.modal_opened": "pages.modal.open",
445
+ "parallel.default_rendered": "pages.slot.default",
446
+ "parallel.loading_rendered": "pages.slot.loading",
447
+ "parallel.error_boundary_captured": "pages.slot.error",
448
+ "parallel.slot_navigated": "pages.slot.navigate"
449
+ },
450
+ "edge-runtime": {
451
+ "action.form_submitted": "edge.action.form.submit",
452
+ "action.revalidate_path": "edge.cache.revalidatePath",
453
+ "action.revalidate_tag": "edge.cache.revalidateTag",
454
+ "action.redirected": "edge.response.redirect",
455
+ "ppr.static_shell_rendered": "edge.ppr.static-shell",
456
+ "ppr.dynamic_hole_opened": "edge.ppr.dynamic-hole",
457
+ "ppr.streaming_boundary_flushed": "edge.stream.boundary",
458
+ "ppr.completed": "edge.ppr.complete",
459
+ "intercept.current_segment": "edge.intercept.current",
460
+ "intercept.parent_segment": "edge.intercept.parent",
461
+ "intercept.root_catchall": "edge.intercept.root",
462
+ "intercept.modal_opened": "edge.modal.open",
463
+ "parallel.default_rendered": "edge.parallel.default",
464
+ "parallel.loading_rendered": "edge.parallel.loading",
465
+ "parallel.error_boundary_captured": "edge.parallel.error",
466
+ "parallel.slot_navigated": "edge.parallel.navigate"
467
+ }
468
+ };
469
+ function providerEventName(target, neutral) {
470
+ return dialect[target][neutral] ?? neutral;
471
+ }
472
+
473
+ // src/semantics/server-action-advanced.ts
474
+ function startServerActionAdvanced(input) {
475
+ if (input.actionId.length === 0) {
476
+ throw new Error("startServerActionAdvanced: actionId must not be empty");
477
+ }
478
+ return {
479
+ target: input.target,
480
+ actionId: input.actionId,
481
+ state: "idle",
482
+ form: {},
483
+ revalidatedPaths: [],
484
+ revalidatedTags: [],
485
+ redirectUrl: null,
486
+ history: []
487
+ };
488
+ }
489
+ function submitFormAction(session, form) {
490
+ if (session.state !== "idle") {
491
+ throw new Error(`submitFormAction: session is ${session.state}, not idle`);
492
+ }
493
+ session.state = "submitted";
494
+ session.form = { ...form };
495
+ return emit(session, "action.form_submitted", {
496
+ fields: Object.keys(form).join(","),
497
+ fieldCount: Object.keys(form).length
498
+ });
499
+ }
500
+ function revalidateActionPath(session, path) {
501
+ if (session.state === "idle") {
502
+ throw new Error("revalidateActionPath: form action was not submitted");
503
+ }
504
+ if (!path.startsWith("/")) {
505
+ throw new Error("revalidateActionPath: path must start with /");
506
+ }
507
+ session.state = "path-revalidated";
508
+ session.revalidatedPaths.push(path);
509
+ return emit(session, "action.revalidate_path", { path, count: session.revalidatedPaths.length });
510
+ }
511
+ function revalidateActionTag(session, tag) {
512
+ if (session.state === "idle") {
513
+ throw new Error("revalidateActionTag: form action was not submitted");
514
+ }
515
+ if (tag.length === 0) {
516
+ throw new Error("revalidateActionTag: tag must not be empty");
517
+ }
518
+ session.state = "tag-revalidated";
519
+ session.revalidatedTags.push(tag);
520
+ return emit(session, "action.revalidate_tag", { tag, count: session.revalidatedTags.length });
521
+ }
522
+ function redirectAction(session, url) {
523
+ if (session.state === "idle") {
524
+ throw new Error("redirectAction: form action was not submitted");
525
+ }
526
+ if (url.length === 0) {
527
+ throw new Error("redirectAction: url must not be empty");
528
+ }
529
+ session.state = "redirected";
530
+ session.redirectUrl = url;
531
+ return emit(session, "action.redirected", { url });
532
+ }
533
+ function emit(session, neutralEvent, metadata) {
534
+ const step = {
535
+ neutralEvent,
536
+ providerEvent: providerEventName(session.target, neutralEvent),
537
+ state: session.state,
538
+ amountCents: 0,
539
+ metadata: { target: session.target, actionId: session.actionId, ...metadata }
540
+ };
541
+ session.history.push(step);
542
+ return step;
543
+ }
544
+
545
+ // src/semantics/partial-prerendering.ts
546
+ function startPartialPrerendering(input) {
547
+ if (input.routeId.length === 0) {
548
+ throw new Error("startPartialPrerendering: routeId must not be empty");
549
+ }
550
+ return {
551
+ target: input.target,
552
+ routeId: input.routeId,
553
+ state: "idle",
554
+ shellHtml: null,
555
+ dynamicHoles: /* @__PURE__ */ new Map(),
556
+ streamedBoundaries: [],
557
+ history: []
558
+ };
559
+ }
560
+ function renderStaticShell(session, html) {
561
+ if (session.state !== "idle") {
562
+ throw new Error(`renderStaticShell: session is ${session.state}, not idle`);
563
+ }
564
+ if (html.length === 0) {
565
+ throw new Error("renderStaticShell: html must not be empty");
566
+ }
567
+ session.state = "static-shell";
568
+ session.shellHtml = html;
569
+ return emit2(session, "ppr.static_shell_rendered", { bytes: html.length });
570
+ }
571
+ function openDynamicHole(session, input) {
572
+ if (session.shellHtml === null) {
573
+ throw new Error("openDynamicHole: static shell must be rendered first");
574
+ }
575
+ if (input.holeId.length === 0) {
576
+ throw new Error("openDynamicHole: holeId must not be empty");
577
+ }
578
+ session.state = "dynamic-hole";
579
+ session.dynamicHoles.set(input.holeId, input.fallback);
580
+ return emit2(session, "ppr.dynamic_hole_opened", {
581
+ holeId: input.holeId,
582
+ fallback: input.fallback,
583
+ holeCount: session.dynamicHoles.size
584
+ });
585
+ }
586
+ function flushStreamingBoundary(session, input) {
587
+ if (!session.dynamicHoles.has(input.holeId)) {
588
+ throw new Error(`flushStreamingBoundary: ${input.holeId} is not an open dynamic hole`);
589
+ }
590
+ if (input.html.length === 0) {
591
+ throw new Error("flushStreamingBoundary: html must not be empty");
592
+ }
593
+ session.state = "streaming";
594
+ session.streamedBoundaries.push(input.holeId);
595
+ session.dynamicHoles.set(input.holeId, input.html);
596
+ return emit2(session, "ppr.streaming_boundary_flushed", {
597
+ holeId: input.holeId,
598
+ bytes: input.html.length
599
+ });
600
+ }
601
+ function completePartialPrerendering(session) {
602
+ if (session.shellHtml === null) {
603
+ throw new Error("completePartialPrerendering: static shell was not rendered");
604
+ }
605
+ session.state = "completed";
606
+ return emit2(session, "ppr.completed", {
607
+ holeCount: session.dynamicHoles.size,
608
+ streamedCount: session.streamedBoundaries.length
609
+ });
610
+ }
611
+ function emit2(session, neutralEvent, metadata) {
612
+ const step = {
613
+ neutralEvent,
614
+ providerEvent: providerEventName(session.target, neutralEvent),
615
+ state: session.state,
616
+ amountCents: 0,
617
+ metadata: { target: session.target, routeId: session.routeId, ...metadata }
618
+ };
619
+ session.history.push(step);
620
+ return step;
621
+ }
622
+
623
+ // src/semantics/interception-routes.ts
624
+ function startInterceptionRoutes(input) {
625
+ if (input.routeId.length === 0) {
626
+ throw new Error("startInterceptionRoutes: routeId must not be empty");
627
+ }
628
+ return {
629
+ target: input.target,
630
+ routeId: input.routeId,
631
+ state: "idle",
632
+ matches: [],
633
+ modalRoute: null,
634
+ history: []
635
+ };
636
+ }
637
+ function interceptCurrentSegment(session, from, to) {
638
+ return intercept(session, "(.)", "current", "intercept.current_segment", from, to);
639
+ }
640
+ function interceptParentSegment(session, from, to) {
641
+ return intercept(session, "(..)", "parent", "intercept.parent_segment", from, to);
642
+ }
643
+ function interceptRootCatchall(session, from, to) {
644
+ return intercept(session, "(...)", "root", "intercept.root_catchall", from, to);
645
+ }
646
+ function openInterceptedModal(session, modalRoute) {
647
+ if (modalRoute.length === 0) {
648
+ throw new Error("openInterceptedModal: modalRoute must not be empty");
649
+ }
650
+ if (session.matches.length === 0) {
651
+ throw new Error("openInterceptedModal: an interception match is required first");
652
+ }
653
+ session.state = "modal-open";
654
+ session.modalRoute = modalRoute;
655
+ return emit3(session, "intercept.modal_opened", {
656
+ modalRoute,
657
+ matchCount: session.matches.length
658
+ });
659
+ }
660
+ function intercept(session, matcher, state, neutralEvent, from, to) {
661
+ if (!from.startsWith("/") || !to.startsWith("/")) {
662
+ throw new Error("intercept: from and to must start with /");
663
+ }
664
+ session.state = state;
665
+ session.matches.push({ matcher, from, to });
666
+ return emit3(session, neutralEvent, { matcher, from, to });
667
+ }
668
+ function emit3(session, neutralEvent, metadata) {
669
+ const step = {
670
+ neutralEvent,
671
+ providerEvent: providerEventName(session.target, neutralEvent),
672
+ state: session.state,
673
+ amountCents: 0,
674
+ metadata: { target: session.target, routeId: session.routeId, ...metadata }
675
+ };
676
+ session.history.push(step);
677
+ return step;
678
+ }
679
+
680
+ // src/semantics/parallel-routes-advanced.ts
681
+ function startParallelRoutesAdvanced(input) {
682
+ if (input.layoutId.length === 0) {
683
+ throw new Error("startParallelRoutesAdvanced: layoutId must not be empty");
684
+ }
685
+ return {
686
+ target: input.target,
687
+ layoutId: input.layoutId,
688
+ state: "idle",
689
+ slots: /* @__PURE__ */ new Map(),
690
+ loadingSlots: /* @__PURE__ */ new Set(),
691
+ errors: [],
692
+ history: []
693
+ };
694
+ }
695
+ function renderDefaultSlot(session, slot, html) {
696
+ assertSlot(slot);
697
+ session.state = "default-rendered";
698
+ session.slots.set(slot, html);
699
+ return emit4(session, "parallel.default_rendered", { slot, html });
700
+ }
701
+ function renderLoadingState(session, slot) {
702
+ assertSlot(slot);
703
+ session.state = "loading-rendered";
704
+ session.loadingSlots.add(slot);
705
+ return emit4(session, "parallel.loading_rendered", {
706
+ slot,
707
+ loadingCount: session.loadingSlots.size
708
+ });
709
+ }
710
+ function captureParallelError(session, input) {
711
+ assertSlot(input.slot);
712
+ const message = typeof input.error === "string" ? input.error : input.error.message;
713
+ session.state = "error-captured";
714
+ session.errors.push({ slot: input.slot, message });
715
+ return emit4(session, "parallel.error_boundary_captured", {
716
+ slot: input.slot,
717
+ message,
718
+ errorCount: session.errors.length
719
+ });
720
+ }
721
+ function navigateSlot(session, input) {
722
+ assertSlot(input.slot);
723
+ if (!input.from.startsWith("/") || !input.to.startsWith("/")) {
724
+ throw new Error("navigateSlot: from and to must start with /");
725
+ }
726
+ session.state = "slot-navigated";
727
+ session.loadingSlots.delete(input.slot);
728
+ return emit4(session, "parallel.slot_navigated", input);
729
+ }
730
+ function assertSlot(slot) {
731
+ if (slot.length === 0) {
732
+ throw new Error("slot must not be empty");
733
+ }
734
+ }
735
+ function emit4(session, neutralEvent, metadata) {
736
+ const step = {
737
+ neutralEvent,
738
+ providerEvent: providerEventName(session.target, neutralEvent),
739
+ state: session.state,
740
+ amountCents: 0,
741
+ metadata: { target: session.target, layoutId: session.layoutId, ...metadata }
742
+ };
743
+ session.history.push(step);
744
+ return step;
745
+ }
746
+
747
+ // src/semantics/fidelity.ts
748
+ var NEXT_AXIS_TO_EVENTS = {
749
+ "server-action-advanced": [
750
+ "action.form_submitted",
751
+ "action.revalidate_path",
752
+ "action.revalidate_tag",
753
+ "action.redirected"
754
+ ],
755
+ "partial-prerendering": [
756
+ "ppr.static_shell_rendered",
757
+ "ppr.dynamic_hole_opened",
758
+ "ppr.streaming_boundary_flushed",
759
+ "ppr.completed"
760
+ ],
761
+ "interception-routes": [
762
+ "intercept.current_segment",
763
+ "intercept.parent_segment",
764
+ "intercept.root_catchall",
765
+ "intercept.modal_opened"
766
+ ],
767
+ "parallel-routes-advanced": [
768
+ "parallel.default_rendered",
769
+ "parallel.loading_rendered",
770
+ "parallel.error_boundary_captured",
771
+ "parallel.slot_navigated"
772
+ ]
773
+ };
774
+ function collectFidelityCoverage(providers = ["app-router", "pages-router", "edge-runtime"]) {
775
+ const axes = Object.keys(NEXT_AXIS_TO_EVENTS);
776
+ const rows = [];
777
+ for (const provider of providers) {
778
+ for (const axis of axes) {
779
+ const neutralEvents = NEXT_AXIS_TO_EVENTS[axis];
780
+ rows.push({
781
+ provider,
782
+ axis,
783
+ neutralEvents,
784
+ providerEvents: neutralEvents.map((event) => providerEventName(provider, event))
785
+ });
786
+ }
787
+ }
788
+ return { providers, axes, rows };
789
+ }
790
+
791
+ // src/real-driver.ts
792
+ var TARGET_KEY_ENV = {
793
+ "app-router": "NEXT_APP_URL",
794
+ "pages-router": "NEXT_PAGES_URL",
795
+ "edge-runtime": "EDGE_RUNTIME_URL"
796
+ };
797
+ function resolveMode(provider, env = process.env) {
798
+ const rawMode = env.KIWA_MODE?.toLowerCase();
799
+ if (rawMode !== void 0 && rawMode !== "real" && rawMode !== "mock") {
800
+ return { provider, mode: "mock", reason: "invalid-mode" };
801
+ }
802
+ if (rawMode !== "real") {
803
+ return { provider, mode: "mock", reason: "default-mock" };
804
+ }
805
+ const keyValue = env[TARGET_KEY_ENV[provider]];
806
+ if (typeof keyValue !== "string" || keyValue.length === 0) {
807
+ return { provider, mode: "mock", reason: "missing-key" };
808
+ }
809
+ return { provider, mode: "real", reason: "kiwa-mode-real" };
810
+ }
811
+ function resolveAllModes(env = process.env) {
812
+ const providers = ["app-router", "pages-router", "edge-runtime"];
813
+ return providers.map((provider) => resolveMode(provider, env));
814
+ }
815
+ function assertMode(provider, expected, env = process.env) {
816
+ const resolved = resolveMode(provider, env);
817
+ if (resolved.mode !== expected) {
818
+ throw new Error(
819
+ `expected ${provider} in ${expected} mode but resolved ${resolved.mode} (${resolved.reason})`
820
+ );
821
+ }
822
+ }
282
823
  // Annotate the CommonJS export names for ESM import in node:
283
824
  0 && (module.exports = {
284
825
  FORBIDDEN_SYMBOL,
285
826
  MIDDLEWARE_ACTION_SYMBOL,
827
+ NEXT_AXIS_TO_EVENTS,
286
828
  NOT_FOUND_SYMBOL,
287
829
  PARALLEL_INTERCEPTION_SYMBOL,
288
830
  REDIRECT_SYMBOL,
831
+ RSC_ERROR_BOUNDARY_SYMBOL,
289
832
  RSC_REDIRECT_SYMBOL,
833
+ assertMode,
834
+ captureParallelError,
835
+ collectFidelityCoverage,
836
+ completePartialPrerendering,
290
837
  findAll,
838
+ flushStreamingBoundary,
839
+ interceptCurrentSegment,
840
+ interceptParentSegment,
841
+ interceptRootCatchall,
291
842
  invokeMiddleware,
292
843
  invokeParallelRoutes,
293
844
  invokeServerAction,
294
845
  middlewareActions,
846
+ navigateSlot,
847
+ openDynamicHole,
848
+ openInterceptedModal,
849
+ providerEventName,
850
+ redirectAction,
851
+ renderDefaultSlot,
852
+ renderLoadingState,
295
853
  renderServerComponent,
854
+ renderStaticShell,
855
+ resolveAllModes,
856
+ resolveMode,
857
+ revalidateActionPath,
858
+ revalidateActionTag,
859
+ setupNextRscEnv,
860
+ startInterceptionRoutes,
861
+ startParallelRoutesAdvanced,
862
+ startPartialPrerendering,
863
+ startServerActionAdvanced,
864
+ submitFormAction,
296
865
  textContent
297
866
  });
298
867
  //# sourceMappingURL=index.cjs.map