mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
a35e43cef3
* feat(skip): add layout safety observation foundations Introduce the per-layout observation primitives that later slices need to decide whether retained static layouts can be reused. This slice adds the machinery only; nothing reads the observations yet, so behavior is unchanged. - app-layout-param-observation: per-layout completeness, param keys, structural scope, finite revalidate, request API, cacheLife, cache tag, cacheable fetch, dynamic fetch, and unstable_cache observation tracker. - app-page-probe: bounded React server subtree probe that walks layout-returned children including memo, forwardRef, and React.lazy wrappers, enforces depth and node limits, and refuses single-use iterable children. - shims: thenable params observer for sync, await, destructure, and enumeration. Cache, fetch, and unified request context record unstable_cache, cacheable fetch, and request-scoped observation state. - app-page-params: structural segment-param scope so empty optional catch-all targets are not treated as param-free downstream. No skip transport behavior. Later slices wire the planner, manifest, and encoder onto these observations. * fix(skip): address review feedback on observation foundations - Add currentFetchSoftTags reset in runWithIsolatedProbeDependencies to prevent soft tag leak from parent scope (matches runWithFetchCache isolation) - Replace unnecessary JSON.stringify in cacheLife check with direct null comparison - Add comment documenting intentional omission of markProbeComplete on probe errors (keeps completeness as unknown, safe fallback to render-and-send) * fix(skip): record cacheable-fetch observation synchronously before first await The patched fetch deferred observation and tag collection past await buildFetchCacheKey(). If a layout probe started a cacheable fetch and returned synchronously without awaiting it, the probe snapshot would see cacheableFetchCount: 0 -- a false proof that could let the skip planner reuse a stale retained layout. Move observation and tag recording before the first await in the cacheable branch. If key generation later fails and the fetch falls back to dynamic, recording both is conservative (false unsafe costs performance, not correctness). Add an integration test that exercises the real patched fetch shim without mocks to verify synchronous observation capture. * fix(test): remove polluting fetch restore and catch background promise Do not restore globalThis.fetch in test cleanup. ensureFetchPatch sets a global _PATCH_KEY guard; restoring fetch without clearing that marker makes later calls skip reinstallation silently. Catch the background fetch promise inside the probe so the patched fetch continuation (cache lookup, network fetch) cannot produce an unhandled rejection after the synchronous assertions complete. * chore: empty commit to trigger CI * chore: empty commit to trigger CI
58 lines
2.6 KiB
TypeScript
58 lines
2.6 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import {
|
|
resolveAppPageSegmentParamScopeKeys,
|
|
resolveAppPageSegmentParams,
|
|
} from "../packages/vinext/src/server/app-page-params.js";
|
|
|
|
describe("app page params helpers", () => {
|
|
it("passes only params that apply to each layout", () => {
|
|
// Ported from Next.js: test/e2e/app-dir/layout-params/layout-params.test.ts
|
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/layout-params/layout-params.test.ts
|
|
const routeSegments = ["base", "[param1]", "[param2]"];
|
|
const matchedParams = { param1: "something", param2: "another" };
|
|
|
|
expect(resolveAppPageSegmentParams(routeSegments, 0, matchedParams)).toEqual({});
|
|
expect(resolveAppPageSegmentParams(routeSegments, 1, matchedParams)).toEqual({});
|
|
expect(resolveAppPageSegmentParams(routeSegments, 2, matchedParams)).toEqual({
|
|
param1: "something",
|
|
});
|
|
expect(resolveAppPageSegmentParams(routeSegments, 3, matchedParams)).toEqual({
|
|
param1: "something",
|
|
param2: "another",
|
|
});
|
|
});
|
|
|
|
it("scopes catch-all params to the catch-all layout", () => {
|
|
// Ported from Next.js: test/e2e/app-dir/layout-params/layout-params.test.ts
|
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/layout-params/layout-params.test.ts
|
|
const routeSegments = ["catchall", "[...params]"];
|
|
const matchedParams = { params: ["something", "another"] };
|
|
|
|
expect(resolveAppPageSegmentParams(routeSegments, 1, matchedParams)).toEqual({});
|
|
expect(resolveAppPageSegmentParams(routeSegments, 2, matchedParams)).toEqual({
|
|
params: ["something", "another"],
|
|
});
|
|
});
|
|
|
|
it("omits empty optional catch-all params from layouts", () => {
|
|
// Ported from Next.js: test/e2e/app-dir/layout-params/layout-params.test.ts
|
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/layout-params/layout-params.test.ts
|
|
const routeSegments = ["optional-catchall", "[[...params]]"];
|
|
|
|
expect(resolveAppPageSegmentParams(routeSegments, 1, { params: [] })).toEqual({});
|
|
expect(resolveAppPageSegmentParams(routeSegments, 2, { params: [] })).toEqual({});
|
|
expect(
|
|
resolveAppPageSegmentParams(routeSegments, 2, { params: ["something", "another"] }),
|
|
).toEqual({
|
|
params: ["something", "another"],
|
|
});
|
|
});
|
|
|
|
it("keeps optional catch-all names in structural layout param scope", () => {
|
|
const routeSegments = ["docs", "[[...slug]]"];
|
|
|
|
expect(resolveAppPageSegmentParams(routeSegments, 2, { slug: [] })).toEqual({});
|
|
expect(resolveAppPageSegmentParamScopeKeys(routeSegments, 2)).toEqual(["slug"]);
|
|
});
|
|
});
|