@kiwa-test/nextjs 1.0.3 → 1.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kiwa-test/nextjs",
3
- "version": "1.0.3",
4
- "description": "Next.js App Router test adapter for kiwa (Server Actions / RSC / middleware) — invoke Server Actions with FormData / headers / cookies and assert redirect / revalidate behavior",
3
+ "version": "1.1.0",
4
+ "description": "Next.js App Router test adapter for kiwa (Server Actions / RSC / middleware / Parallel Routes + Intercepting Routes) — invoke Server Actions with FormData / headers / cookies, render parallel-route layouts with named slots + intercepting URL matchers, and assert redirect / revalidate behavior",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "cardene",
@@ -16,6 +16,8 @@
16
16
  "rsc",
17
17
  "react-server-components",
18
18
  "middleware",
19
+ "parallel-routes",
20
+ "intercepting-routes",
19
21
  "testing",
20
22
  "vitest"
21
23
  ],
@@ -55,8 +57,15 @@
55
57
  "engines": {
56
58
  "node": ">=20"
57
59
  },
60
+ "scripts": {
61
+ "build": "tsup",
62
+ "test": "node -e \"require('node:fs').rmSync('.vitest-dist',{recursive:true,force:true})\" && tsc -p tsconfig.vitest.json && vitest run .vitest-dist/tests --environment node",
63
+ "test:cov": "node -e \"require('node:fs').rmSync('.vitest-dist',{recursive:true,force:true})\" && tsc -p tsconfig.vitest.json && vitest run .vitest-dist/tests --environment node --coverage --coverage.provider=v8 --coverage.reporter=json --coverage.reporter=json-summary --coverage.reporter=text --coverage.reportsDirectory=coverage --coverage.include='.vitest-dist/src/invoke-server-action.js' --coverage.include='.vitest-dist/src/invoke-middleware.js' --coverage.include='.vitest-dist/src/render-server-component.js' --coverage.include='.vitest-dist/src/invoke-parallel-routes.js' --coverage.include='.vitest-dist/src/setup-next-rsc-env.js' --coverage.exclude='.vitest-dist/tests/**' --coverage.exclude='.vitest-dist/src/index.js'",
64
+ "test:mutation": "stryker run",
65
+ "typecheck": "tsc --noEmit"
66
+ },
58
67
  "dependencies": {
59
- "@kiwa-test/core": "1.0.1"
68
+ "@kiwa-test/core": "workspace:*"
60
69
  },
61
70
  "peerDependencies": {
62
71
  "vitest": "^2"
@@ -69,12 +78,5 @@
69
78
  "tsup": "^8.3.0",
70
79
  "typescript": "^5.6.0",
71
80
  "vitest": "^2.1.0"
72
- },
73
- "scripts": {
74
- "build": "tsup",
75
- "test": "node -e \"require('node:fs').rmSync('.vitest-dist',{recursive:true,force:true})\" && tsc -p tsconfig.vitest.json && vitest run .vitest-dist/tests --environment node",
76
- "test:cov": "node -e \"require('node:fs').rmSync('.vitest-dist',{recursive:true,force:true})\" && tsc -p tsconfig.vitest.json && vitest run .vitest-dist/tests --environment node --coverage --coverage.provider=v8 --coverage.reporter=json --coverage.reporter=json-summary --coverage.reporter=text --coverage.reportsDirectory=coverage --coverage.include='.vitest-dist/src/invoke-server-action.js' --coverage.include='.vitest-dist/src/invoke-middleware.js' --coverage.include='.vitest-dist/src/render-server-component.js' --coverage.exclude='.vitest-dist/tests/**' --coverage.exclude='.vitest-dist/src/index.js'",
77
- "test:mutation": "stryker run",
78
- "typecheck": "tsc --noEmit"
79
81
  }
