Files
wehub-resource-sync 70bf21e064
Deploy (to testing) and Test Playground Preview Worker / Deploy Playground Preview Worker (testing) (push) Has been skipped
Deploy Workers Shared Staging / Deploy Workers Shared Staging (push) Failing after 0s
Prerelease / build (push) Has been skipped
Handle Changesets / Handle Changesets (push) Has been cancelled
Semgrep OSS scan / semgrep-oss (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:11 +08:00

473 lines
14 KiB
TypeScript

import { existsSync, readFileSync } from "node:fs";
import { mkdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import * as cliPackages from "@cloudflare/cli-shared-helpers/packages";
import { NpmPackageManager } from "@cloudflare/workers-utils";
import { runInTempDir } from "@cloudflare/workers-utils/test-helpers";
import { beforeEach, describe, it, vi } from "vitest";
import {
hasV8MiddlewareFlag,
ReactRouter,
} from "../../src/frameworks/react-router";
import * as packagesUtils from "../../src/frameworks/utils/packages";
import { createMockContext } from "../helpers/mock-context";
function fixture(name: string): string {
return readFileSync(join(__dirname, "fixtures/react-router", name), "utf-8");
}
vi.mock("../../src/frameworks/utils/vite-config", () => ({
transformViteConfig: vi.fn(),
}));
vi.mock("../../src/frameworks/utils/vite-plugin", () => ({
installCloudflareVitePlugin: vi.fn(),
}));
vi.mock("../../src/frameworks/utils/packages", () => ({
getInstalledPackageVersion: vi.fn(),
isPackageInstalled: vi.fn(() => true),
}));
const context = createMockContext();
function getBaseOptions() {
return {
projectPath: process.cwd(),
outputDir: "build/",
workerName: "my-react-router-app",
dryRun: false,
packageManager: NpmPackageManager,
isWorkspaceRoot: false,
context,
};
}
function createFramework(version: string): ReactRouter {
vi.mocked(packagesUtils.getInstalledPackageVersion).mockReturnValue(version);
const framework = new ReactRouter({
id: "react-router",
name: "React Router",
});
framework.validateFrameworkVersion(
".",
{
name: "react-router",
minimumVersion: "7.0.0",
maximumKnownMajorVersion: "8",
},
context
);
return framework;
}
describe("hasV8MiddlewareFlag()", () => {
runInTempDir();
it("returns false when no config file exists", ({ expect }) => {
expect(hasV8MiddlewareFlag(process.cwd())).toBe(false);
});
it("returns false when config has no future block", async ({ expect }) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
expect(hasV8MiddlewareFlag(process.cwd())).toBe(false);
});
it("returns false when future block does not contain v8_middleware", async ({
expect,
}) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-future-no-middleware.ts")
);
expect(hasV8MiddlewareFlag(process.cwd())).toBe(false);
});
it("returns true when v8_middleware is set to true", async ({ expect }) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-middleware-true.ts")
);
expect(hasV8MiddlewareFlag(process.cwd())).toBe(true);
});
it("returns false when v8_middleware is set to false", async ({ expect }) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-middleware-false.ts")
);
expect(hasV8MiddlewareFlag(process.cwd())).toBe(false);
});
it("handles plain object export without satisfies", async ({ expect }) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-plain-object-middleware.ts")
);
expect(hasV8MiddlewareFlag(process.cwd())).toBe(true);
});
it("returns false when config file has syntax errors", async ({ expect }) => {
await writeFile(
resolve("react-router.config.ts"),
"export default { this is not valid syntax"
);
expect(hasV8MiddlewareFlag(process.cwd())).toBe(false);
});
it("detects v8_middleware inside a `satisfies Config` expression", async ({
expect,
}) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-middleware-and-split.ts")
);
expect(hasV8MiddlewareFlag(process.cwd())).toBe(true);
});
});
describe("React Router framework configure()", () => {
runInTempDir();
beforeEach(async () => {
vi.spyOn(cliPackages, "installPackages").mockImplementation(async () => {});
await mkdir(resolve("app"), { recursive: true });
await writeFile(resolve("vite.config.ts"), fixture("vite-config-basic.ts"));
});
describe("workers/app.ts generation — without v8_middleware", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
});
it("creates workers/app.ts with AppLoadContext augmentation", async ({
expect,
}) => {
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("workers/app.ts"), "utf-8");
expect(content).toContain("AppLoadContext");
expect(content).toContain("declare module");
expect(content).toContain("cloudflare: { env, ctx }");
expect(content).toContain("async fetch(request, env, ctx)");
expect(content).toContain("satisfies ExportedHandler<Env>");
});
});
describe("workers/app.ts generation — with v8_middleware", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-middleware-true.ts")
);
});
it("creates workers/app.ts without AppLoadContext augmentation", async ({
expect,
}) => {
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("workers/app.ts"), "utf-8");
expect(content).not.toContain("AppLoadContext");
expect(content).not.toContain("declare module");
expect(content).toContain(
'import { createRequestHandler } from "react-router"'
);
expect(content).toContain("requestHandler(request)");
expect(content).toContain("satisfies ExportedHandler<Env>");
});
it("creates workers/app.ts with a simple fetch handler", async ({
expect,
}) => {
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("workers/app.ts"), "utf-8");
expect(content).toContain("async fetch(request)");
expect(content).not.toContain("env, ctx");
expect(content).not.toContain("cloudflare: { env, ctx }");
});
});
describe("entry.server.tsx generation — without v8_middleware", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
});
it("creates entry.server.tsx with AppLoadContext", async ({ expect }) => {
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8");
expect(content).toContain("AppLoadContext");
expect(content).toContain("_loadContext: AppLoadContext");
expect(content).toContain(
'import type { AppLoadContext, EntryContext } from "react-router"'
);
});
});
describe("entry.server.tsx generation — with v8_middleware", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-middleware-true.ts")
);
});
it("creates entry.server.tsx without AppLoadContext", async ({
expect,
}) => {
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8");
expect(content).not.toContain("AppLoadContext");
expect(content).not.toContain("_loadContext");
expect(content).toContain(
'import type { EntryContext } from "react-router"'
);
expect(content).toContain("ServerRouter");
expect(content).toContain("renderToReadableStream");
});
});
describe("entry.server.tsx — existing file not overwritten", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-middleware-true.ts")
);
});
it("does not overwrite existing entry.server.tsx", async ({ expect }) => {
const existingContent = "// existing entry server";
await mkdir(resolve("app"), { recursive: true });
await writeFile(resolve("app/entry.server.tsx"), existingContent);
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8");
expect(content).toBe(existingContent);
});
});
describe("react-router.config.ts transformation", () => {
it("adds v8_viteEnvironmentApi for React Router >= 7.10.0", async ({
expect,
}) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("react-router.config.ts"), "utf-8");
expect(content).toContain("v8_viteEnvironmentApi: true");
// Should NOT add other v8 flags
expect(content).not.toContain("v8_middleware");
expect(content).not.toContain("v8_splitRouteModules");
expect(content).not.toContain("v8_passThroughRequests");
expect(content).not.toContain("v8_trailingSlashAwareDataRequests");
});
it("uses unstable_viteEnvironmentApi for React Router < 7.10.0", async ({
expect,
}) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
const framework = createFramework("7.9.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("react-router.config.ts"), "utf-8");
expect(content).toContain("unstable_viteEnvironmentApi: true");
expect(content).not.toContain("v8_viteEnvironmentApi");
});
it("preserves existing future flags when adding viteEnvironmentApi", async ({
expect,
}) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-middleware-true.ts")
);
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("react-router.config.ts"), "utf-8");
// Existing flag preserved
expect(content).toContain("v8_middleware: true");
// New flag added
expect(content).toContain("v8_viteEnvironmentApi: true");
});
});
describe("React Router >= 8.0.0", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
});
it("does not add future flags to react-router.config.ts", async ({
expect,
}) => {
const original = readFileSync(resolve("react-router.config.ts"), "utf-8");
const framework = createFramework("8.0.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("react-router.config.ts"), "utf-8");
expect(content).not.toContain("v8_viteEnvironmentApi");
expect(content).not.toContain("unstable_viteEnvironmentApi");
expect(content).toBe(original);
});
it("does not add future flags for React Router 8.x", async ({ expect }) => {
const original = readFileSync(resolve("react-router.config.ts"), "utf-8");
const framework = createFramework("8.2.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("react-router.config.ts"), "utf-8");
expect(content).not.toContain("v8_viteEnvironmentApi");
expect(content).not.toContain("unstable_viteEnvironmentApi");
expect(content).toBe(original);
});
it("uses middleware pattern by default for workers/app.ts", async ({
expect,
}) => {
const framework = createFramework("8.0.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("workers/app.ts"), "utf-8");
expect(content).not.toContain("AppLoadContext");
expect(content).not.toContain("declare module");
expect(content).toContain("async fetch(request)");
expect(content).not.toContain("env, ctx");
expect(content).toContain("requestHandler(request)");
});
it("uses middleware pattern by default for entry.server.tsx", async ({
expect,
}) => {
const framework = createFramework("8.0.0");
await framework.configure(getBaseOptions());
const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8");
expect(content).not.toContain("AppLoadContext");
expect(content).not.toContain("_loadContext");
expect(content).toContain(
'import type { EntryContext } from "react-router"'
);
});
});
describe("hasV8MiddlewareFlag with version >= 8.0.0", () => {
it("returns true without config file when version >= 8.0.0", ({
expect,
}) => {
expect(hasV8MiddlewareFlag(process.cwd(), "8.0.0")).toBe(true);
});
it("returns true regardless of config content when version >= 8.0.0", async ({
expect,
}) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
expect(hasV8MiddlewareFlag(process.cwd(), "8.0.0")).toBe(true);
});
it("still parses config for versions < 8.0.0", async ({ expect }) => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
expect(hasV8MiddlewareFlag(process.cwd(), "7.16.0")).toBe(false);
});
});
describe("dry run", () => {
it("does not create files in dry run mode", async ({ expect }) => {
const framework = createFramework("7.16.0");
const result = await framework.configure({
...getBaseOptions(),
dryRun: true,
});
expect(result.wranglerConfig).toEqual({
main: "./workers/app.ts",
});
expect(existsSync(resolve("workers/app.ts"))).toBe(false);
expect(existsSync(resolve("app/entry.server.tsx"))).toBe(false);
});
});
describe("wrangler config", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
});
it("returns wrangler config with main pointing to workers/app.ts", async ({
expect,
}) => {
const framework = createFramework("7.16.0");
const result = await framework.configure(getBaseOptions());
expect(result.wranglerConfig).toEqual({
main: "./workers/app.ts",
});
});
});
describe("package installation", () => {
beforeEach(async () => {
await writeFile(
resolve("react-router.config.ts"),
fixture("config-no-future.ts")
);
});
it("installs isbot as a dev dependency", async ({ expect }) => {
const framework = createFramework("7.16.0");
await framework.configure(getBaseOptions());
expect(cliPackages.installPackages).toHaveBeenCalledWith(
NpmPackageManager.type,
["isbot"],
expect.objectContaining({ dev: true })
);
});
});
});