mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
25f6e0fc87
* chore: enable typeAware and typeCheck, use vp check
* Build
* Try cache false
* Revert "Try cache false"
This reverts commit 5f76ed02f1.
* Update
* workaround for vp check
* Try no workaround
* Fix check
* Update to 0.1.12
* denyWarnings
547 lines
20 KiB
TypeScript
547 lines
20 KiB
TypeScript
import { describe, it, expect } from "vite-plus/test";
|
|
import {
|
|
createRequestContext,
|
|
runWithRequestContext,
|
|
getRequestContext,
|
|
isInsideUnifiedScope,
|
|
} from "../packages/vinext/src/shims/unified-request-context.js";
|
|
import {
|
|
getRequestExecutionContext,
|
|
runWithExecutionContext,
|
|
} from "../packages/vinext/src/shims/request-context.js";
|
|
|
|
describe("unified-request-context", () => {
|
|
describe("isInsideUnifiedScope", () => {
|
|
it("returns false outside any scope", () => {
|
|
expect(isInsideUnifiedScope()).toBe(false);
|
|
});
|
|
|
|
it("returns true inside a runWithRequestContext scope", () => {
|
|
const ctx = createRequestContext();
|
|
void runWithRequestContext(ctx, () => {
|
|
expect(isInsideUnifiedScope()).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getRequestContext", () => {
|
|
it("returns default values outside any scope", () => {
|
|
const ctx = getRequestContext();
|
|
expect(ctx).toBeDefined();
|
|
expect(ctx.headersContext).toBeNull();
|
|
expect(ctx.dynamicUsageDetected).toBe(false);
|
|
expect(ctx.pendingSetCookies).toEqual([]);
|
|
expect(ctx.draftModeCookieHeader).toBeNull();
|
|
expect(ctx.phase).toBe("render");
|
|
expect(ctx.i18nContext).toBeNull();
|
|
expect(ctx.serverContext).toBeNull();
|
|
expect(ctx.serverInsertedHTMLCallbacks).toEqual([]);
|
|
expect(ctx.requestScopedCacheLife).toBeNull();
|
|
expect(ctx._privateCache).toBeNull();
|
|
expect(ctx.currentRequestTags).toEqual([]);
|
|
expect(ctx.executionContext).toBeNull();
|
|
expect(ctx.ssrContext).toBeNull();
|
|
expect(ctx.ssrHeadChildren).toEqual([]);
|
|
});
|
|
|
|
it("returns a fresh detached context on each call outside any scope", () => {
|
|
const first = getRequestContext();
|
|
first.dynamicUsageDetected = true;
|
|
first.pendingSetCookies.push("first=1");
|
|
|
|
const second = getRequestContext();
|
|
expect(second).not.toBe(first);
|
|
expect(second.dynamicUsageDetected).toBe(false);
|
|
expect(second.pendingSetCookies).toEqual([]);
|
|
});
|
|
|
|
it("inherits the standalone ExecutionContext ALS in detached fallback mode", () => {
|
|
const outerCtx = {
|
|
waitUntil() {},
|
|
};
|
|
|
|
void runWithExecutionContext(outerCtx, () => {
|
|
expect(isInsideUnifiedScope()).toBe(false);
|
|
expect(getRequestContext().executionContext).toBe(outerCtx);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("runWithRequestContext", () => {
|
|
it("makes all fields accessible inside the scope", () => {
|
|
const headers = new Headers({ "x-test": "1" });
|
|
const cookies = new Map([["session", "abc"]]);
|
|
const fakeCtx = { waitUntil: () => {} };
|
|
|
|
const reqCtx = createRequestContext({
|
|
headersContext: { headers, cookies },
|
|
executionContext: fakeCtx,
|
|
});
|
|
|
|
void runWithRequestContext(reqCtx, () => {
|
|
const ctx = getRequestContext();
|
|
expect((ctx.headersContext as any).headers.get("x-test")).toBe("1");
|
|
expect((ctx.headersContext as any).cookies.get("session")).toBe("abc");
|
|
expect(ctx.executionContext).toBe(fakeCtx);
|
|
expect(ctx.dynamicUsageDetected).toBe(false);
|
|
expect(ctx.phase).toBe("render");
|
|
expect(ctx.i18nContext).toBeNull();
|
|
expect(ctx.pendingSetCookies).toEqual([]);
|
|
expect(ctx.currentRequestTags).toEqual([]);
|
|
expect(ctx._privateCache).toBeNull();
|
|
});
|
|
});
|
|
|
|
it("returns the value from fn (sync)", () => {
|
|
const ctx = createRequestContext();
|
|
const result = runWithRequestContext(ctx, () => 42);
|
|
expect(result).toBe(42);
|
|
});
|
|
|
|
it("returns the value from fn (async)", async () => {
|
|
const ctx = createRequestContext();
|
|
const result = await runWithRequestContext(ctx, async () => {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 1));
|
|
return 99;
|
|
});
|
|
expect(result).toBe(99);
|
|
});
|
|
|
|
it("scope is exited after fn completes", async () => {
|
|
const ctx = createRequestContext({
|
|
headersContext: { headers: new Headers(), cookies: new Map() },
|
|
});
|
|
|
|
await runWithRequestContext(ctx, async () => {
|
|
expect(isInsideUnifiedScope()).toBe(true);
|
|
});
|
|
|
|
expect(isInsideUnifiedScope()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("concurrent isolation", () => {
|
|
it("20 parallel requests each see their own headers/navigation/tags", async () => {
|
|
const results = await Promise.all(
|
|
Array.from({ length: 20 }, (_, i) => {
|
|
const reqCtx = createRequestContext({
|
|
headersContext: {
|
|
headers: new Headers({ "x-id": String(i) }),
|
|
cookies: new Map(),
|
|
},
|
|
currentRequestTags: [`tag-${i}`],
|
|
serverContext: {
|
|
pathname: `/path-${i}`,
|
|
searchParams: new URLSearchParams(),
|
|
params: {},
|
|
},
|
|
});
|
|
return runWithRequestContext(reqCtx, async () => {
|
|
const delayMs = (i % 10) + 1;
|
|
await new Promise<void>((resolve) => setTimeout(resolve, delayMs));
|
|
const ctx = getRequestContext();
|
|
return {
|
|
headerId: (ctx.headersContext as any)?.headers?.get("x-id"),
|
|
tag: ctx.currentRequestTags[0],
|
|
pathname: (ctx.serverContext as any)?.pathname,
|
|
};
|
|
});
|
|
}),
|
|
);
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
expect(results[i].headerId).toBe(String(i));
|
|
expect(results[i].tag).toBe(`tag-${i}`);
|
|
expect(results[i].pathname).toBe(`/path-${i}`);
|
|
}
|
|
});
|
|
|
|
it("mutations in one scope don't leak to another", async () => {
|
|
const ctxA = createRequestContext();
|
|
const ctxB = createRequestContext();
|
|
|
|
const pA = runWithRequestContext(ctxA, async () => {
|
|
getRequestContext().dynamicUsageDetected = true;
|
|
getRequestContext().pendingSetCookies.push("a=1");
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
return {
|
|
dynamic: getRequestContext().dynamicUsageDetected,
|
|
cookies: [...getRequestContext().pendingSetCookies],
|
|
};
|
|
});
|
|
|
|
const pB = runWithRequestContext(ctxB, async () => {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 1));
|
|
return {
|
|
dynamic: getRequestContext().dynamicUsageDetected,
|
|
cookies: [...getRequestContext().pendingSetCookies],
|
|
};
|
|
});
|
|
|
|
const [a, b] = await Promise.all([pA, pB]);
|
|
expect(a.dynamic).toBe(true);
|
|
expect(a.cookies).toEqual(["a=1"]);
|
|
expect(b.dynamic).toBe(false);
|
|
expect(b.cookies).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("privateCache lazy initialization", () => {
|
|
it("is null by default", () => {
|
|
const ctx = createRequestContext();
|
|
expect(ctx._privateCache).toBeNull();
|
|
});
|
|
|
|
it("stays null until explicitly set", () => {
|
|
const ctx = createRequestContext();
|
|
void runWithRequestContext(ctx, () => {
|
|
expect(getRequestContext()._privateCache).toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("nested scopes", () => {
|
|
it("inner runWithRequestContext overrides outer, restores on exit", () => {
|
|
const outerCtx = createRequestContext({
|
|
headersContext: {
|
|
headers: new Headers({ "x-id": "outer" }),
|
|
cookies: new Map(),
|
|
},
|
|
});
|
|
const innerCtx = createRequestContext({
|
|
headersContext: {
|
|
headers: new Headers({ "x-id": "inner" }),
|
|
cookies: new Map(),
|
|
},
|
|
});
|
|
|
|
void runWithRequestContext(outerCtx, () => {
|
|
expect((getRequestContext().headersContext as any).headers.get("x-id")).toBe("outer");
|
|
|
|
void runWithRequestContext(innerCtx, () => {
|
|
expect((getRequestContext().headersContext as any).headers.get("x-id")).toBe("inner");
|
|
});
|
|
|
|
// Outer scope restored
|
|
expect((getRequestContext().headersContext as any).headers.get("x-id")).toBe("outer");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("executionContext", () => {
|
|
it("is null by default", () => {
|
|
const ctx = createRequestContext();
|
|
void runWithRequestContext(ctx, () => {
|
|
expect(getRequestContext().executionContext).toBeNull();
|
|
});
|
|
});
|
|
|
|
it("is accessible when provided", () => {
|
|
const calls: Promise<unknown>[] = [];
|
|
const fakeCtx = {
|
|
waitUntil(p: Promise<unknown>) {
|
|
calls.push(p);
|
|
},
|
|
};
|
|
const ctx = createRequestContext({ executionContext: fakeCtx });
|
|
void runWithRequestContext(ctx, () => {
|
|
const ec = getRequestContext().executionContext as any;
|
|
expect(ec).toBe(fakeCtx);
|
|
ec.waitUntil(Promise.resolve("done"));
|
|
});
|
|
expect(calls).toHaveLength(1);
|
|
});
|
|
|
|
it("inherits the outer ExecutionContext ALS when none is provided", () => {
|
|
const outerCtx = {
|
|
waitUntil() {},
|
|
};
|
|
|
|
void runWithExecutionContext(outerCtx, () => {
|
|
void runWithRequestContext(createRequestContext(), () => {
|
|
expect(getRequestContext().executionContext).toBe(outerCtx);
|
|
expect(getRequestExecutionContext()).toBe(outerCtx);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("sub-state field access", () => {
|
|
it("each sub-state getter returns correct sub-fields", () => {
|
|
const reqCtx = createRequestContext({
|
|
headersContext: { headers: new Headers(), cookies: new Map() },
|
|
dynamicUsageDetected: true,
|
|
pendingSetCookies: ["a=b"],
|
|
draftModeCookieHeader: "c=d",
|
|
phase: "action",
|
|
i18nContext: { locale: "fr", defaultLocale: "en" },
|
|
serverContext: { pathname: "/test", searchParams: new URLSearchParams(), params: {} },
|
|
serverInsertedHTMLCallbacks: [() => "html"],
|
|
requestScopedCacheLife: { stale: 10, revalidate: 20 },
|
|
currentRequestTags: ["tag1"],
|
|
executionContext: { waitUntil: () => {} },
|
|
});
|
|
|
|
void runWithRequestContext(reqCtx, () => {
|
|
const ctx = getRequestContext();
|
|
expect(ctx.dynamicUsageDetected).toBe(true);
|
|
expect(ctx.pendingSetCookies).toEqual(["a=b"]);
|
|
expect(ctx.draftModeCookieHeader).toBe("c=d");
|
|
expect(ctx.phase).toBe("action");
|
|
expect(ctx.i18nContext).toEqual({ locale: "fr", defaultLocale: "en" });
|
|
expect((ctx.serverContext as any).pathname).toBe("/test");
|
|
expect(ctx.serverInsertedHTMLCallbacks).toHaveLength(1);
|
|
expect(ctx.requestScopedCacheLife).toEqual({
|
|
stale: 10,
|
|
revalidate: 20,
|
|
});
|
|
expect(ctx.currentRequestTags).toEqual(["tag1"]);
|
|
expect(ctx.executionContext).not.toBeNull();
|
|
expect(ctx.ssrContext).toBeNull();
|
|
expect(ctx.ssrHeadChildren).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("legacy wrapper semantics inside unified scope", () => {
|
|
it("runWithHeadersContext restores the outer headers sub-state", async () => {
|
|
const { runWithHeadersContext } = await import("../packages/vinext/src/shims/headers.js");
|
|
|
|
const outerHeaders = {
|
|
headers: new Headers({ "x-id": "outer" }),
|
|
cookies: new Map([["outer", "1"]]),
|
|
};
|
|
const innerHeaders = {
|
|
headers: new Headers({ "x-id": "inner" }),
|
|
cookies: new Map([["inner", "1"]]),
|
|
};
|
|
|
|
void runWithRequestContext(
|
|
createRequestContext({
|
|
headersContext: outerHeaders,
|
|
dynamicUsageDetected: true,
|
|
pendingSetCookies: ["outer=1"],
|
|
draftModeCookieHeader: "outer=draft",
|
|
phase: "action",
|
|
}),
|
|
() => {
|
|
void runWithHeadersContext(innerHeaders as any, () => {
|
|
const ctx = getRequestContext();
|
|
expect((ctx.headersContext as any).headers.get("x-id")).toBe("inner");
|
|
expect(ctx.dynamicUsageDetected).toBe(false);
|
|
expect(ctx.pendingSetCookies).toEqual([]);
|
|
expect(ctx.draftModeCookieHeader).toBeNull();
|
|
expect(ctx.phase).toBe("render");
|
|
|
|
ctx.dynamicUsageDetected = true;
|
|
ctx.pendingSetCookies.push("inner=1");
|
|
ctx.draftModeCookieHeader = "inner=draft";
|
|
ctx.phase = "route-handler";
|
|
});
|
|
|
|
const ctx = getRequestContext();
|
|
expect(ctx.headersContext).toBe(outerHeaders);
|
|
expect(ctx.dynamicUsageDetected).toBe(true);
|
|
expect(ctx.pendingSetCookies).toEqual(["outer=1"]);
|
|
expect(ctx.draftModeCookieHeader).toBe("outer=draft");
|
|
expect(ctx.phase).toBe("action");
|
|
},
|
|
);
|
|
});
|
|
|
|
it("runWithHeadersContext keeps spawned async work on the inner sub-state", async () => {
|
|
const { runWithHeadersContext } = await import("../packages/vinext/src/shims/headers.js");
|
|
|
|
let releaseInnerRead!: () => void;
|
|
const waitForInnerRead = new Promise<void>((resolve) => {
|
|
releaseInnerRead = resolve;
|
|
});
|
|
let innerRead!: Promise<string | null>;
|
|
|
|
await runWithRequestContext(
|
|
createRequestContext({
|
|
headersContext: {
|
|
headers: new Headers({ "x-id": "outer" }),
|
|
cookies: new Map(),
|
|
},
|
|
}),
|
|
async () => {
|
|
void runWithHeadersContext(
|
|
{
|
|
headers: new Headers({ "x-id": "inner" }),
|
|
cookies: new Map(),
|
|
},
|
|
() => {
|
|
innerRead = (async () => {
|
|
await waitForInnerRead;
|
|
return (getRequestContext().headersContext as any)?.headers?.get("x-id") ?? null;
|
|
})();
|
|
},
|
|
);
|
|
|
|
expect((getRequestContext().headersContext as any)?.headers?.get("x-id")).toBe("outer");
|
|
releaseInnerRead();
|
|
await expect(innerRead).resolves.toBe("inner");
|
|
},
|
|
);
|
|
});
|
|
|
|
it("runWithNavigationContext restores the outer navigation sub-state", async () => {
|
|
const { runWithNavigationContext } =
|
|
await import("../packages/vinext/src/shims/navigation-state.js");
|
|
const { setNavigationContext, getNavigationContext } =
|
|
await import("../packages/vinext/src/shims/navigation.js");
|
|
|
|
const outerCallback = () => "outer";
|
|
|
|
void runWithRequestContext(
|
|
createRequestContext({
|
|
serverContext: { pathname: "/outer", searchParams: new URLSearchParams(), params: {} },
|
|
serverInsertedHTMLCallbacks: [outerCallback],
|
|
}),
|
|
() => {
|
|
void runWithNavigationContext(() => {
|
|
expect(getNavigationContext()).toBeNull();
|
|
expect(getRequestContext().serverInsertedHTMLCallbacks).toEqual([]);
|
|
|
|
setNavigationContext({
|
|
pathname: "/inner",
|
|
searchParams: new URLSearchParams("q=1"),
|
|
params: { id: "1" },
|
|
});
|
|
getRequestContext().serverInsertedHTMLCallbacks.push(() => "inner");
|
|
});
|
|
|
|
expect((getNavigationContext() as any)?.pathname).toBe("/outer");
|
|
expect(getRequestContext().serverInsertedHTMLCallbacks).toEqual([outerCallback]);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("runWithI18nState restores the outer i18n sub-state", async () => {
|
|
const { runWithI18nState } = await import("../packages/vinext/src/shims/i18n-state.js");
|
|
const { getI18nContext, setI18nContext } =
|
|
await import("../packages/vinext/src/shims/i18n-context.js");
|
|
|
|
const outerI18n = {
|
|
locale: "fr",
|
|
locales: ["en", "fr"],
|
|
defaultLocale: "en",
|
|
};
|
|
|
|
void runWithRequestContext(
|
|
createRequestContext({
|
|
i18nContext: outerI18n,
|
|
}),
|
|
() => {
|
|
void runWithI18nState(() => {
|
|
expect(getI18nContext()).toBeNull();
|
|
|
|
setI18nContext({
|
|
locale: "de",
|
|
locales: ["en", "de"],
|
|
defaultLocale: "en",
|
|
});
|
|
|
|
expect(getRequestContext().i18nContext).toEqual({
|
|
locale: "de",
|
|
locales: ["en", "de"],
|
|
defaultLocale: "en",
|
|
});
|
|
});
|
|
|
|
expect(getI18nContext()).toEqual(outerI18n);
|
|
expect(getRequestContext().i18nContext).toEqual(outerI18n);
|
|
},
|
|
);
|
|
});
|
|
|
|
it("cache/private/fetch/router/head sub-scopes reset and restore correctly", async () => {
|
|
const { _runWithCacheState } = await import("../packages/vinext/src/shims/cache.js");
|
|
const { runWithPrivateCache } = await import("../packages/vinext/src/shims/cache-runtime.js");
|
|
const { runWithFetchCache, getCollectedFetchTags } =
|
|
await import("../packages/vinext/src/shims/fetch-cache.js");
|
|
const { runWithRouterState } = await import("../packages/vinext/src/shims/router-state.js");
|
|
const { setSSRContext } = await import("../packages/vinext/src/shims/router.js");
|
|
const { runWithHeadState } = await import("../packages/vinext/src/shims/head-state.js");
|
|
|
|
await runWithRequestContext(
|
|
createRequestContext({
|
|
requestScopedCacheLife: { revalidate: 60 },
|
|
_privateCache: new Map([["outer", 1]]),
|
|
currentRequestTags: ["outer-tag"],
|
|
ssrContext: { pathname: "/outer", query: {}, asPath: "/outer" },
|
|
ssrHeadChildren: ["<meta data-outer />"],
|
|
}),
|
|
async () => {
|
|
void _runWithCacheState(() => {
|
|
expect(getRequestContext().requestScopedCacheLife).toBeNull();
|
|
getRequestContext().requestScopedCacheLife = { revalidate: 1 } as any;
|
|
});
|
|
expect(getRequestContext().requestScopedCacheLife).toEqual({ revalidate: 60 });
|
|
|
|
void runWithPrivateCache(() => {
|
|
expect(getRequestContext()._privateCache).toBeInstanceOf(Map);
|
|
expect(getRequestContext()._privateCache?.size).toBe(0);
|
|
getRequestContext()._privateCache?.set("inner", 2);
|
|
});
|
|
expect([...getRequestContext()._privateCache!.entries()]).toEqual([["outer", 1]]);
|
|
|
|
await runWithFetchCache(async () => {
|
|
expect(getCollectedFetchTags()).toEqual([]);
|
|
getRequestContext().currentRequestTags.push("inner-tag");
|
|
});
|
|
expect(getCollectedFetchTags()).toEqual(["outer-tag"]);
|
|
|
|
void runWithRouterState(() => {
|
|
expect(getRequestContext().ssrContext).toBeNull();
|
|
setSSRContext({ pathname: "/inner", query: {}, asPath: "/inner" } as any);
|
|
});
|
|
expect((getRequestContext().ssrContext as any).pathname).toBe("/outer");
|
|
|
|
void runWithHeadState(() => {
|
|
expect(getRequestContext().ssrHeadChildren).toEqual([]);
|
|
getRequestContext().ssrHeadChildren.push("<meta data-inner />");
|
|
});
|
|
expect(getRequestContext().ssrHeadChildren).toEqual(["<meta data-outer />"]);
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("createRequestContext", () => {
|
|
it("creates context with all defaults", () => {
|
|
const ctx = createRequestContext();
|
|
expect(ctx.headersContext).toBeNull();
|
|
expect(ctx.dynamicUsageDetected).toBe(false);
|
|
expect(ctx.pendingSetCookies).toEqual([]);
|
|
expect(ctx.draftModeCookieHeader).toBeNull();
|
|
expect(ctx.phase).toBe("render");
|
|
expect(ctx.i18nContext).toBeNull();
|
|
expect(ctx.serverContext).toBeNull();
|
|
expect(ctx.serverInsertedHTMLCallbacks).toEqual([]);
|
|
expect(ctx.requestScopedCacheLife).toBeNull();
|
|
expect(ctx._privateCache).toBeNull();
|
|
expect(ctx.currentRequestTags).toEqual([]);
|
|
expect(ctx.executionContext).toBeNull();
|
|
expect(ctx.ssrContext).toBeNull();
|
|
expect(ctx.ssrHeadChildren).toEqual([]);
|
|
});
|
|
|
|
it("merges partial overrides", () => {
|
|
const ctx = createRequestContext({
|
|
phase: "action",
|
|
dynamicUsageDetected: true,
|
|
});
|
|
expect(ctx.phase).toBe("action");
|
|
expect(ctx.dynamicUsageDetected).toBe(true);
|
|
// Other fields get defaults
|
|
expect(ctx.i18nContext).toBeNull();
|
|
expect(ctx.headersContext).toBeNull();
|
|
expect(ctx.currentRequestTags).toEqual([]);
|
|
expect(ctx.ssrContext).toBeNull();
|
|
expect(ctx.ssrHeadChildren).toEqual([]);
|
|
});
|
|
});
|
|
});
|