80
- }
82
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 cardene777
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/dist/index.cjs DELETED
@@ -1,237 +0,0 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- FORBIDDEN_SYMBOL: () => FORBIDDEN_SYMBOL,
24
- MIDDLEWARE_ACTION_SYMBOL: () => MIDDLEWARE_ACTION_SYMBOL,
25
- NOT_FOUND_SYMBOL: () => NOT_FOUND_SYMBOL,
26
- REDIRECT_SYMBOL: () => REDIRECT_SYMBOL,
27
- RSC_REDIRECT_SYMBOL: () => RSC_REDIRECT_SYMBOL,
28
- findAll: () => findAll,
29
- invokeMiddleware: () => invokeMiddleware,
30
- invokeServerAction: () => invokeServerAction,
31
- middlewareActions: () => middlewareActions,
32
- renderServerComponent: () => renderServerComponent,
33
- textContent: () => textContent
34
- });
35
- module.exports = __toCommonJS(index_exports);
36
-
37
- // src/invoke-server-action.ts
38
- var REDIRECT_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.redirect");
39
- function createCookieJar(initial) {
40
- const store = new Map(Object.entries(initial));
41
- return {
42
- get(name) {
43
- return store.get(name);
44
- },
45
- set(name, value) {
46
- store.set(name, value);
47
- },
48
- delete(name) {
49
- store.delete(name);
50
- },
51
- entries() {
52
- return Array.from(store.entries());
53
- }
54
- };
55
- }
56
- function isRedirectSignal(value) {
57
- return typeof value === "object" && value !== null && value[REDIRECT_SYMBOL] === true;
58
- }
59
- async function invokeServerAction(opts) {
60
- const headers = /* @__PURE__ */ new Map();
61
- for (const [name, value] of Object.entries(opts.headers ?? {})) {
62
- headers.set(name.toLowerCase(), value);
63
- }
64
- const env = {
65
- cookies: createCookieJar(opts.cookies ?? {}),
66
- headers,
67
- revalidated: { paths: [], tags: [] },
68
- redirect: null
69
- };
70
- const callArgs = [opts.formData ?? new FormData(), ...opts.args ?? []];
71
- let result;
72
- let error;
73
- try {
74
- result = await opts.action(...callArgs);
75
- } catch (caught) {
76
- if (isRedirectSignal(caught)) {
77
- env.redirect = caught;
78
- } else {
79
- error = caught;
80
- }
81
- }
82
- return { result, error, env };
83
- }
84
-
85
- // src/invoke-middleware.ts
86
- var MIDDLEWARE_ACTION_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.middleware.action");
87
- function buildRequest(opts) {
88
- const url = new URL(opts.url);
89
- const headers = /* @__PURE__ */ new Map();
90
- for (const [name, value] of Object.entries(opts.headers ?? {})) {
91
- headers.set(name.toLowerCase(), value);
92
- }
93
- const cookies = new Map(Object.entries(opts.cookies ?? {}));
94
- return {
95
- url: opts.url,
96
- method: opts.method ?? "GET",
97
- headers,
98
- cookies,
99
- nextUrl: {
100
- pathname: url.pathname,
101
- search: url.search,
102
- searchParams: url.searchParams
103
- },
104
- geo: opts.geo ?? {}
105
- };
106
- }
107
- var middlewareActions = {
108
- next() {
109
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "next" };
110
- },
111
- redirect(url, status = 307) {
112
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "redirect", url, status };
113
- },
114
- rewrite(url) {
115
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "rewrite", url };
116
- },
117
- json(body, status = 200) {
118
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "json", body, status };
119
- }
120
- };
121
- async function invokeMiddleware(opts) {
122
- const req = buildRequest(opts);
123
- const responseHeaders = /* @__PURE__ */ new Map();
124
- const responseCookies = /* @__PURE__ */ new Map();
125
- const seam = {
126
- setHeader(name, value) {
127
- responseHeaders.set(name.toLowerCase(), value);
128
- },
129
- setCookie(name, value) {
130
- responseCookies.set(name, value);
131
- }
132
- };
133
- let action = middlewareActions.next();
134
- let error;
135
- try {
136
- const result = await opts.middleware(req, seam);
137
- action = result;
138
- } catch (caught) {
139
- error = caught;
140
- action = { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "noop" };
141
- }
142
- return {
143
- env: {
144
- responseHeaders,
145
- responseCookies,
146
- action
147
- },
148
- error
149
- };
150
- }
151
-
152
- // src/render-server-component.ts
153
- var NOT_FOUND_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.rsc.notFound");
154
- var FORBIDDEN_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.rsc.forbidden");
155
- var RSC_REDIRECT_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.rsc.redirect");
156
- function isRscElement(node) {
157
- if (typeof node !== "object" || node === null) return false;
158
- const candidate = node;
159
- return typeof candidate.type !== "undefined" && typeof candidate.props === "object" && candidate.props !== null;
160
- }
161
- function isNotFound(value) {
162
- return typeof value === "object" && value !== null && value[NOT_FOUND_SYMBOL] === true;
163
- }
164
- function isForbidden(value) {
165
- return typeof value === "object" && value !== null && value[FORBIDDEN_SYMBOL] === true;
166
- }
167
- function isRscRedirect(value) {
168
- return typeof value === "object" && value !== null && value[RSC_REDIRECT_SYMBOL] === true;
169
- }
170
- function findAll(tree, predicate) {
171
- const out = [];
172
- function visit(node) {
173
- if (Array.isArray(node)) {
174
- node.forEach(visit);
175
- return;
176
- }
177
- if (!isRscElement(node)) return;
178
- if (predicate(node)) out.push(node);
179
- const children = node.props.children;
180
- if (typeof children !== "undefined") {
181
- visit(children);
182
- }
183
- }
184
- visit(tree);
185
- return out;
186
- }
187
- function textContent(tree) {
188
- const parts = [];
189
- function visit(node) {
190
- if (Array.isArray(node)) {
191
- node.forEach(visit);
192
- return;
193
- }
194
- if (typeof node === "string" || typeof node === "number") {
195
- parts.push(String(node));
196
- return;
197
- }
198
- if (isRscElement(node)) {
199
- const children = node.props.children;
200
- if (typeof children !== "undefined") visit(children);
201
- }
202
- }
203
- visit(tree);
204
- return parts.filter((p) => p.length > 0).join(" ");
205
- }
206
- async function renderServerComponent(opts) {
207
- let tree = null;
208
- let signal = null;
209
- let error;
210
- const props = opts.props ?? {};
211
- try {
212
- const result = await opts.component(props);
213
- tree = result;
214
- } catch (caught) {
215
- if (isNotFound(caught) || isForbidden(caught) || isRscRedirect(caught)) {
216
- signal = caught;
217
- } else {
218
- error = caught;
219
- }
220
- }
221
- return { tree, signal, error };
222
- }
223
- // Annotate the CommonJS export names for ESM import in node:
224
- 0 && (module.exports = {
225
- FORBIDDEN_SYMBOL,
226
- MIDDLEWARE_ACTION_SYMBOL,
227
- NOT_FOUND_SYMBOL,
228
- REDIRECT_SYMBOL,
229
- RSC_REDIRECT_SYMBOL,
230
- findAll,
231
- invokeMiddleware,
232
- invokeServerAction,
233
- middlewareActions,
234
- renderServerComponent,
235
- textContent
236
- });
237
- //# sourceMappingURL=index.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/index.ts","../src/invoke-server-action.ts","../src/invoke-middleware.ts","../src/render-server-component.ts"],"sourcesContent":["export {\n invokeServerAction,\n type ServerActionFunction,\n type ServerActionResult,\n type ServerActionInvocation,\n type ServerActionEnv,\n type CookieJar,\n REDIRECT_SYMBOL,\n type RedirectSignal,\n} from './invoke-server-action.js';\n\nexport {\n invokeMiddleware,\n middlewareActions,\n MIDDLEWARE_ACTION_SYMBOL,\n type InvokeMiddlewareOptions,\n type InvokeMiddlewareResult,\n type MiddlewareFunction,\n type MiddlewareRequest,\n type MiddlewareEnv,\n type MiddlewareAction,\n type MiddlewareActionKind,\n} from './invoke-middleware.js';\n\nexport {\n renderServerComponent,\n findAll,\n textContent,\n NOT_FOUND_SYMBOL,\n FORBIDDEN_SYMBOL,\n RSC_REDIRECT_SYMBOL,\n type RenderServerComponentOptions,\n type RenderServerComponentResult,\n type RscElement,\n type RscNode,\n type RscSignal,\n type NotFoundSignal,\n type ForbiddenSignal,\n type RscRedirectSignal,\n} from './render-server-component.js';\n","// Next.js Server Action invocation helper for kiwa tests.\n//\n// Wraps an `'use server'` async function and captures Next.js side-effects\n// (redirect / revalidatePath / cookies set) without requiring a real Next.js\n// runtime. The test calls `invokeServerAction({ action, formData, ... })`\n// and asserts on the returned `ServerActionResult` instead of relying on\n// integration-level behavior.\n//\n// Out of scope on purpose:\n// - real `next/server` middleware semantics (use #495 helper instead)\n// - RSC payload format (use #494 helper instead)\n// - rendering a page after the action (use Playwright + `/kiwa-e2e` for that)\n\nexport const REDIRECT_SYMBOL = Symbol.for('kiwa.next.redirect');\n\nexport interface RedirectSignal {\n readonly [REDIRECT_SYMBOL]: true;\n readonly url: string;\n readonly type: 'replace' | 'push';\n}\n\nexport interface CookieJar {\n get(name: string): string | undefined;\n set(name: string, value: string, options?: Record<string, unknown>): void;\n delete(name: string): void;\n entries(): Array<[string, string]>;\n}\n\nexport interface ServerActionEnv {\n readonly cookies: CookieJar;\n readonly headers: Map<string, string>;\n readonly revalidated: { paths: string[]; tags: string[] };\n readonly redirect: RedirectSignal | null;\n}\n\n// We deliberately accept `any[]` here so callers can pass typed actions like\n// `(fd: FormData) => Promise<T>` or `(prev: State, fd: FormData) => Promise<T>`\n// without contortions. The runtime always invokes `action(formData, ...args)`.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ServerActionFunction<TResult> = (...args: any[]) => Promise<TResult> | TResult;\n\nexport interface ServerActionInvocation<TResult> {\n /** The `'use server'` async function under test. */\n readonly action: ServerActionFunction<TResult>;\n /** Optional FormData first argument (default empty). */\n readonly formData?: FormData;\n /** Extra positional args appended after FormData (e.g. previous state for useFormState). */\n readonly args?: unknown[];\n /** Initial cookie jar entries (name → value). */\n readonly cookies?: Record<string, string>;\n /** Initial request headers (case-insensitive). */\n readonly headers?: Record<string, string>;\n}\n\nexport interface ServerActionResult<TResult> {\n /** Resolved return value (or `undefined` if the action threw a redirect signal). */\n readonly result: TResult | undefined;\n /** Error thrown by the action (excluding redirect signals which are normalized). */\n readonly error: unknown;\n /** Side-effects captured during the invocation. */\n readonly env: ServerActionEnv;\n}\n\nfunction createCookieJar(initial: Record<string, string>): CookieJar {\n const store = new Map<string, string>(Object.entries(initial));\n return {\n get(name) {\n return store.get(name);\n },\n set(name, value) {\n store.set(name, value);\n },\n delete(name) {\n store.delete(name);\n },\n entries() {\n return Array.from(store.entries());\n },\n };\n}\n\nfunction isRedirectSignal(value: unknown): value is RedirectSignal {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { [REDIRECT_SYMBOL]?: true })[REDIRECT_SYMBOL] === true\n );\n}\n\n/**\n * Invoke a Next.js Server Action in isolation and capture its side-effects.\n *\n * The action is called as `await action(formData, ...args)`. The kiwa helper\n * does NOT monkey-patch global `next/navigation` / `next/headers` / `next/cache`\n * imports. Instead the action under test should accept its dependencies via\n * an injectable seam (a parameter or a module-level setter) so tests stay\n * deterministic. See `examples/nextjs-server-actions-poc/` for the pattern.\n */\nexport async function invokeServerAction<TResult>(\n opts: ServerActionInvocation<TResult>,\n): Promise<ServerActionResult<TResult>> {\n const headers = new Map<string, string>();\n for (const [name, value] of Object.entries(opts.headers ?? {})) {\n headers.set(name.toLowerCase(), value);\n }\n const env: ServerActionEnv = {\n cookies: createCookieJar(opts.cookies ?? {}),\n headers,\n revalidated: { paths: [], tags: [] },\n redirect: null,\n };\n const callArgs: unknown[] = [opts.formData ?? new FormData(), ...(opts.args ?? [])];\n let result: TResult | undefined;\n let error: unknown;\n try {\n result = await opts.action(...callArgs);\n } catch (caught) {\n if (isRedirectSignal(caught)) {\n (env as { redirect: RedirectSignal | null }).redirect = caught;\n } else {\n error = caught;\n }\n }\n return { result, error, env };\n}\n","// Next.js middleware.ts invocation helper for kiwa tests (Issue #495).\n//\n// Middleware in App Router is `(req: NextRequest) => NextResponse | Response | void`.\n// It can set response headers / cookies, throw redirect via NextResponse.redirect(),\n// rewrite via NextResponse.rewrite(), short-circuit with NextResponse.json(), or\n// pass through with NextResponse.next(). kiwa wraps the call so unit tests can\n// assert on the captured action + outgoing headers/cookies without a running\n// Next.js server.\n//\n// Out of scope on purpose:\n// - matcher config evaluation (the test already knows which middleware to call)\n// - revalidate / cache header semantics\n// - Edge runtime polyfill (use Node test environment + the helper's simulated request)\n\nexport const MIDDLEWARE_ACTION_SYMBOL = Symbol.for('kiwa.next.middleware.action');\n\nexport type MiddlewareActionKind = 'next' | 'redirect' | 'rewrite' | 'json' | 'noop';\n\nexport interface MiddlewareAction {\n readonly [MIDDLEWARE_ACTION_SYMBOL]: true;\n readonly kind: MiddlewareActionKind;\n readonly url?: string;\n readonly body?: unknown;\n readonly status?: number;\n}\n\nexport interface MiddlewareRequest {\n readonly url: string;\n readonly method: string;\n readonly headers: ReadonlyMap<string, string>;\n readonly cookies: ReadonlyMap<string, string>;\n readonly nextUrl: {\n readonly pathname: string;\n readonly search: string;\n readonly searchParams: URLSearchParams;\n };\n readonly geo: {\n readonly country?: string;\n readonly region?: string;\n readonly city?: string;\n };\n}\n\nexport interface MiddlewareEnv {\n readonly responseHeaders: Map<string, string>;\n readonly responseCookies: Map<string, string>;\n readonly action: MiddlewareAction;\n}\n\nexport type MiddlewareFunction = (\n req: MiddlewareRequest,\n env: { setHeader: (name: string, value: string) => void; setCookie: (name: string, value: string) => void },\n) => MiddlewareAction | Promise<MiddlewareAction>;\n\nexport interface InvokeMiddlewareOptions {\n readonly middleware: MiddlewareFunction;\n readonly url: string;\n readonly method?: string;\n readonly headers?: Record<string, string>;\n readonly cookies?: Record<string, string>;\n readonly geo?: {\n readonly country?: string;\n readonly region?: string;\n readonly city?: string;\n };\n}\n\nexport interface InvokeMiddlewareResult {\n readonly env: MiddlewareEnv;\n readonly error: unknown;\n}\n\nfunction buildRequest(opts: InvokeMiddlewareOptions): MiddlewareRequest {\n const url = new URL(opts.url);\n const headers = new Map<string, string>();\n for (const [name, value] of Object.entries(opts.headers ?? {})) {\n headers.set(name.toLowerCase(), value);\n }\n const cookies = new Map<string, string>(Object.entries(opts.cookies ?? {}));\n return {\n url: opts.url,\n method: opts.method ?? 'GET',\n headers,\n cookies,\n nextUrl: {\n pathname: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n },\n geo: opts.geo ?? {},\n };\n}\n\n/**\n * Helpers your `middleware.ts` returns instead of constructing NextResponse\n * directly. Keep the production code shape close by re-exporting these from\n * a shared module; the helper expects the returned value to be a\n * MiddlewareAction shaped object.\n */\nexport const middlewareActions = {\n next(): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'next' };\n },\n redirect(url: string, status = 307): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'redirect', url, status };\n },\n rewrite(url: string): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'rewrite', url };\n },\n json(body: unknown, status = 200): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'json', body, status };\n },\n};\n\n/**\n * Invoke a middleware function in isolation and capture its outgoing\n * response shape + headers + cookies. Mirrors the kiwa style of\n * invokeServerAction: no globals, no real Next.js runtime.\n */\nexport async function invokeMiddleware(opts: InvokeMiddlewareOptions): Promise<InvokeMiddlewareResult> {\n const req = buildRequest(opts);\n const responseHeaders = new Map<string, string>();\n const responseCookies = new Map<string, string>();\n const seam = {\n setHeader(name: string, value: string) {\n responseHeaders.set(name.toLowerCase(), value);\n },\n setCookie(name: string, value: string) {\n responseCookies.set(name, value);\n },\n };\n let action: MiddlewareAction = middlewareActions.next();\n let error: unknown;\n try {\n const result = await opts.middleware(req, seam);\n action = result;\n } catch (caught) {\n error = caught;\n action = { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'noop' };\n }\n return {\n env: {\n responseHeaders,\n responseCookies,\n action,\n },\n error,\n };\n}\n","// Next.js async React Server Component (RSC) test helper for kiwa (Issue #494).\n//\n// Real RSC rendering involves a streaming runtime + a flight payload format\n// that requires `vitest-environment-rsc` or a Next.js dev server. kiwa takes\n// a lighter approach: treat the async server component as `async (props) =>\n// JSX.Element` and await it directly. The returned React element tree is\n// inspected with a small finder API instead of a full DOM.\n//\n// Out of scope:\n// - flight payload serialization\n// - client component boundary (`'use client'`)\n// - suspense streaming (synchronous resolution only)\n// - dangerous HTML rendering — tests assert on element shape, not strings\n\nexport const NOT_FOUND_SYMBOL = Symbol.for('kiwa.next.rsc.notFound');\nexport const FORBIDDEN_SYMBOL = Symbol.for('kiwa.next.rsc.forbidden');\nexport const RSC_REDIRECT_SYMBOL = Symbol.for('kiwa.next.rsc.redirect');\n\nexport interface NotFoundSignal {\n readonly [NOT_FOUND_SYMBOL]: true;\n}\nexport interface ForbiddenSignal {\n readonly [FORBIDDEN_SYMBOL]: true;\n}\nexport interface RscRedirectSignal {\n readonly [RSC_REDIRECT_SYMBOL]: true;\n readonly url: string;\n readonly type: 'replace' | 'push';\n}\n\nexport type RscSignal = NotFoundSignal | ForbiddenSignal | RscRedirectSignal;\n\nexport interface RscElement {\n readonly type: string | symbol | ((props: Record<string, unknown>) => unknown);\n readonly props: Record<string, unknown>;\n readonly key: string | null;\n}\n\nexport type RscNode = RscElement | string | number | boolean | null | undefined | RscNode[];\n\nexport interface RenderServerComponentOptions<TProps> {\n readonly component: (props: TProps) => Promise<RscNode> | RscNode;\n readonly props?: TProps;\n}\n\nexport interface RenderServerComponentResult {\n readonly tree: RscNode;\n readonly signal: RscSignal | null;\n readonly error: unknown;\n}\n\nfunction isRscElement(node: unknown): node is RscElement {\n if (typeof node !== 'object' || node === null) return false;\n const candidate = node as { type?: unknown; props?: unknown };\n return (\n typeof candidate.type !== 'undefined' &&\n typeof candidate.props === 'object' &&\n candidate.props !== null\n );\n}\n\nfunction isNotFound(value: unknown): value is NotFoundSignal {\n return typeof value === 'object' && value !== null && (value as { [NOT_FOUND_SYMBOL]?: true })[NOT_FOUND_SYMBOL] === true;\n}\nfunction isForbidden(value: unknown): value is ForbiddenSignal {\n return typeof value === 'object' && value !== null && (value as { [FORBIDDEN_SYMBOL]?: true })[FORBIDDEN_SYMBOL] === true;\n}\nfunction isRscRedirect(value: unknown): value is RscRedirectSignal {\n return typeof value === 'object' && value !== null && (value as { [RSC_REDIRECT_SYMBOL]?: true })[RSC_REDIRECT_SYMBOL] === true;\n}\n\n/**\n * Recursively walk an RSC tree and collect every node that satisfies the\n * predicate. Children are read from `props.children` and are normalized to a\n * flat array regardless of how the component spelled them.\n */\nexport function findAll(tree: RscNode, predicate: (node: RscElement) => boolean): RscElement[] {\n const out: RscElement[] = [];\n function visit(node: RscNode): void {\n if (Array.isArray(node)) {\n node.forEach(visit);\n return;\n }\n if (!isRscElement(node)) return;\n if (predicate(node)) out.push(node);\n const children = node.props.children;\n if (typeof children !== 'undefined') {\n visit(children as RscNode);\n }\n }\n visit(tree);\n return out;\n}\n\n/**\n * Concatenate every string/number leaf of an RSC tree, joined by a single\n * space. Useful for `expect(textContent(tree)).toContain('hello')` style\n * assertions where the exact element structure does not matter.\n */\nexport function textContent(tree: RscNode): string {\n const parts: string[] = [];\n function visit(node: RscNode): void {\n if (Array.isArray(node)) {\n node.forEach(visit);\n return;\n }\n if (typeof node === 'string' || typeof node === 'number') {\n parts.push(String(node));\n return;\n }\n if (isRscElement(node)) {\n const children = node.props.children;\n if (typeof children !== 'undefined') visit(children as RscNode);\n }\n }\n visit(tree);\n return parts.filter((p) => p.length > 0).join(' ');\n}\n\n/**\n * Invoke an async server component in isolation and capture its return tree.\n * Throws of `notFound() / forbidden() / redirect()` from `next/navigation`\n * should be replaced with the kiwa signals below (Pattern A from the\n * server-action seam doc); the helper normalizes them into `result.signal`\n * instead of leaving them as `result.error`.\n */\nexport async function renderServerComponent<TProps = Record<string, unknown>>(\n opts: RenderServerComponentOptions<TProps>,\n): Promise<RenderServerComponentResult> {\n let tree: RscNode = null;\n let signal: RscSignal | null = null;\n let error: unknown;\n const props = (opts.props ?? ({} as TProps)) as TProps;\n try {\n const result = await opts.component(props);\n tree = result;\n } catch (caught) {\n if (isNotFound(caught) || isForbidden(caught) || isRscRedirect(caught)) {\n signal = caught;\n } else {\n error = caught;\n }\n }\n return { tree, signal, error };\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts DELETED
@@ -1,174 +0,0 @@
1
- declare const REDIRECT_SYMBOL: unique symbol;
2
- interface RedirectSignal {
3
- readonly [REDIRECT_SYMBOL]: true;
4
- readonly url: string;
5
- readonly type: 'replace' | 'push';
6
- }
7
- interface CookieJar {
8
- get(name: string): string | undefined;
9
- set(name: string, value: string, options?: Record<string, unknown>): void;
10
- delete(name: string): void;
11
- entries(): Array<[string, string]>;
12
- }
13
- interface ServerActionEnv {
14
- readonly cookies: CookieJar;
15
- readonly headers: Map<string, string>;
16
- readonly revalidated: {
17
- paths: string[];
18
- tags: string[];
19
- };
20
- readonly redirect: RedirectSignal | null;
21
- }
22
- type ServerActionFunction<TResult> = (...args: any[]) => Promise<TResult> | TResult;
23
- interface ServerActionInvocation<TResult> {
24
- /** The `'use server'` async function under test. */
25
- readonly action: ServerActionFunction<TResult>;
26
- /** Optional FormData first argument (default empty). */
27
- readonly formData?: FormData;
28
- /** Extra positional args appended after FormData (e.g. previous state for useFormState). */
29
- readonly args?: unknown[];
30
- /** Initial cookie jar entries (name → value). */
31
- readonly cookies?: Record<string, string>;
32
- /** Initial request headers (case-insensitive). */
33
- readonly headers?: Record<string, string>;
34
- }
35
- interface ServerActionResult<TResult> {
36
- /** Resolved return value (or `undefined` if the action threw a redirect signal). */
37
- readonly result: TResult | undefined;
38
- /** Error thrown by the action (excluding redirect signals which are normalized). */
39
- readonly error: unknown;
40
- /** Side-effects captured during the invocation. */
41
- readonly env: ServerActionEnv;
42
- }
43
- /**
44
- * Invoke a Next.js Server Action in isolation and capture its side-effects.
45
- *
46
- * The action is called as `await action(formData, ...args)`. The kiwa helper
47
- * does NOT monkey-patch global `next/navigation` / `next/headers` / `next/cache`
48
- * imports. Instead the action under test should accept its dependencies via
49
- * an injectable seam (a parameter or a module-level setter) so tests stay
50
- * deterministic. See `examples/nextjs-server-actions-poc/` for the pattern.
51
- */
52
- declare function invokeServerAction<TResult>(opts: ServerActionInvocation<TResult>): Promise<ServerActionResult<TResult>>;
53
-
54
- declare const MIDDLEWARE_ACTION_SYMBOL: unique symbol;
55
- type MiddlewareActionKind = 'next' | 'redirect' | 'rewrite' | 'json' | 'noop';
56
- interface MiddlewareAction {
57
- readonly [MIDDLEWARE_ACTION_SYMBOL]: true;
58
- readonly kind: MiddlewareActionKind;
59
- readonly url?: string;
60
- readonly body?: unknown;
61
- readonly status?: number;
62
- }
63
- interface MiddlewareRequest {
64
- readonly url: string;
65
- readonly method: string;
66
- readonly headers: ReadonlyMap<string, string>;
67
- readonly cookies: ReadonlyMap<string, string>;
68
- readonly nextUrl: {
69
- readonly pathname: string;
70
- readonly search: string;
71
- readonly searchParams: URLSearchParams;
72
- };
73
- readonly geo: {
74
- readonly country?: string;
75
- readonly region?: string;
76
- readonly city?: string;
77
- };
78
- }
79
- interface MiddlewareEnv {
80
- readonly responseHeaders: Map<string, string>;
81
- readonly responseCookies: Map<string, string>;
82
- readonly action: MiddlewareAction;
83
- }
84
- type MiddlewareFunction = (req: MiddlewareRequest, env: {
85
- setHeader: (name: string, value: string) => void;
86
- setCookie: (name: string, value: string) => void;
87
- }) => MiddlewareAction | Promise<MiddlewareAction>;
88
- interface InvokeMiddlewareOptions {
89
- readonly middleware: MiddlewareFunction;
90
- readonly url: string;
91
- readonly method?: string;
92
- readonly headers?: Record<string, string>;
93
- readonly cookies?: Record<string, string>;
94
- readonly geo?: {
95
- readonly country?: string;
96
- readonly region?: string;
97
- readonly city?: string;
98
- };
99
- }
100
- interface InvokeMiddlewareResult {
101
- readonly env: MiddlewareEnv;
102
- readonly error: unknown;
103
- }
104
- /**
105
- * Helpers your `middleware.ts` returns instead of constructing NextResponse
106
- * directly. Keep the production code shape close by re-exporting these from
107
- * a shared module; the helper expects the returned value to be a
108
- * MiddlewareAction shaped object.
109
- */
110
- declare const middlewareActions: {
111
- next(): MiddlewareAction;
112
- redirect(url: string, status?: number): MiddlewareAction;
113
- rewrite(url: string): MiddlewareAction;
114
- json(body: unknown, status?: number): MiddlewareAction;
115
- };
116
- /**
117
- * Invoke a middleware function in isolation and capture its outgoing
118
- * response shape + headers + cookies. Mirrors the kiwa style of
119
- * invokeServerAction: no globals, no real Next.js runtime.
120
- */
121
- declare function invokeMiddleware(opts: InvokeMiddlewareOptions): Promise<InvokeMiddlewareResult>;
122
-
123
- declare const NOT_FOUND_SYMBOL: unique symbol;
124
- declare const FORBIDDEN_SYMBOL: unique symbol;
125
- declare const RSC_REDIRECT_SYMBOL: unique symbol;
126
- interface NotFoundSignal {
127
- readonly [NOT_FOUND_SYMBOL]: true;
128
- }
129
- interface ForbiddenSignal {
130
- readonly [FORBIDDEN_SYMBOL]: true;
131
- }
132
- interface RscRedirectSignal {
133
- readonly [RSC_REDIRECT_SYMBOL]: true;
134
- readonly url: string;
135
- readonly type: 'replace' | 'push';
136
- }
137
- type RscSignal = NotFoundSignal | ForbiddenSignal | RscRedirectSignal;
138
- interface RscElement {
139
- readonly type: string | symbol | ((props: Record<string, unknown>) => unknown);
140
- readonly props: Record<string, unknown>;
141
- readonly key: string | null;
142
- }
143
- type RscNode = RscElement | string | number | boolean | null | undefined | RscNode[];
144
- interface RenderServerComponentOptions<TProps> {
145
- readonly component: (props: TProps) => Promise<RscNode> | RscNode;
146
- readonly props?: TProps;
147
- }
148
- interface RenderServerComponentResult {
149
- readonly tree: RscNode;
150
- readonly signal: RscSignal | null;
151
- readonly error: unknown;
152
- }
153
- /**
154
- * Recursively walk an RSC tree and collect every node that satisfies the
155
- * predicate. Children are read from `props.children` and are normalized to a
156
- * flat array regardless of how the component spelled them.
157
- */
158
- declare function findAll(tree: RscNode, predicate: (node: RscElement) => boolean): RscElement[];
159
- /**
160
- * Concatenate every string/number leaf of an RSC tree, joined by a single
161
- * space. Useful for `expect(textContent(tree)).toContain('hello')` style
162
- * assertions where the exact element structure does not matter.
163
- */
164
- declare function textContent(tree: RscNode): string;
165
- /**
166
- * Invoke an async server component in isolation and capture its return tree.
167
- * Throws of `notFound() / forbidden() / redirect()` from `next/navigation`
168
- * should be replaced with the kiwa signals below (Pattern A from the
169
- * server-action seam doc); the helper normalizes them into `result.signal`
170
- * instead of leaving them as `result.error`.
171
- */
172
- declare function renderServerComponent<TProps = Record<string, unknown>>(opts: RenderServerComponentOptions<TProps>): Promise<RenderServerComponentResult>;
173
-
174
- export { type CookieJar, FORBIDDEN_SYMBOL, type ForbiddenSignal, type InvokeMiddlewareOptions, type InvokeMiddlewareResult, MIDDLEWARE_ACTION_SYMBOL, type MiddlewareAction, type MiddlewareActionKind, type MiddlewareEnv, type MiddlewareFunction, type MiddlewareRequest, NOT_FOUND_SYMBOL, type NotFoundSignal, REDIRECT_SYMBOL, RSC_REDIRECT_SYMBOL, type RedirectSignal, type RenderServerComponentOptions, type RenderServerComponentResult, type RscElement, type RscNode, type RscRedirectSignal, type RscSignal, type ServerActionEnv, type ServerActionFunction, type ServerActionInvocation, type ServerActionResult, findAll, invokeMiddleware, invokeServerAction, middlewareActions, renderServerComponent, textContent };
package/dist/index.d.ts DELETED
@@ -1,174 +0,0 @@
1
- declare const REDIRECT_SYMBOL: unique symbol;
2
- interface RedirectSignal {
3
- readonly [REDIRECT_SYMBOL]: true;
4
- readonly url: string;
5
- readonly type: 'replace' | 'push';
6
- }
7
- interface CookieJar {
8
- get(name: string): string | undefined;
9
- set(name: string, value: string, options?: Record<string, unknown>): void;
10
- delete(name: string): void;
11
- entries(): Array<[string, string]>;
12
- }
13
- interface ServerActionEnv {
14
- readonly cookies: CookieJar;
15
- readonly headers: Map<string, string>;
16
- readonly revalidated: {
17
- paths: string[];
18
- tags: string[];
19
- };
20
- readonly redirect: RedirectSignal | null;
21
- }
22
- type ServerActionFunction<TResult> = (...args: any[]) => Promise<TResult> | TResult;
23
- interface ServerActionInvocation<TResult> {
24
- /** The `'use server'` async function under test. */
25
- readonly action: ServerActionFunction<TResult>;
26
- /** Optional FormData first argument (default empty). */
27
- readonly formData?: FormData;
28
- /** Extra positional args appended after FormData (e.g. previous state for useFormState). */
29
- readonly args?: unknown[];
30
- /** Initial cookie jar entries (name → value). */
31
- readonly cookies?: Record<string, string>;
32
- /** Initial request headers (case-insensitive). */
33
- readonly headers?: Record<string, string>;
34
- }
35
- interface ServerActionResult<TResult> {
36
- /** Resolved return value (or `undefined` if the action threw a redirect signal). */
37
- readonly result: TResult | undefined;
38
- /** Error thrown by the action (excluding redirect signals which are normalized). */
39
- readonly error: unknown;
40
- /** Side-effects captured during the invocation. */
41
- readonly env: ServerActionEnv;
42
- }
43
- /**
44
- * Invoke a Next.js Server Action in isolation and capture its side-effects.
45
- *
46
- * The action is called as `await action(formData, ...args)`. The kiwa helper
47
- * does NOT monkey-patch global `next/navigation` / `next/headers` / `next/cache`
48
- * imports. Instead the action under test should accept its dependencies via
49
- * an injectable seam (a parameter or a module-level setter) so tests stay
50
- * deterministic. See `examples/nextjs-server-actions-poc/` for the pattern.
51
- */
52
- declare function invokeServerAction<TResult>(opts: ServerActionInvocation<TResult>): Promise<ServerActionResult<TResult>>;
53
-
54
- declare const MIDDLEWARE_ACTION_SYMBOL: unique symbol;
55
- type MiddlewareActionKind = 'next' | 'redirect' | 'rewrite' | 'json' | 'noop';
56
- interface MiddlewareAction {
57
- readonly [MIDDLEWARE_ACTION_SYMBOL]: true;
58
- readonly kind: MiddlewareActionKind;
59
- readonly url?: string;
60
- readonly body?: unknown;
61
- readonly status?: number;
62
- }
63
- interface MiddlewareRequest {
64
- readonly url: string;
65
- readonly method: string;
66
- readonly headers: ReadonlyMap<string, string>;
67
- readonly cookies: ReadonlyMap<string, string>;
68
- readonly nextUrl: {
69
- readonly pathname: string;
70
- readonly search: string;
71
- readonly searchParams: URLSearchParams;
72
- };
73
- readonly geo: {
74
- readonly country?: string;
75
- readonly region?: string;
76
- readonly city?: string;
77
- };
78
- }
79
- interface MiddlewareEnv {
80
- readonly responseHeaders: Map<string, string>;
81
- readonly responseCookies: Map<string, string>;
82
- readonly action: MiddlewareAction;
83
- }
84
- type MiddlewareFunction = (req: MiddlewareRequest, env: {
85
- setHeader: (name: string, value: string) => void;
86
- setCookie: (name: string, value: string) => void;
87
- }) => MiddlewareAction | Promise<MiddlewareAction>;
88
- interface InvokeMiddlewareOptions {
89
- readonly middleware: MiddlewareFunction;
90
- readonly url: string;
91
- readonly method?: string;
92
- readonly headers?: Record<string, string>;
93
- readonly cookies?: Record<string, string>;
94
- readonly geo?: {
95
- readonly country?: string;
96
- readonly region?: string;
97
- readonly city?: string;
98
- };
99
- }
100
- interface InvokeMiddlewareResult {
101
- readonly env: MiddlewareEnv;
102
- readonly error: unknown;
103
- }
104
- /**
105
- * Helpers your `middleware.ts` returns instead of constructing NextResponse
106
- * directly. Keep the production code shape close by re-exporting these from
107
- * a shared module; the helper expects the returned value to be a
108
- * MiddlewareAction shaped object.
109
- */
110
- declare const middlewareActions: {
111
- next(): MiddlewareAction;
112
- redirect(url: string, status?: number): MiddlewareAction;
113
- rewrite(url: string): MiddlewareAction;
114
- json(body: unknown, status?: number): MiddlewareAction;
115
- };
116
- /**
117
- * Invoke a middleware function in isolation and capture its outgoing
118
- * response shape + headers + cookies. Mirrors the kiwa style of
119
- * invokeServerAction: no globals, no real Next.js runtime.
120
- */
121
- declare function invokeMiddleware(opts: InvokeMiddlewareOptions): Promise<InvokeMiddlewareResult>;
122
-
123
- declare const NOT_FOUND_SYMBOL: unique symbol;
124
- declare const FORBIDDEN_SYMBOL: unique symbol;
125
- declare const RSC_REDIRECT_SYMBOL: unique symbol;
126
- interface NotFoundSignal {
127
- readonly [NOT_FOUND_SYMBOL]: true;
128
- }
129
- interface ForbiddenSignal {
130
- readonly [FORBIDDEN_SYMBOL]: true;
131
- }
132
- interface RscRedirectSignal {
133
- readonly [RSC_REDIRECT_SYMBOL]: true;
134
- readonly url: string;
135
- readonly type: 'replace' | 'push';
136
- }
137
- type RscSignal = NotFoundSignal | ForbiddenSignal | RscRedirectSignal;
138
- interface RscElement {
139
- readonly type: string | symbol | ((props: Record<string, unknown>) => unknown);
140
- readonly props: Record<string, unknown>;
141
- readonly key: string | null;
142
- }
143
- type RscNode = RscElement | string | number | boolean | null | undefined | RscNode[];
144
- interface RenderServerComponentOptions<TProps> {
145
- readonly component: (props: TProps) => Promise<RscNode> | RscNode;
146
- readonly props?: TProps;
147
- }
148
- interface RenderServerComponentResult {
149
- readonly tree: RscNode;
150
- readonly signal: RscSignal | null;
151
- readonly error: unknown;
152
- }
153
- /**
154
- * Recursively walk an RSC tree and collect every node that satisfies the
155
- * predicate. Children are read from `props.children` and are normalized to a
156
- * flat array regardless of how the component spelled them.
157
- */
158
- declare function findAll(tree: RscNode, predicate: (node: RscElement) => boolean): RscElement[];
159
- /**
160
- * Concatenate every string/number leaf of an RSC tree, joined by a single
161
- * space. Useful for `expect(textContent(tree)).toContain('hello')` style
162
- * assertions where the exact element structure does not matter.
163
- */
164
- declare function textContent(tree: RscNode): string;
165
- /**
166
- * Invoke an async server component in isolation and capture its return tree.
167
- * Throws of `notFound() / forbidden() / redirect()` from `next/navigation`
168
- * should be replaced with the kiwa signals below (Pattern A from the
169
- * server-action seam doc); the helper normalizes them into `result.signal`
170
- * instead of leaving them as `result.error`.
171
- */
172
- declare function renderServerComponent<TProps = Record<string, unknown>>(opts: RenderServerComponentOptions<TProps>): Promise<RenderServerComponentResult>;
173
-
174
- export { type CookieJar, FORBIDDEN_SYMBOL, type ForbiddenSignal, type InvokeMiddlewareOptions, type InvokeMiddlewareResult, MIDDLEWARE_ACTION_SYMBOL, type MiddlewareAction, type MiddlewareActionKind, type MiddlewareEnv, type MiddlewareFunction, type MiddlewareRequest, NOT_FOUND_SYMBOL, type NotFoundSignal, REDIRECT_SYMBOL, RSC_REDIRECT_SYMBOL, type RedirectSignal, type RenderServerComponentOptions, type RenderServerComponentResult, type RscElement, type RscNode, type RscRedirectSignal, type RscSignal, type ServerActionEnv, type ServerActionFunction, type ServerActionInvocation, type ServerActionResult, findAll, invokeMiddleware, invokeServerAction, middlewareActions, renderServerComponent, textContent };
package/dist/index.js DELETED
@@ -1,200 +0,0 @@
1
- // src/invoke-server-action.ts
2
- var REDIRECT_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.redirect");
3
- function createCookieJar(initial) {
4
- const store = new Map(Object.entries(initial));
5
- return {
6
- get(name) {
7
- return store.get(name);
8
- },
9
- set(name, value) {
10
- store.set(name, value);
11
- },
12
- delete(name) {
13
- store.delete(name);
14
- },
15
- entries() {
16
- return Array.from(store.entries());
17
- }
18
- };
19
- }
20
- function isRedirectSignal(value) {
21
- return typeof value === "object" && value !== null && value[REDIRECT_SYMBOL] === true;
22
- }
23
- async function invokeServerAction(opts) {
24
- const headers = /* @__PURE__ */ new Map();
25
- for (const [name, value] of Object.entries(opts.headers ?? {})) {
26
- headers.set(name.toLowerCase(), value);
27
- }
28
- const env = {
29
- cookies: createCookieJar(opts.cookies ?? {}),
30
- headers,
31
- revalidated: { paths: [], tags: [] },
32
- redirect: null
33
- };
34
- const callArgs = [opts.formData ?? new FormData(), ...opts.args ?? []];
35
- let result;
36
- let error;
37
- try {
38
- result = await opts.action(...callArgs);
39
- } catch (caught) {
40
- if (isRedirectSignal(caught)) {
41
- env.redirect = caught;
42
- } else {
43
- error = caught;
44
- }
45
- }
46
- return { result, error, env };
47
- }
48
-
49
- // src/invoke-middleware.ts
50
- var MIDDLEWARE_ACTION_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.middleware.action");
51
- function buildRequest(opts) {
52
- const url = new URL(opts.url);
53
- const headers = /* @__PURE__ */ new Map();
54
- for (const [name, value] of Object.entries(opts.headers ?? {})) {
55
- headers.set(name.toLowerCase(), value);
56
- }
57
- const cookies = new Map(Object.entries(opts.cookies ?? {}));
58
- return {
59
- url: opts.url,
60
- method: opts.method ?? "GET",
61
- headers,
62
- cookies,
63
- nextUrl: {
64
- pathname: url.pathname,
65
- search: url.search,
66
- searchParams: url.searchParams
67
- },
68
- geo: opts.geo ?? {}
69
- };
70
- }
71
- var middlewareActions = {
72
- next() {
73
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "next" };
74
- },
75
- redirect(url, status = 307) {
76
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "redirect", url, status };
77
- },
78
- rewrite(url) {
79
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "rewrite", url };
80
- },
81
- json(body, status = 200) {
82
- return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "json", body, status };
83
- }
84
- };
85
- async function invokeMiddleware(opts) {
86
- const req = buildRequest(opts);
87
- const responseHeaders = /* @__PURE__ */ new Map();
88
- const responseCookies = /* @__PURE__ */ new Map();
89
- const seam = {
90
- setHeader(name, value) {
91
- responseHeaders.set(name.toLowerCase(), value);
92
- },
93
- setCookie(name, value) {
94
- responseCookies.set(name, value);
95
- }
96
- };
97
- let action = middlewareActions.next();
98
- let error;
99
- try {
100
- const result = await opts.middleware(req, seam);
101
- action = result;
102
- } catch (caught) {
103
- error = caught;
104
- action = { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: "noop" };
105
- }
106
- return {
107
- env: {
108
- responseHeaders,
109
- responseCookies,
110
- action
111
- },
112
- error
113
- };
114
- }
115
-
116
- // src/render-server-component.ts
117
- var NOT_FOUND_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.rsc.notFound");
118
- var FORBIDDEN_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.rsc.forbidden");
119
- var RSC_REDIRECT_SYMBOL = /* @__PURE__ */ Symbol.for("kiwa.next.rsc.redirect");
120
- function isRscElement(node) {
121
- if (typeof node !== "object" || node === null) return false;
122
- const candidate = node;
123
- return typeof candidate.type !== "undefined" && typeof candidate.props === "object" && candidate.props !== null;
124
- }
125
- function isNotFound(value) {
126
- return typeof value === "object" && value !== null && value[NOT_FOUND_SYMBOL] === true;
127
- }
128
- function isForbidden(value) {
129
- return typeof value === "object" && value !== null && value[FORBIDDEN_SYMBOL] === true;
130
- }
131
- function isRscRedirect(value) {
132
- return typeof value === "object" && value !== null && value[RSC_REDIRECT_SYMBOL] === true;
133
- }
134
- function findAll(tree, predicate) {
135
- const out = [];
136
- function visit(node) {
137
- if (Array.isArray(node)) {
138
- node.forEach(visit);
139
- return;
140
- }
141
- if (!isRscElement(node)) return;
142
- if (predicate(node)) out.push(node);
143
- const children = node.props.children;
144
- if (typeof children !== "undefined") {
145
- visit(children);
146
- }
147
- }
148
- visit(tree);
149
- return out;
150
- }
151
- function textContent(tree) {
152
- const parts = [];
153
- function visit(node) {
154
- if (Array.isArray(node)) {
155
- node.forEach(visit);
156
- return;
157
- }
158
- if (typeof node === "string" || typeof node === "number") {
159
- parts.push(String(node));
160
- return;
161
- }
162
- if (isRscElement(node)) {
163
- const children = node.props.children;
164
- if (typeof children !== "undefined") visit(children);
165
- }
166
- }
167
- visit(tree);
168
- return parts.filter((p) => p.length > 0).join(" ");
169
- }
170
- async function renderServerComponent(opts) {
171
- let tree = null;
172
- let signal = null;
173
- let error;
174
- const props = opts.props ?? {};
175
- try {
176
- const result = await opts.component(props);
177
- tree = result;
178
- } catch (caught) {
179
- if (isNotFound(caught) || isForbidden(caught) || isRscRedirect(caught)) {
180
- signal = caught;
181
- } else {
182
- error = caught;
183
- }
184
- }
185
- return { tree, signal, error };
186
- }
187
- export {
188
- FORBIDDEN_SYMBOL,
189
- MIDDLEWARE_ACTION_SYMBOL,
190
- NOT_FOUND_SYMBOL,
191
- REDIRECT_SYMBOL,
192
- RSC_REDIRECT_SYMBOL,
193
- findAll,
194
- invokeMiddleware,
195
- invokeServerAction,
196
- middlewareActions,
197
- renderServerComponent,
198
- textContent
199
- };
200
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/invoke-server-action.ts","../src/invoke-middleware.ts","../src/render-server-component.ts"],"sourcesContent":["// Next.js Server Action invocation helper for kiwa tests.\n//\n// Wraps an `'use server'` async function and captures Next.js side-effects\n// (redirect / revalidatePath / cookies set) without requiring a real Next.js\n// runtime. The test calls `invokeServerAction({ action, formData, ... })`\n// and asserts on the returned `ServerActionResult` instead of relying on\n// integration-level behavior.\n//\n// Out of scope on purpose:\n// - real `next/server` middleware semantics (use #495 helper instead)\n// - RSC payload format (use #494 helper instead)\n// - rendering a page after the action (use Playwright + `/kiwa-e2e` for that)\n\nexport const REDIRECT_SYMBOL = Symbol.for('kiwa.next.redirect');\n\nexport interface RedirectSignal {\n readonly [REDIRECT_SYMBOL]: true;\n readonly url: string;\n readonly type: 'replace' | 'push';\n}\n\nexport interface CookieJar {\n get(name: string): string | undefined;\n set(name: string, value: string, options?: Record<string, unknown>): void;\n delete(name: string): void;\n entries(): Array<[string, string]>;\n}\n\nexport interface ServerActionEnv {\n readonly cookies: CookieJar;\n readonly headers: Map<string, string>;\n readonly revalidated: { paths: string[]; tags: string[] };\n readonly redirect: RedirectSignal | null;\n}\n\n// We deliberately accept `any[]` here so callers can pass typed actions like\n// `(fd: FormData) => Promise<T>` or `(prev: State, fd: FormData) => Promise<T>`\n// without contortions. The runtime always invokes `action(formData, ...args)`.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type ServerActionFunction<TResult> = (...args: any[]) => Promise<TResult> | TResult;\n\nexport interface ServerActionInvocation<TResult> {\n /** The `'use server'` async function under test. */\n readonly action: ServerActionFunction<TResult>;\n /** Optional FormData first argument (default empty). */\n readonly formData?: FormData;\n /** Extra positional args appended after FormData (e.g. previous state for useFormState). */\n readonly args?: unknown[];\n /** Initial cookie jar entries (name → value). */\n readonly cookies?: Record<string, string>;\n /** Initial request headers (case-insensitive). */\n readonly headers?: Record<string, string>;\n}\n\nexport interface ServerActionResult<TResult> {\n /** Resolved return value (or `undefined` if the action threw a redirect signal). */\n readonly result: TResult | undefined;\n /** Error thrown by the action (excluding redirect signals which are normalized). */\n readonly error: unknown;\n /** Side-effects captured during the invocation. */\n readonly env: ServerActionEnv;\n}\n\nfunction createCookieJar(initial: Record<string, string>): CookieJar {\n const store = new Map<string, string>(Object.entries(initial));\n return {\n get(name) {\n return store.get(name);\n },\n set(name, value) {\n store.set(name, value);\n },\n delete(name) {\n store.delete(name);\n },\n entries() {\n return Array.from(store.entries());\n },\n };\n}\n\nfunction isRedirectSignal(value: unknown): value is RedirectSignal {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { [REDIRECT_SYMBOL]?: true })[REDIRECT_SYMBOL] === true\n );\n}\n\n/**\n * Invoke a Next.js Server Action in isolation and capture its side-effects.\n *\n * The action is called as `await action(formData, ...args)`. The kiwa helper\n * does NOT monkey-patch global `next/navigation` / `next/headers` / `next/cache`\n * imports. Instead the action under test should accept its dependencies via\n * an injectable seam (a parameter or a module-level setter) so tests stay\n * deterministic. See `examples/nextjs-server-actions-poc/` for the pattern.\n */\nexport async function invokeServerAction<TResult>(\n opts: ServerActionInvocation<TResult>,\n): Promise<ServerActionResult<TResult>> {\n const headers = new Map<string, string>();\n for (const [name, value] of Object.entries(opts.headers ?? {})) {\n headers.set(name.toLowerCase(), value);\n }\n const env: ServerActionEnv = {\n cookies: createCookieJar(opts.cookies ?? {}),\n headers,\n revalidated: { paths: [], tags: [] },\n redirect: null,\n };\n const callArgs: unknown[] = [opts.formData ?? new FormData(), ...(opts.args ?? [])];\n let result: TResult | undefined;\n let error: unknown;\n try {\n result = await opts.action(...callArgs);\n } catch (caught) {\n if (isRedirectSignal(caught)) {\n (env as { redirect: RedirectSignal | null }).redirect = caught;\n } else {\n error = caught;\n }\n }\n return { result, error, env };\n}\n","// Next.js middleware.ts invocation helper for kiwa tests (Issue #495).\n//\n// Middleware in App Router is `(req: NextRequest) => NextResponse | Response | void`.\n// It can set response headers / cookies, throw redirect via NextResponse.redirect(),\n// rewrite via NextResponse.rewrite(), short-circuit with NextResponse.json(), or\n// pass through with NextResponse.next(). kiwa wraps the call so unit tests can\n// assert on the captured action + outgoing headers/cookies without a running\n// Next.js server.\n//\n// Out of scope on purpose:\n// - matcher config evaluation (the test already knows which middleware to call)\n// - revalidate / cache header semantics\n// - Edge runtime polyfill (use Node test environment + the helper's simulated request)\n\nexport const MIDDLEWARE_ACTION_SYMBOL = Symbol.for('kiwa.next.middleware.action');\n\nexport type MiddlewareActionKind = 'next' | 'redirect' | 'rewrite' | 'json' | 'noop';\n\nexport interface MiddlewareAction {\n readonly [MIDDLEWARE_ACTION_SYMBOL]: true;\n readonly kind: MiddlewareActionKind;\n readonly url?: string;\n readonly body?: unknown;\n readonly status?: number;\n}\n\nexport interface MiddlewareRequest {\n readonly url: string;\n readonly method: string;\n readonly headers: ReadonlyMap<string, string>;\n readonly cookies: ReadonlyMap<string, string>;\n readonly nextUrl: {\n readonly pathname: string;\n readonly search: string;\n readonly searchParams: URLSearchParams;\n };\n readonly geo: {\n readonly country?: string;\n readonly region?: string;\n readonly city?: string;\n };\n}\n\nexport interface MiddlewareEnv {\n readonly responseHeaders: Map<string, string>;\n readonly responseCookies: Map<string, string>;\n readonly action: MiddlewareAction;\n}\n\nexport type MiddlewareFunction = (\n req: MiddlewareRequest,\n env: { setHeader: (name: string, value: string) => void; setCookie: (name: string, value: string) => void },\n) => MiddlewareAction | Promise<MiddlewareAction>;\n\nexport interface InvokeMiddlewareOptions {\n readonly middleware: MiddlewareFunction;\n readonly url: string;\n readonly method?: string;\n readonly headers?: Record<string, string>;\n readonly cookies?: Record<string, string>;\n readonly geo?: {\n readonly country?: string;\n readonly region?: string;\n readonly city?: string;\n };\n}\n\nexport interface InvokeMiddlewareResult {\n readonly env: MiddlewareEnv;\n readonly error: unknown;\n}\n\nfunction buildRequest(opts: InvokeMiddlewareOptions): MiddlewareRequest {\n const url = new URL(opts.url);\n const headers = new Map<string, string>();\n for (const [name, value] of Object.entries(opts.headers ?? {})) {\n headers.set(name.toLowerCase(), value);\n }\n const cookies = new Map<string, string>(Object.entries(opts.cookies ?? {}));\n return {\n url: opts.url,\n method: opts.method ?? 'GET',\n headers,\n cookies,\n nextUrl: {\n pathname: url.pathname,\n search: url.search,\n searchParams: url.searchParams,\n },\n geo: opts.geo ?? {},\n };\n}\n\n/**\n * Helpers your `middleware.ts` returns instead of constructing NextResponse\n * directly. Keep the production code shape close by re-exporting these from\n * a shared module; the helper expects the returned value to be a\n * MiddlewareAction shaped object.\n */\nexport const middlewareActions = {\n next(): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'next' };\n },\n redirect(url: string, status = 307): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'redirect', url, status };\n },\n rewrite(url: string): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'rewrite', url };\n },\n json(body: unknown, status = 200): MiddlewareAction {\n return { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'json', body, status };\n },\n};\n\n/**\n * Invoke a middleware function in isolation and capture its outgoing\n * response shape + headers + cookies. Mirrors the kiwa style of\n * invokeServerAction: no globals, no real Next.js runtime.\n */\nexport async function invokeMiddleware(opts: InvokeMiddlewareOptions): Promise<InvokeMiddlewareResult> {\n const req = buildRequest(opts);\n const responseHeaders = new Map<string, string>();\n const responseCookies = new Map<string, string>();\n const seam = {\n setHeader(name: string, value: string) {\n responseHeaders.set(name.toLowerCase(), value);\n },\n setCookie(name: string, value: string) {\n responseCookies.set(name, value);\n },\n };\n let action: MiddlewareAction = middlewareActions.next();\n let error: unknown;\n try {\n const result = await opts.middleware(req, seam);\n action = result;\n } catch (caught) {\n error = caught;\n action = { [MIDDLEWARE_ACTION_SYMBOL]: true, kind: 'noop' };\n }\n return {\n env: {\n responseHeaders,\n responseCookies,\n action,\n },\n error,\n };\n}\n","// Next.js async React Server Component (RSC) test helper for kiwa (Issue #494).\n//\n// Real RSC rendering involves a streaming runtime + a flight payload format\n// that requires `vitest-environment-rsc` or a Next.js dev server. kiwa takes\n// a lighter approach: treat the async server component as `async (props) =>\n// JSX.Element` and await it directly. The returned React element tree is\n// inspected with a small finder API instead of a full DOM.\n//\n// Out of scope:\n// - flight payload serialization\n// - client component boundary (`'use client'`)\n// - suspense streaming (synchronous resolution only)\n// - dangerous HTML rendering — tests assert on element shape, not strings\n\nexport const NOT_FOUND_SYMBOL = Symbol.for('kiwa.next.rsc.notFound');\nexport const FORBIDDEN_SYMBOL = Symbol.for('kiwa.next.rsc.forbidden');\nexport const RSC_REDIRECT_SYMBOL = Symbol.for('kiwa.next.rsc.redirect');\n\nexport interface NotFoundSignal {\n readonly [NOT_FOUND_SYMBOL]: true;\n}\nexport interface ForbiddenSignal {\n readonly [FORBIDDEN_SYMBOL]: true;\n}\nexport interface RscRedirectSignal {\n readonly [RSC_REDIRECT_SYMBOL]: true;\n readonly url: string;\n readonly type: 'replace' | 'push';\n}\n\nexport type RscSignal = NotFoundSignal | ForbiddenSignal | RscRedirectSignal;\n\nexport interface RscElement {\n readonly type: string | symbol | ((props: Record<string, unknown>) => unknown);\n readonly props: Record<string, unknown>;\n readonly key: string | null;\n}\n\nexport type RscNode = RscElement | string | number | boolean | null | undefined | RscNode[];\n\nexport interface RenderServerComponentOptions<TProps> {\n readonly component: (props: TProps) => Promise<RscNode> | RscNode;\n readonly props?: TProps;\n}\n\nexport interface RenderServerComponentResult {\n readonly tree: RscNode;\n readonly signal: RscSignal | null;\n readonly error: unknown;\n}\n\nfunction isRscElement(node: unknown): node is RscElement {\n if (typeof node !== 'object' || node === null) return false;\n const candidate = node as { type?: unknown; props?: unknown };\n return (\n typeof candidate.type !== 'undefined' &&\n typeof candidate.props === 'object' &&\n candidate.props !== null\n );\n}\n\nfunction isNotFound(value: unknown): value is NotFoundSignal {\n return typeof value === 'object' && value !== null && (value as { [NOT_FOUND_SYMBOL]?: true })[NOT_FOUND_SYMBOL] === true;\n}\nfunction isForbidden(value: unknown): value is ForbiddenSignal {\n return typeof value === 'object' && value !== null && (value as { [FORBIDDEN_SYMBOL]?: true })[FORBIDDEN_SYMBOL] === true;\n}\nfunction isRscRedirect(value: unknown): value is RscRedirectSignal {\n return typeof value === 'object' && value !== null && (value as { [RSC_REDIRECT_SYMBOL]?: true })[RSC_REDIRECT_SYMBOL] === true;\n}\n\n/**\n * Recursively walk an RSC tree and collect every node that satisfies the\n * predicate. Children are read from `props.children` and are normalized to a\n * flat array regardless of how the component spelled them.\n */\nexport function findAll(tree: RscNode, predicate: (node: RscElement) => boolean): RscElement[] {\n const out: RscElement[] = [];\n function visit(node: RscNode): void {\n if (Array.isArray(node)) {\n node.forEach(visit);\n return;\n }\n if (!isRscElement(node)) return;\n if (predicate(node)) out.push(node);\n const children = node.props.children;\n if (typeof children !== 'undefined') {\n visit(children as RscNode);\n }\n }\n visit(tree);\n return out;\n}\n\n/**\n * Concatenate every string/number leaf of an RSC tree, joined by a single\n * space. Useful for `expect(textContent(tree)).toContain('hello')` style\n * assertions where the exact element structure does not matter.\n */\nexport function textContent(tree: RscNode): string {\n const parts: string[] = [];\n function visit(node: RscNode): void {\n if (Array.isArray(node)) {\n node.forEach(visit);\n return;\n }\n if (typeof node === 'string' || typeof node === 'number') {\n parts.push(String(node));\n return;\n }\n if (isRscElement(node)) {\n const children = node.props.children;\n if (typeof children !== 'undefined') visit(children as RscNode);\n }\n }\n visit(tree);\n return parts.filter((p) => p.length > 0).join(' ');\n}\n\n/**\n * Invoke an async server component in isolation and capture its return tree.\n * Throws of `notFound() / forbidden() / redirect()` from `next/navigation`\n * should be replaced with the kiwa signals below (Pattern A from the\n * server-action seam doc); the helper normalizes them into `result.signal`\n * instead of leaving them as `result.error`.\n */\nexport async function renderServerComponent<TProps = Record<string, unknown>>(\n opts: RenderServerComponentOptions<TProps>,\n): Promise<RenderServerComponentResult> {\n let tree: RscNode = null;\n let signal: RscSignal | null = null;\n let error: unknown;\n const props = (opts.props ?? ({} as TProps)) as TProps;\n try {\n const result = await opts.component(props);\n tree = result;\n } catch (caught) {\n if (isNotFound(caught) || isForbidden(caught) || isRscRedirect(caught)) {\n signal = caught;\n } else {\n error = caught;\n }\n }\n return { tree, signal, error };\n}\n"],"mappings":"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","names":[]}