@rangojs/router 0.22.1 → 0.24.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/dist/bin/rango.js +10 -4
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +39 -8
- package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +24 -10
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +22 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +15 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +31 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +52 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +4 -4
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +40 -1
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +14 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +974 -379
- package/package.json +1 -1
- package/skills/caching/SKILL.md +54 -5
- package/skills/catalog.json +3 -3
- package/skills/client-urls/SKILL.md +197 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +16 -3
- package/skills/hooks/data.md +3 -1
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +199 -2
- package/skills/middleware/SKILL.md +183 -2
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +22 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +13 -7
- package/skills/route/SKILL.md +139 -0
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +2 -1
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +107 -19
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/use-cache/SKILL.md +4 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +46 -32
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +82 -16
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +554 -146
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +88 -49
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +25 -63
- package/src/browser/types.ts +24 -10
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +45 -11
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +21 -107
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/context-var.ts +32 -2
- package/src/errors.ts +34 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +148 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +35 -16
- package/src/router/internal-params.ts +16 -0
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +85 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +16 -4
- package/src/router/match-middleware/cache-lookup.ts +35 -7
- package/src/router/match-middleware/cache-store.ts +15 -1
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/match-result.ts +14 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
- package/src/router/segment-resolution/revalidation.ts +215 -82
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +54 -9
- package/src/rsc/shell-capture.ts +2 -1
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +152 -25
- package/src/server/request-context.ts +175 -64
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/action-form.ts +2 -11
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +7 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +4 -1
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +131 -8
- package/src/testing/vitest.ts +67 -1
- package/src/thenable-status.ts +11 -0
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/request-scope.ts +8 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +28 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +116 -53
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
package/src/ssr/index.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import React from "react";
|
|
2
3
|
import { createSsrRootComponent, deserializeSsrPayload } from "./ssr-root.js";
|
|
3
4
|
import { injectRSCPayloadEager } from "./inject-rsc-eager.js";
|
|
@@ -263,23 +264,40 @@ export interface SSRDependencies<TEnv = unknown> {
|
|
|
263
264
|
*/
|
|
264
265
|
const activeCaptureClientLoads = new Set<Set<PromiseLike<unknown>>>();
|
|
265
266
|
|
|
266
|
-
/**
|
|
267
|
+
/**
|
|
268
|
+
* Dev only: the client-reference loads requested by the Flight decode of the
|
|
269
|
+
* document render whose context this is (see settleDocumentClientLoads).
|
|
270
|
+
*
|
|
271
|
+
* Gated on `process.env.NODE_ENV`, the router's dev signal, not on the
|
|
272
|
+
* `import.meta.env.DEV` that adds plugin-rsc's cache tag: a build folds it
|
|
273
|
+
* (the app's NODE_ENV define, or the Cloudflare plugin), so the hold is
|
|
274
|
+
* compiled out. A server where the two disagree loses nothing: a dev server
|
|
275
|
+
* run with NODE_ENV=production streams as it did before the hold, and an
|
|
276
|
+
* unfolded production server collects only loads still pending, the first
|
|
277
|
+
* load of each module, bounded like any other.
|
|
278
|
+
*/
|
|
279
|
+
const documentClientLoads =
|
|
280
|
+
process.env.NODE_ENV !== "production"
|
|
281
|
+
? new AsyncLocalStorage<Set<PromiseLike<unknown>>>()
|
|
282
|
+
: undefined;
|
|
283
|
+
|
|
284
|
+
/** Bound on settleDocumentClientLoads; a later load flushes as before. */
|
|
285
|
+
const DEV_DOCUMENT_CLIENT_LOAD_WAIT_MS = 5_000;
|
|
286
|
+
|
|
287
|
+
/** The loader wrapper {@link installClientLoadRecorder} last installed. */
|
|
267
288
|
let recordingClientReferenceLoader: ((id: string) => unknown) | undefined;
|
|
268
289
|
|
|
269
290
|
/**
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
291
|
+
* Wrap plugin-rsc's SSR loader (`globalThis.__vite_rsc_client_require__`,
|
|
292
|
+
* which `__vite_rsc_require__` reads per call) to collect pending loads into
|
|
293
|
+
* every running capture's set and the current dev document's set. Installed
|
|
294
|
+
* on the first capture or dev document, so a production isolate that never
|
|
295
|
+
* captures keeps the bare loader. Wraps again when another wrapper replaced
|
|
296
|
+
* this one (the build shell phase bridges hashed ids the same way,
|
|
276
297
|
* vite/discovery/shell-prerender-phase.ts). Collects nothing without the
|
|
277
298
|
* loader.
|
|
278
299
|
*/
|
|
279
|
-
function
|
|
280
|
-
loads: Set<PromiseLike<unknown>>;
|
|
281
|
-
stop: () => void;
|
|
282
|
-
} {
|
|
300
|
+
function installClientLoadRecorder(): void {
|
|
283
301
|
const g = globalThis as {
|
|
284
302
|
__vite_rsc_client_require__?: (id: string) => unknown;
|
|
285
303
|
};
|
|
@@ -287,24 +305,104 @@ function captureClientLoads(): {
|
|
|
287
305
|
if (typeof load === "function" && load !== recordingClientReferenceLoader) {
|
|
288
306
|
recordingClientReferenceLoader = (id) => {
|
|
289
307
|
const loading = load(id);
|
|
308
|
+
const documentLoads = documentClientLoads?.getStore();
|
|
290
309
|
// plugin-rsc memoizes one promise per id and the Flight client stamps
|
|
291
310
|
// status "fulfilled" on it once loaded: a warm module is not collected.
|
|
292
311
|
if (
|
|
293
|
-
activeCaptureClientLoads.size > 0 &&
|
|
312
|
+
(activeCaptureClientLoads.size > 0 || documentLoads) &&
|
|
294
313
|
isThenable(loading) &&
|
|
295
314
|
(loading as { status?: string }).status !== "fulfilled"
|
|
296
315
|
) {
|
|
297
316
|
for (const loads of activeCaptureClientLoads) loads.add(loading);
|
|
317
|
+
if (documentLoads) {
|
|
318
|
+
// The document's set holds only loads still pending.
|
|
319
|
+
documentLoads.add(loading);
|
|
320
|
+
const drop = (): void => {
|
|
321
|
+
documentLoads.delete(loading);
|
|
322
|
+
};
|
|
323
|
+
loading.then(drop, drop);
|
|
324
|
+
}
|
|
298
325
|
}
|
|
299
326
|
return loading;
|
|
300
327
|
};
|
|
301
328
|
g.__vite_rsc_client_require__ = recordingClientReferenceLoader;
|
|
302
329
|
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Start collecting the client-reference loads requested during a capture;
|
|
334
|
+
* call the returned `stop` when the capture ends.
|
|
335
|
+
*/
|
|
336
|
+
function captureClientLoads(): {
|
|
337
|
+
loads: Set<PromiseLike<unknown>>;
|
|
338
|
+
stop: () => void;
|
|
339
|
+
} {
|
|
340
|
+
installClientLoadRecorder();
|
|
303
341
|
const loads = new Set<PromiseLike<unknown>>();
|
|
304
342
|
activeCaptureClientLoads.add(loads);
|
|
305
343
|
return { loads, stop: () => activeCaptureClientLoads.delete(loads) };
|
|
306
344
|
}
|
|
307
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Dev only: when the shell is ready, hold the first flush until the
|
|
348
|
+
* client-reference loads still pending at that moment have settled, so a
|
|
349
|
+
* boundary waiting on one completes in place, as in a build (#1106).
|
|
350
|
+
*
|
|
351
|
+
* plugin-rsc tags client reference ids with a per-render cache tag in dev
|
|
352
|
+
* (createClientManifest in @vitejs/plugin-rsc core/rsc.js), so its SSR loader
|
|
353
|
+
* memo misses on every request and each reference loads through the module
|
|
354
|
+
* runner again: an RPC per import on workerd. A client component a loader's
|
|
355
|
+
* answer draws under an inline <Suspense> resolved after the shell flushed,
|
|
356
|
+
* and the document streamed the fallback, which a browser without JavaScript
|
|
357
|
+
* never replaces. A build loads each reference once per isolate and reads it
|
|
358
|
+
* synchronously after that.
|
|
359
|
+
*
|
|
360
|
+
* Only that snapshot is waited for: a load starts when the row naming it
|
|
361
|
+
* arrives, so its data is already in the payload. A load requested later
|
|
362
|
+
* belongs to data that arrives after the shell, which streams as in a build,
|
|
363
|
+
* and nothing pending means no wait at all: the router adds no wait of its
|
|
364
|
+
* own, so a loading() skeleton or an inline fallback still streams.
|
|
365
|
+
*/
|
|
366
|
+
async function settleDocumentClientLoads(
|
|
367
|
+
pending: Set<PromiseLike<unknown>>,
|
|
368
|
+
): Promise<void> {
|
|
369
|
+
const snapshot = [...pending];
|
|
370
|
+
if (snapshot.length === 0) return;
|
|
371
|
+
const deadline = createCancelableTimeout(DEV_DOCUMENT_CLIENT_LOAD_WAIT_MS);
|
|
372
|
+
try {
|
|
373
|
+
const expired = await Promise.race([
|
|
374
|
+
Promise.allSettled(snapshot).then(() => false),
|
|
375
|
+
deadline.promise.then(() => true),
|
|
376
|
+
]);
|
|
377
|
+
if (expired) return;
|
|
378
|
+
// A settled load pings the Fizz task that waited on it, which retries on
|
|
379
|
+
// a setTimeout(0) turn (pingTask).
|
|
380
|
+
await macrotask();
|
|
381
|
+
await macrotask();
|
|
382
|
+
} finally {
|
|
383
|
+
deadline.cancel();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Run a document's Flight decode. In dev it runs in its own client-load set,
|
|
389
|
+
* and `settle` holds the first flush until the loads pending when the shell
|
|
390
|
+
* is ready have settled (settleDocumentClientLoads); a build gets the decode
|
|
391
|
+
* alone.
|
|
392
|
+
*/
|
|
393
|
+
function trackDocumentClientLoads<T>(decode: () => T): {
|
|
394
|
+
value: T;
|
|
395
|
+
settle?: () => Promise<void>;
|
|
396
|
+
} {
|
|
397
|
+
if (!documentClientLoads) return { value: decode() };
|
|
398
|
+
installClientLoadRecorder();
|
|
399
|
+
const loads = new Set<PromiseLike<unknown>>();
|
|
400
|
+
return {
|
|
401
|
+
value: documentClientLoads.run(loads, decode),
|
|
402
|
+
settle: () => settleDocumentClientLoads(loads),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
308
406
|
/**
|
|
309
407
|
* Route an SSR error through the deps.onError notification callback with the
|
|
310
408
|
* "rendering" phase. Swallows callback failures so a broken reporter never
|
|
@@ -708,10 +806,10 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
708
806
|
// The parallel no-op catch keeps a payload rejection off the
|
|
709
807
|
// unhandledRejection path if fizz errors before consuming the promise;
|
|
710
808
|
// SsrRoot's React.use() on the SAME promise still observes the rejection.
|
|
711
|
-
const payload =
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
809
|
+
const { value: payload, settle: settleClientLoads } =
|
|
810
|
+
trackDocumentClientLoads(() =>
|
|
811
|
+
deserializeSsrPayload(createFromReadableStream, rscStream1),
|
|
812
|
+
);
|
|
715
813
|
payload.catch(() => {});
|
|
716
814
|
|
|
717
815
|
const SsrRoot = createSsrRootComponent({
|
|
@@ -763,6 +861,10 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
763
861
|
}),
|
|
764
862
|
);
|
|
765
863
|
|
|
864
|
+
// Dev only: the client-reference loads the decode requested settle
|
|
865
|
+
// before the first flush (trackDocumentClientLoads).
|
|
866
|
+
if (settleClientLoads) await settleClientLoads();
|
|
867
|
+
|
|
766
868
|
// Wait for all Suspense boundaries to resolve when streamMode is "allReady".
|
|
767
869
|
// This buffers the entire HTML before flushing — used for bots that
|
|
768
870
|
// cannot process streamed HTML.
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* Post the result with `serveShellRequest(router, url, { form })`.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
import { registerServerModule } from "./internal/server-modules.js";
|
|
15
|
+
import { registerTestAction } from "./internal/test-action.js";
|
|
17
16
|
|
|
18
17
|
/** Options for {@link createActionForm}. */
|
|
19
18
|
export interface CreateActionFormOptions {
|
|
@@ -41,15 +40,7 @@ export function createActionForm(
|
|
|
41
40
|
options: CreateActionFormOptions,
|
|
42
41
|
): FormData {
|
|
43
42
|
const { id, useActionState = true, fields = {} } = options;
|
|
44
|
-
|
|
45
|
-
if (!moduleId || !name) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`createActionForm: id must be "<module id>#<export name>", received ${JSON.stringify(id)}`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
registerServerModule(moduleId, {
|
|
51
|
-
[name]: RSDServer.registerServerReference(action, moduleId, name),
|
|
52
|
-
});
|
|
43
|
+
registerTestAction(action, id, "createActionForm");
|
|
53
44
|
|
|
54
45
|
const form = new FormData();
|
|
55
46
|
if (useActionState) {
|
package/src/testing/dispatch.ts
CHANGED
|
@@ -106,16 +106,15 @@ import {
|
|
|
106
106
|
runWithRequestContext,
|
|
107
107
|
setRequestContextParams,
|
|
108
108
|
} from "../server/request-context.js";
|
|
109
|
+
import { REWRITE_HEADER, runRewritePasses } from "../rsc/rewrite-passes.js";
|
|
110
|
+
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
109
111
|
import { resolveRouterVersions } from "../server/build-version-table.js";
|
|
110
112
|
import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
|
|
111
113
|
import type {
|
|
112
114
|
MiddlewareEntry,
|
|
113
115
|
MiddlewareFn,
|
|
114
116
|
} from "../router/middleware-types.js";
|
|
115
|
-
import {
|
|
116
|
-
createReverseFunction,
|
|
117
|
-
stripInternalParams,
|
|
118
|
-
} from "../router/handler-context.js";
|
|
117
|
+
import { createReverseFunction } from "../router/handler-context.js";
|
|
119
118
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
120
119
|
import {
|
|
121
120
|
compileSearchParamsFilter,
|
|
@@ -135,6 +134,7 @@ import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
|
135
134
|
import { RouterError } from "../errors.js";
|
|
136
135
|
import { createProblemDetails } from "../rsc/response-error.js";
|
|
137
136
|
import {
|
|
137
|
+
createReloadResponse,
|
|
138
138
|
createResponseWithMergedHeaders,
|
|
139
139
|
createSimpleRedirectResponse,
|
|
140
140
|
finalizeResponse,
|
|
@@ -143,7 +143,7 @@ import {
|
|
|
143
143
|
} from "../rsc/helpers.js";
|
|
144
144
|
import { guardOutgoingRedirect } from "../rsc/redirect-guard.js";
|
|
145
145
|
import { stringifyJsonRouteResult } from "../rsc/json-route-result.js";
|
|
146
|
-
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
146
|
+
import { isWebSocketUpgradeResponse, trySetHeader } from "../response-utils.js";
|
|
147
147
|
import { invokeOnError } from "../router/error-handling.js";
|
|
148
148
|
import type { OnErrorCallback } from "../types/error-types.js";
|
|
149
149
|
import type { Rango } from "../router/router-interfaces.js";
|
|
@@ -321,6 +321,68 @@ function serializeResponseRouteError(
|
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
/** What one pass of a dispatch matched (the visitor's URL, or a rewrite target). */
|
|
325
|
+
interface DispatchPass<TEnv> {
|
|
326
|
+
req: Request;
|
|
327
|
+
url: URL;
|
|
328
|
+
redirectTo?: string;
|
|
329
|
+
isUnmatched: boolean;
|
|
330
|
+
preview: Awaited<ReturnType<DispatchableRouter<TEnv>["previewMatch"]>>;
|
|
331
|
+
responseType?: string;
|
|
332
|
+
handler?: Function;
|
|
333
|
+
params: Record<string, string>;
|
|
334
|
+
routeKey?: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function matchDispatchPass<TEnv>(
|
|
338
|
+
router: DispatchableRouter<TEnv>,
|
|
339
|
+
req: Request,
|
|
340
|
+
env: TEnv,
|
|
341
|
+
): Promise<DispatchPass<TEnv>> {
|
|
342
|
+
const url = new URL(req.url);
|
|
343
|
+
// findMatch carries trailing-slash/redirect targets and null on no match.
|
|
344
|
+
// previewMatch swallows redirects, so detect them here first.
|
|
345
|
+
const match = await router.findMatch(url.pathname);
|
|
346
|
+
const redirectTo = match?.redirectTo;
|
|
347
|
+
const isUnmatched = !match;
|
|
348
|
+
|
|
349
|
+
// previewMatch resolves responseType, the response-route handler, and the
|
|
350
|
+
// route middleware from the matched entry tree (with content negotiation).
|
|
351
|
+
// Skip it for a redirect/unmatched path — there is no response route to
|
|
352
|
+
// resolve, and previewMatch would return null / a redirect marker anyway.
|
|
353
|
+
const preview =
|
|
354
|
+
redirectTo || isUnmatched ? null : await router.previewMatch(req, { env });
|
|
355
|
+
|
|
356
|
+
// A bare match with no responseType is an RSC route. The RSC-route throw is a
|
|
357
|
+
// hard boundary of this primitive (no Flight runtime), distinct from the
|
|
358
|
+
// 308/404 outcomes, so it stays a pre-middleware guard.
|
|
359
|
+
const responseType = preview?.responseType;
|
|
360
|
+
const handler = preview?.handler;
|
|
361
|
+
if (
|
|
362
|
+
!redirectTo &&
|
|
363
|
+
!isUnmatched &&
|
|
364
|
+
(!responseType || typeof handler !== "function")
|
|
365
|
+
) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`dispatch() does not render RSC routes — the route matched at ` +
|
|
368
|
+
`"${url.pathname}" is a React Server Component route, not a response ` +
|
|
369
|
+
`route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
|
|
370
|
+
`e2e test to exercise component rendering.`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
req,
|
|
375
|
+
url,
|
|
376
|
+
redirectTo,
|
|
377
|
+
isUnmatched,
|
|
378
|
+
preview,
|
|
379
|
+
responseType,
|
|
380
|
+
handler,
|
|
381
|
+
params: preview?.params ?? match?.params ?? {},
|
|
382
|
+
routeKey: preview?.routeKey ?? match?.routeKey,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
324
386
|
/**
|
|
325
387
|
* Run a request through the router in-process and return the Response.
|
|
326
388
|
*
|
|
@@ -353,39 +415,10 @@ export async function dispatch<TEnv = any>(
|
|
|
353
415
|
setRouterManifest(routerId, router.routeMap as Record<string, string>);
|
|
354
416
|
}
|
|
355
417
|
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
const isUnmatched = !match;
|
|
361
|
-
|
|
362
|
-
// previewMatch resolves responseType, the response-route handler, and the
|
|
363
|
-
// route middleware from the matched entry tree (with content negotiation).
|
|
364
|
-
// Skip it for a redirect/unmatched path — there is no response route to
|
|
365
|
-
// resolve, and previewMatch would return null / a redirect marker anyway.
|
|
366
|
-
const preview =
|
|
367
|
-
redirectTo || isUnmatched ? null : await router.previewMatch(req, { env });
|
|
368
|
-
|
|
369
|
-
// A bare match with no responseType is an RSC route. The RSC-route throw is a
|
|
370
|
-
// hard boundary of this primitive (no Flight runtime), distinct from the
|
|
371
|
-
// 308/404 outcomes below, so it stays a pre-middleware guard.
|
|
372
|
-
const responseType = preview?.responseType;
|
|
373
|
-
const handler = preview?.handler;
|
|
374
|
-
const params = preview?.params ?? match?.params ?? {};
|
|
375
|
-
const routeKey = preview?.routeKey ?? match?.routeKey;
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
!redirectTo &&
|
|
379
|
-
!isUnmatched &&
|
|
380
|
-
(!responseType || typeof handler !== "function")
|
|
381
|
-
) {
|
|
382
|
-
throw new Error(
|
|
383
|
-
`dispatch() does not render RSC routes — the route matched at ` +
|
|
384
|
-
`"${url.pathname}" is a React Server Component route, not a response ` +
|
|
385
|
-
`route. Use renderHandler/renderServerTree/renderToFlightString or an ` +
|
|
386
|
-
`e2e test to exercise component rendering.`,
|
|
387
|
-
);
|
|
388
|
-
}
|
|
418
|
+
// The visitor's URL is matched before any middleware runs, so an RSC route
|
|
419
|
+
// fails here, before a middleware side effect. A rewrite target is matched
|
|
420
|
+
// the same way, at the start of its pass (below).
|
|
421
|
+
const firstPass = await matchDispatchPass(router, req, env);
|
|
389
422
|
|
|
390
423
|
const variables: Record<string, unknown> = {};
|
|
391
424
|
|
|
@@ -474,29 +507,28 @@ export async function dispatch<TEnv = any>(
|
|
|
474
507
|
// Set params before middleware/handler run, so global middleware sees
|
|
475
508
|
// ctx.params (production sets them during matching, before middleware).
|
|
476
509
|
// On a redirect/unmatched path there are no route params.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
510
|
+
const recordPassParams = (pass: DispatchPass<TEnv>): void => {
|
|
511
|
+
if (pass.routeKey !== undefined) {
|
|
512
|
+
setRequestContextParams(pass.params, pass.routeKey);
|
|
513
|
+
} else {
|
|
514
|
+
requestContext.params = pass.params;
|
|
515
|
+
}
|
|
516
|
+
};
|
|
482
517
|
|
|
483
518
|
// The response-route handler (with its own route middleware) lives inside
|
|
484
519
|
// coreHandler below, mirroring production where handleResponseRoute is
|
|
485
520
|
// nested inside coreHandler. Built lazily so a redirect/404 path never
|
|
486
521
|
// touches it.
|
|
487
|
-
const callResponseRoute = async (
|
|
522
|
+
const callResponseRoute = async (
|
|
523
|
+
pass: DispatchPass<TEnv>,
|
|
524
|
+
): Promise<Response> => {
|
|
525
|
+
const { req, url, preview, params, responseType, handler } = pass;
|
|
488
526
|
// Match production: a partial (client-navigation) request to a response
|
|
489
527
|
// route is short-circuited to X-RSC-Reload (handleResponseRoute), BEFORE
|
|
490
528
|
// route-level middleware runs. Route-level middleware is skipped on a
|
|
491
529
|
// partial, exactly as production skips it.
|
|
492
530
|
const partialFinalHandler = async (): Promise<Response> =>
|
|
493
|
-
|
|
494
|
-
status: 200,
|
|
495
|
-
headers: {
|
|
496
|
-
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
497
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
498
|
-
},
|
|
499
|
-
});
|
|
531
|
+
createReloadResponse(requestContext, url);
|
|
500
532
|
|
|
501
533
|
const cleanUrl = new URL(req.url);
|
|
502
534
|
for (const key of [...cleanUrl.searchParams.keys()]) {
|
|
@@ -523,6 +555,7 @@ export async function dispatch<TEnv = any>(
|
|
|
523
555
|
// Brand as request-scoped so a "use cache" inside a response-route handler
|
|
524
556
|
// is detected as a request-scope violation here exactly as in production
|
|
525
557
|
// (response-route-handler.ts brands the same shape).
|
|
558
|
+
defineRewrittenFrom(responseHandlerCtx, requestContext);
|
|
526
559
|
(responseHandlerCtx as Record<symbol, unknown>)[NOCACHE_SYMBOL] = true;
|
|
527
560
|
|
|
528
561
|
const callHandler = async (): Promise<Response> => {
|
|
@@ -668,8 +701,11 @@ export async function dispatch<TEnv = any>(
|
|
|
668
701
|
// onTimeout Response is RETURNED from coreHandler so it flows back out
|
|
669
702
|
// through the global chain and finalizeResponse, identical to production
|
|
670
703
|
// (handler.ts:857 returns handleTimeoutResponse into the same next() path).
|
|
671
|
-
const runResponseRoute = async (
|
|
672
|
-
|
|
704
|
+
const runResponseRoute = async (
|
|
705
|
+
pass: DispatchPass<TEnv>,
|
|
706
|
+
): Promise<Response> => {
|
|
707
|
+
const { routeKey } = pass;
|
|
708
|
+
const responseWork = callResponseRoute(pass);
|
|
673
709
|
const renderOutcome = await withTimeout(
|
|
674
710
|
responseWork,
|
|
675
711
|
router.timeouts?.renderStartMs,
|
|
@@ -752,7 +788,8 @@ export async function dispatch<TEnv = any>(
|
|
|
752
788
|
// wins, never reaching the 308/404. The renderStartMs deadline lives inside
|
|
753
789
|
// runResponseRoute (below the chain), never covering the 308/404 paths —
|
|
754
790
|
// production only wraps the response-route plan (handler.ts:834-856).
|
|
755
|
-
const coreHandler = async (): Promise<Response> => {
|
|
791
|
+
const coreHandler = async (pass: DispatchPass<TEnv>): Promise<Response> => {
|
|
792
|
+
const { redirectTo, isUnmatched, url } = pass;
|
|
756
793
|
if (redirectTo) {
|
|
757
794
|
return createResponseWithMergedHeaders(null, {
|
|
758
795
|
status: 308,
|
|
@@ -765,7 +802,7 @@ export async function dispatch<TEnv = any>(
|
|
|
765
802
|
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
766
803
|
});
|
|
767
804
|
}
|
|
768
|
-
return runResponseRoute();
|
|
805
|
+
return runResponseRoute(pass);
|
|
769
806
|
};
|
|
770
807
|
|
|
771
808
|
// request.start opens the match transaction, mirroring match-handlers.ts.
|
|
@@ -790,18 +827,49 @@ export async function dispatch<TEnv = any>(
|
|
|
790
827
|
// only the response-route unit inside coreHandler (see runResponseRoute),
|
|
791
828
|
// mirroring handler.ts:845 so global middleware time is never counted
|
|
792
829
|
// toward the deadline.
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
830
|
+
// A throw rewrite() re-dispatches the same request context as the
|
|
831
|
+
// target, through the production handler's pass loop
|
|
832
|
+
// (rsc/rewrite-passes.ts). Each pass matches its URL and builds its own
|
|
833
|
+
// core handler.
|
|
834
|
+
let passIndex = 0;
|
|
835
|
+
const { response: mwResponse, rewrittenTo } = await runRewritePasses({
|
|
836
|
+
reqCtx: requestContext,
|
|
837
|
+
request: req,
|
|
838
|
+
url,
|
|
839
|
+
routeMap: () => router.routeMap as Record<string, string>,
|
|
840
|
+
basename: router.basename,
|
|
841
|
+
runPass: async (passRequest) => {
|
|
842
|
+
const pass =
|
|
843
|
+
passIndex++ === 0
|
|
844
|
+
? firstPass
|
|
845
|
+
: await matchDispatchPass(router, passRequest, env);
|
|
846
|
+
recordPassParams(pass);
|
|
847
|
+
const globalMatches = matchMiddleware(
|
|
848
|
+
pass.url.pathname,
|
|
849
|
+
router.middleware,
|
|
850
|
+
);
|
|
851
|
+
const passCore = (): Promise<Response> => coreHandler(pass);
|
|
852
|
+
return globalMatches.length === 0
|
|
853
|
+
? passCore()
|
|
854
|
+
: executeMiddleware<TEnv>(
|
|
855
|
+
globalMatches,
|
|
856
|
+
pass.req,
|
|
857
|
+
env,
|
|
858
|
+
variables,
|
|
859
|
+
passCore,
|
|
860
|
+
reverse,
|
|
861
|
+
);
|
|
862
|
+
},
|
|
863
|
+
report: (error, at) => {
|
|
864
|
+
invokeOnError(
|
|
865
|
+
router.onError,
|
|
866
|
+
error,
|
|
867
|
+
"routing",
|
|
868
|
+
{ request: at.request, url: at.url, env, handledByBoundary: false },
|
|
869
|
+
"RSC",
|
|
870
|
+
);
|
|
871
|
+
},
|
|
872
|
+
});
|
|
805
873
|
|
|
806
874
|
// Match production's global-chain exit (handler.ts): on a partial/action
|
|
807
875
|
// request a middleware 3xx redirect is converted to a Flight-safe response
|
|
@@ -832,6 +900,9 @@ export async function dispatch<TEnv = any>(
|
|
|
832
900
|
} else {
|
|
833
901
|
finalResponse = finalizeResponse(mwResponse);
|
|
834
902
|
}
|
|
903
|
+
if (rewrittenTo !== undefined) {
|
|
904
|
+
trySetHeader(finalResponse, REWRITE_HEADER, rewrittenTo);
|
|
905
|
+
}
|
|
835
906
|
|
|
836
907
|
// request.end closes the transaction. dispatch produces no RSC segments and
|
|
837
908
|
// holds no match-cache state, so segmentCount/cacheHit are 0/false — the
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// returned from `createPageHelpers(expect)`; the rest are standalone.
|
|
4
4
|
|
|
5
5
|
import type { ConsoleMessage, Expect, Locator, Page } from "@playwright/test";
|
|
6
|
+
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
6
7
|
|
|
7
8
|
// ============================================================================
|
|
8
9
|
// Standalone helpers (need only a page/locator)
|
|
@@ -170,7 +171,8 @@ export async function measureTime<T>(
|
|
|
170
171
|
export function isPrefetchRequest(req: {
|
|
171
172
|
headers: () => Record<string, string>;
|
|
172
173
|
}): boolean {
|
|
173
|
-
|
|
174
|
+
// Playwright lowercases header names.
|
|
175
|
+
return !!req.headers()[PREFETCH_HEADER.toLowerCase()];
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
// Module-level matcher + handler so blockPrefetch/unblockPrefetch share
|
package/src/testing/index.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* condition and would throw if pulled into this barrel.
|
|
27
27
|
*
|
|
28
28
|
* Layers:
|
|
29
|
-
* - Unit: runMiddleware, runLoader,
|
|
29
|
+
* - Unit: runMiddleware, runLoader, runRevalidate, runTransitionWhen
|
|
30
30
|
* - Integration: dispatch (request -> Response)
|
|
31
31
|
* - Cross-cut: assertCacheStatus, assertShellStatus, assertGeneratedRoutesMatch
|
|
32
32
|
* - Component: see @rangojs/router/testing/dom (renderRoute)
|
|
@@ -38,6 +38,7 @@ export { runMiddleware } from "./run-middleware.js";
|
|
|
38
38
|
export type {
|
|
39
39
|
RunMiddlewareOptions,
|
|
40
40
|
RunMiddlewareResult,
|
|
41
|
+
RunMiddlewareRewrite,
|
|
41
42
|
} from "./run-middleware.js";
|
|
42
43
|
export { runLoader, runLoaderResult } from "./run-loader.js";
|
|
43
44
|
export type {
|
|
@@ -55,8 +56,11 @@ export type {
|
|
|
55
56
|
RunTransitionWhenResult,
|
|
56
57
|
} from "./run-transition-when.js";
|
|
57
58
|
|
|
58
|
-
export {
|
|
59
|
-
export type {
|
|
59
|
+
export { runRevalidate } from "./run-revalidate.js";
|
|
60
|
+
export type {
|
|
61
|
+
RunRevalidateAction,
|
|
62
|
+
RunRevalidateOptions,
|
|
63
|
+
} from "./run-revalidate.js";
|
|
60
64
|
|
|
61
65
|
export { dispatch } from "./dispatch.js";
|
|
62
66
|
export type { DispatchOptions } from "./dispatch.js";
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { resolveActionRefId } from "../../router/is-action.js";
|
|
2
2
|
|
|
3
|
+
/** An imported server action, as a testing primitive's `action` option takes it. */
|
|
4
|
+
export type ImportedAction = (...args: never[]) => unknown;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes a testing primitive's `action` option: a bare imported action or
|
|
8
|
+
* actionId string becomes `{ ref }`; the object form passes through.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeTestAction<
|
|
11
|
+
T extends { ref?: ImportedAction | string },
|
|
12
|
+
>(action: ImportedAction | string | T | undefined): T | undefined {
|
|
13
|
+
return typeof action === "function" || typeof action === "string"
|
|
14
|
+
? ({ ref: action } as T)
|
|
15
|
+
: action;
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
/**
|
|
4
19
|
* The id a testing primitive's `action` option names: a single imported
|
|
5
20
|
* server action (its `$id ?? $$id`, as the action boundary derives it) or a
|
|
@@ -7,7 +22,7 @@ import { resolveActionRefId } from "../../router/is-action.js";
|
|
|
7
22
|
* primitive.
|
|
8
23
|
*/
|
|
9
24
|
export function resolveTestActionId(
|
|
10
|
-
action:
|
|
25
|
+
action: ImportedAction | string | undefined,
|
|
11
26
|
primitive: string,
|
|
12
27
|
): string | undefined {
|
|
13
28
|
if (action === undefined) return undefined;
|