mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
31b3e17843
Route handlers used a local Object.assign(Promise.resolve(...), params) helper that did not protect any well-known properties from shadowing. Page params used a Proxy but only guarded then/catch/finally, missing React's Promise status field and other well-known properties. Next.js explicitly reserves a well-known property set (then, catch, finally, status, toString, hasOwnProperty, etc.) so that params with those names do not break Promise behaviour or React introspection. This change: - Expands the shared makeThenableParams shim to protect the full well-known property set, matching Next.js reflect-utils.ts. - Replaces the weak route-handler local helper with the shared shim. - Adds unit tests for status and Object.prototype method shadowing. - Adds App Router integration tests for pages and route handlers with params named then, catch, finally, and status. Ported from Next.js: - https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/utils/reflect-utils.ts - https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/cache-components/cache-components.params.test.ts
106 lines
3.8 KiB
TypeScript
106 lines
3.8 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import { makeThenableParams } from "../packages/vinext/src/shims/thenable-params.js";
|
|
|
|
describe("makeThenableParams", () => {
|
|
it("is awaitable even when params contain then", async () => {
|
|
// eslint-disable-next-line unicorn/no-thenable
|
|
const params = makeThenableParams({ then: "foo" });
|
|
// If `then` were shadowing Promise.prototype.then, `await` would try to
|
|
// treat the object as a thenable and call `then` with resolve/reject,
|
|
// producing garbage or hanging. This must resolve to the plain object.
|
|
const resolved = await params;
|
|
// eslint-disable-next-line unicorn/no-thenable
|
|
expect(resolved).toEqual({ then: "foo" });
|
|
});
|
|
|
|
it("allows sync access to non-well-known params", () => {
|
|
// eslint-disable-next-line unicorn/no-thenable
|
|
const params = makeThenableParams({ slug: "post", then: "foo" });
|
|
expect(params.slug).toBe("post");
|
|
});
|
|
|
|
it("protects Promise methods from shadowing so the object stays thenable", () => {
|
|
// eslint-disable-next-line unicorn/no-thenable
|
|
const params = makeThenableParams({ then: "foo", catch: "bar", finally: "baz" });
|
|
|
|
expect(typeof params.then).toBe("function");
|
|
expect(typeof params.catch).toBe("function");
|
|
expect(typeof params.finally).toBe("function");
|
|
});
|
|
|
|
it("protects React Promise status from shadowing", () => {
|
|
const params = makeThenableParams({ status: "ok" });
|
|
|
|
// React uses `status` on Promises for introspection; it must not be
|
|
// shadowed by a param of the same name. Use Reflect.get because the
|
|
// type system omits well-known properties from the sync intersection.
|
|
expect(Reflect.get(params, "status")).not.toBe("ok");
|
|
});
|
|
|
|
it("protects React Promise value and error from shadowing", () => {
|
|
const params = makeThenableParams({ value: "foo", error: "bar" });
|
|
|
|
// React may mutate resolved promises to attach `.value` and `.error`
|
|
// for `use()` caching. Our Proxy must not return the param value for
|
|
// these keys, or React would read the wrong value on re-render.
|
|
expect(Reflect.get(params, "value")).not.toBe("foo");
|
|
expect(Reflect.get(params, "error")).not.toBe("bar");
|
|
});
|
|
|
|
it("excludes well-known properties from enumeration", () => {
|
|
/* eslint-disable unicorn/no-thenable */
|
|
const params = makeThenableParams({
|
|
slug: "post",
|
|
then: "foo",
|
|
catch: "bar",
|
|
finally: "baz",
|
|
status: "ok",
|
|
value: "val",
|
|
error: "err",
|
|
toString: "str",
|
|
});
|
|
/* eslint-enable unicorn/no-thenable */
|
|
|
|
const keys = Object.keys(params);
|
|
expect(keys).toEqual(["slug"]);
|
|
});
|
|
|
|
it("makes well-known param values available after awaiting", async () => {
|
|
/* eslint-disable unicorn/no-thenable */
|
|
const params = makeThenableParams({
|
|
slug: "post",
|
|
then: "foo",
|
|
catch: "bar",
|
|
finally: "baz",
|
|
status: "ok",
|
|
value: "val",
|
|
error: "err",
|
|
});
|
|
/* eslint-enable unicorn/no-thenable */
|
|
|
|
const resolved = await params;
|
|
expect(resolved.slug).toBe("post");
|
|
expect(resolved.then).toBe("foo");
|
|
expect(resolved.catch).toBe("bar");
|
|
expect(resolved.finally).toBe("baz");
|
|
expect(resolved.status).toBe("ok");
|
|
expect(resolved.value).toBe("val");
|
|
expect(resolved.error).toBe("err");
|
|
});
|
|
|
|
it("preserves catch-all array params through sync and awaited access", async () => {
|
|
const params = makeThenableParams({ slug: ["a", "b"] });
|
|
|
|
expect(Reflect.get(params, "slug")).toEqual(["a", "b"]);
|
|
expect(Object.keys(params)).toEqual(["slug"]);
|
|
expect(await params).toEqual({ slug: ["a", "b"] });
|
|
});
|
|
|
|
it("preserves empty params through sync keys and awaiting", async () => {
|
|
const params = makeThenableParams({});
|
|
|
|
expect(Object.keys(params)).toEqual([]);
|
|
expect(await params).toEqual({});
|
|
});
|
|
});
|