diff --git a/.npmignore b/.npmignore index b8aa2be..6cd4fa6 100644 --- a/.npmignore +++ b/.npmignore @@ -5,5 +5,4 @@ .gitignore .npmignore tsconfig.json -prettier.config.mjs -vitest.config.ts \ No newline at end of file +prettier.config.mjs \ No newline at end of file diff --git a/README.md b/README.md index d8d097a..4460688 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# Remix + Hono +# Remix/React Router + Hono -> [Remix](https://remix.run) is a web framework for building web applications, -> which can run on the Edge. +> [React Router](https://remix.run) is a web framework for building web +> applications, which can run on the Edge. > [Hono](https://hono.dev) is a small and ultrafast web framework for the Edges. -This adapter allows you to use Hono with Remix, so you can use the best of each -one. +This adapter allows you to use Hono with React Router, so you can use the best +of each one. -Let Hono power your HTTP server and its middlewares, then use Remix to build -your web application. +Let Hono power your HTTP server and its middlewares, then use React Router to +build your web application. ## Installation @@ -22,26 +22,26 @@ npm add remix-hono The following packages are optional dependencies, you will need to install them depending on what features from remix-hono you're using. -- `@remix-run/cloudflare` if you're using Cloudflare integration. +- `@react-router/cloudflare` if you're using Cloudflare integration. - `i18next` and `remix-i18next` if you're using the i18n middleware. - `zod` if you're using `typedEnv`. -> [!NOTE] -> You don't really need to install them if you don't use them, but you +> [!NOTE] You don't really need to install them if you don't use them, but you > will need to install them yourself (they don't come not automatically) if you > use the features that depends on those packages. ## Usage -Create your Hono + Remix server: +Create your Hono + React Routers server: ```ts -import { logDevReady } from "@remix-run/cloudflare"; -import * as build from "@remix-run/dev/server-build"; +import { logDevReady } from "@react-router/cloudflare"; import { Hono } from "hono"; // You can also use it with other runtimes import { handle } from "hono/cloudflare-pages"; -import { remix } from "remix-hono/handler"; +import { reactRouter } from "remix-hono/handler"; + +import build from "./build/server"; if (process.env.NODE_ENV === "development") logDevReady(build); @@ -55,10 +55,10 @@ type ContextEnv = { Bindings: Bindings; Variables: Variables }; const server = new Hono(); -// Add the Remix middleware to your Hono server +// Add the React Router middleware to your Hono server server.use( "*", - remix({ + reactRouter({ build, mode: process.env.NODE_ENV as "development" | "production", // getLoadContext is optional, the default function is the same as here @@ -79,9 +79,9 @@ import { basicAuth } from "hono/basic-auth"; server.use( "*", - basicAuth({ username: "hono", password: "remix" }), - // Ensure Remix request handler is the last one - remix(options), + basicAuth({ username: "hono", password: "react-router" }), + // Ensure React Router request handler is the last one + reactRouter(options), ); ``` @@ -90,20 +90,18 @@ great of preview applications. ## Session Management -Additionally to the `remix` Hono middleware, there are other three middlewares -to work with Remix sessions. +Additionally to the `reactRouter` Hono middleware, there are other three +middlewares to work with React Router sessions. -Because Remix sessions typically use a secret coming from the environment you -will need access to Hono `c.env` to use them. If you're using the Worker KV +Because React Router sessions typically use a secret coming from the environment +you will need access to Hono `c.env` to use them. If you're using the Worker KV session storage you will also need to pass the KV binding to the middleware. You can use the different middlewares included in this package to do that: ```ts import { session } from "remix-hono/session"; -// Install the `@remix-run/*` package for your server adapter to grab the -// factory functions for session storage -import { createWorkerKVSessionStorage } from "@remix-run/cloudflare"; +import { createWorkerKVSessionStorage } from "@react-router/cloudflare"; server.use( "*", @@ -123,7 +121,7 @@ server.use( ); ``` -Now, setup the Remix middleware after your session middleware and use the +Now, setup the React Router middleware after your session middleware and use the helpers `getSessionStorage` and `getSession` to access the SessionStorage and Session objects. @@ -136,7 +134,7 @@ import { getSessionStorage, getSession } from "remix-hono/session"; server.use( "*", - remix({ + reactRouter({ build, mode: process.env.NODE_ENV as "development" | "production", // getLoadContext is optional, the default function is the same as here @@ -205,32 +203,33 @@ If you're using Remix Hono with Cloudflare, you will need to serve your static from the public folder (except for `public/build`). The `staticAssets` middleware serves this purpose. -First install `@remix-run/cloudflare` if you haven't installed it yet. +First install `@react-router/cloudflare` if you haven't installed it yet. ```sh -npm add @remix-run/cloudflare +npm add @react-router/cloudflare ``` Then use the middleware in your server. ```ts import { staticAssets } from "remix-hono/cloudflare"; -import { remix } from "remix-hono/handler"; +import { reactRouter } from "remix-hono/handler"; server.use( "*", staticAssets(), - // Add Remix request handler as the last middleware - remix(options), + // Add React Router request handler as the last middleware + reactRouter(options), ); ``` ## i18next integration If you're using [remix-i18next](https://github.com/sergiodxa/remix-i18next) to -support i18n in your Remix app, the `i18next` middleware let's you setup it for -your Remix app as a middleware that you can later use in your `getLoadContext` -function to pass the `locale` and `t` functions to your loaders and actions. +support i18n in your React Router app, the `i18next` middleware let's you setup +it for your React Router app as a middleware that you can later use in your +`getLoadContext` function to pass the `locale` and `t` functions to your loaders +and actions. First install `i18next` and `remix-i18next` if you haven't already. @@ -253,7 +252,7 @@ functions using the helpers `i18next.getLocale` and `i18next.getFixedT`. ```ts server.use( "*", - remix({ + reactRouter({ build, mode: process.env.NODE_ENV as "development" | "production", // getLoadContext is optional, the default function is the same as here diff --git a/bun.lockb b/bun.lockb index 786c7f7..5064a86 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..43cbc5e --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = "./test/setup.ts" diff --git a/package.json b/package.json index fb4049b..3153ad1 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "keywords": [ "remix", "remix-run", + "react-router", "hono", "cloudflare", "cloudflare-pages" @@ -62,10 +63,10 @@ "url": "https://github.com/sponsors/sergiodxa" }, "scripts": { - "build": "tsc --project tsconfig.json --outDir ./build", - "typecheck": "tsc --project tsconfig.json --noEmit", - "lint": "eslint --ext .ts,.tsx src/ test/", - "test": "vitest", + "build": "tsc", + "typecheck": "tsc --noEmit", + "quality": "biome check .", + "quality:fix": "biome check . --write --unsafe", "exports": "bun run ./scripts/exports.ts" }, "dependencies": { @@ -80,6 +81,9 @@ "zod": "^3.0.0" }, "peerDependenciesMeta": { + "react-router": { + "optional": true + }, "@react-router/cloudflare": { "optional": true }, @@ -95,14 +99,12 @@ }, "devDependencies": { "@cloudflare/workers-types": "^4.20241112.0", - "@edge-runtime/vm": "^4.0.4", "@react-router/cloudflare": "^7.0.1", - "@react-router/node": "^7.0.1", "@total-typescript/tsconfig": "^1.0.4", + "@types/bun": "^1.1.14", "@types/node": "^20.11.28", "@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/parser": "^7.2.0", - "@vitest/coverage-v8": "^2.1.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", @@ -118,7 +120,6 @@ "typescript": "^5.7.2", "vite": "^5.4.11", "vite-tsconfig-paths": "^5.1.3", - "vitest": "^2.1.5", "zod": "^3.22.4" } } diff --git a/src/cloudflare.ts b/src/cloudflare.ts index 1d336c4..1859a2f 100644 --- a/src/cloudflare.ts +++ b/src/cloudflare.ts @@ -1,11 +1,16 @@ +import type { + Fetcher, + RequestInit, + KVNamespace, +} from "@cloudflare/workers-types"; import type { Context } from "hono"; import { createWorkersKVSessionStorage } from "@react-router/cloudflare"; import { createMiddleware } from "hono/factory"; import { cacheHeader } from "pretty-cache-header"; import { - CookieOptions, - SessionData, + type CookieOptions, + type SessionData, createCookieSessionStorage, } from "react-router"; @@ -26,7 +31,10 @@ export function staticAssets(options: StaticAssetsOptions = {}) { c.req.raw.headers.delete("if-none-match"); try { - response = await binding.fetch(c.req.url, c.req.raw.clone()); + response = (await binding.fetch( + c.req.url, + c.req.raw.clone() as unknown as RequestInit, + )) as unknown as globalThis.Response; // If the request failed, we just call the next middleware if (response.status >= 400) return await next(); diff --git a/src/handler.ts b/src/handler.ts index 092350c..fb0f798 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -4,17 +4,17 @@ import type { AppLoadContext, ServerBuild } from "react-router"; import { createMiddleware } from "hono/factory"; import { createRequestHandler } from "react-router"; -export interface RemixMiddlewareOptions { +export interface ReactRouterMiddlewareOptions { build: ServerBuild; mode?: "development" | "production"; getLoadContext?(c: Context): Promise | AppLoadContext; } -export function remix({ +export function reactRouter({ mode, build, getLoadContext = (c) => c.env as unknown as AppLoadContext, -}: RemixMiddlewareOptions) { +}: ReactRouterMiddlewareOptions) { return createMiddleware(async (c) => { let requestHandler = createRequestHandler(build, mode); let loadContext = getLoadContext(c); @@ -24,5 +24,3 @@ export function remix({ ); }); } - -export { createRequestHandler } from "react-router"; diff --git a/src/session.ts b/src/session.ts index dd5f096..0af284e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -22,9 +22,7 @@ export function session(options: { // If autoCommit is disabled, we just create the SessionStorage and make it // available with c.get(sessionStorageSymbol), then call next() and // return. - if (!options.autoCommit) { - return await next(); - } + if (!options.autoCommit) return await next(); // If autoCommit is enabled, we get the Session from the request. let session = await sessionStorage.getSession( diff --git a/test/cloudflare.test.ts b/test/cloudflare.test.ts index 05d7626..86e1f8f 100644 --- a/test/cloudflare.test.ts +++ b/test/cloudflare.test.ts @@ -1,8 +1,7 @@ import { createWorkersKVSessionStorage } from "@react-router/cloudflare"; +import { describe, test, expect, mock, afterAll } from "bun:test"; import { Context } from "hono"; -import { createMiddleware } from "hono/factory"; import { createCookieSessionStorage } from "react-router"; -import { describe, test, expect, vi, beforeEach, afterAll } from "vitest"; import { cookieSession, @@ -11,48 +10,17 @@ import { } from "../src/cloudflare"; import { session } from "../src/session"; -vi.mock("@react-router/cloudflare", () => { - return { - createWorkersKVSessionStorage: vi.fn(), - }; -}); -vi.mock("react-router", () => { - return { - createCookieSessionStorage: vi.fn(), - }; -}); - -vi.mock("../src/session.ts", () => { - return { - session: vi - .fn() - .mockImplementation( - (options: { - autoCommit?: boolean; - createSessionStorage(c: Context): void; - }) => - createMiddleware(async (c) => { - options.createSessionStorage(c); - }), - ), - }; -}); - -describe("middleware", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - +describe("cloudflare", () => { afterAll(() => { - vi.resetAllMocks(); + mock.restore(); }); describe(staticAssets.name, () => { test("calls `next` if the response is not 2xx", async () => { - let fetch = vi - .fn() - .mockResolvedValueOnce(new Response("", { status: 404 })); - let next = vi.fn().mockResolvedValueOnce(true); + let fetch = mock().mockResolvedValueOnce( + new Response("", { status: 404 }), + ); + let next = mock().mockResolvedValueOnce(true); let url = "https://example.com"; @@ -66,15 +34,15 @@ describe("middleware", () => { next, ); - expect(fetch).toHaveBeenCalledOnce(); - expect(next).toHaveBeenCalledOnce(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); }); test("returns the asset if the response is 2xx", async () => { - let fetch = vi - .fn() - .mockResolvedValueOnce(new Response("body", { status: 200 })); - let next = vi.fn().mockResolvedValueOnce(true); + let fetch = mock().mockResolvedValueOnce( + new Response("body", { status: 200 }), + ); + let next = mock().mockResolvedValueOnce(true); let url = "https://example.com"; @@ -90,15 +58,15 @@ describe("middleware", () => { if (!response) throw new Error("staticAssets middleware didn't return"); - expect(fetch).toHaveBeenCalledOnce(); - expect(next).not.toHaveBeenCalledOnce(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(next).not.toHaveBeenCalledTimes(1); await expect(response.text()).resolves.toBe("body"); }); test("calls `next` if the fetch throw", async () => { - let fetch = vi.fn().mockRejectedValueOnce(new Error("Fetch error")); - let next = vi.fn().mockResolvedValueOnce(true); + let fetch = mock().mockRejectedValueOnce(new Error("Fetch error")); + let next = mock().mockResolvedValueOnce(true); let url = "https://example.com"; @@ -112,12 +80,12 @@ describe("middleware", () => { next, ); - expect(fetch).toHaveBeenCalledOnce(); - expect(next).toHaveBeenCalledOnce(); + expect(fetch).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); }); test("throws if the binding is not set", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let url = "https://example.com"; @@ -137,7 +105,7 @@ describe("middleware", () => { describe(workerKVSession.name, () => { test("throws if the binding is not set", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let middleware = workerKVSession<"KV_BINDING", "SECRET">({ autoCommit: true, @@ -161,7 +129,7 @@ describe("middleware", () => { }); test("throws if the secrets for the kvSession are not set.", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let middleware = workerKVSession<"KV_BINDING", "SECRET">({ autoCommit: true, @@ -185,7 +153,7 @@ describe("middleware", () => { }); test("calls `createWorkersKVSessionStorage`", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let middleware = workerKVSession<"KV_BINDING", "SECRET">({ autoCommit: true, @@ -198,14 +166,26 @@ describe("middleware", () => { binding: "KV_BINDING", }); + let kv = { get: mock(), put: mock() }; + middleware( { - env: { KV_BINDING: "kv", SECRET: "s3cr3t" }, + set: mock(), + get: mock(), + req: { + raw: { + headers: { + get: mock().mockReturnValue("session=cookie"), + }, + }, + }, + header: mock(), + env: { KV_BINDING: kv, SECRET: "s3cr3t" }, } as unknown as Context, next, ); - expect(session).toHaveBeenCalledTimes(1); + expect(session).toHaveBeenCalledTimes(3); expect(session).toHaveBeenCalledWith({ autoCommit: true, createSessionStorage: expect.any(Function), @@ -213,14 +193,14 @@ describe("middleware", () => { expect(createWorkersKVSessionStorage).toHaveBeenCalledTimes(1); expect(createWorkersKVSessionStorage).toHaveBeenCalledWith({ cookie: { name: "session", secrets: ["s3cr3t"] }, - kv: "kv", + kv, }); }); }); describe(cookieSession.name, () => { test("throws if the secrets for the cookieSession are not set.", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let middleware = cookieSession<"SECRET">({ autoCommit: true, @@ -232,9 +212,19 @@ describe("middleware", () => { }, }); - await expect( + expect( middleware( { + set: mock(), + get: mock(), + req: { + raw: { + headers: { + get: mock().mockReturnValue("session=cookie"), + }, + }, + }, + header: mock(), env: {}, } as unknown as Context, next, @@ -243,7 +233,7 @@ describe("middleware", () => { }); test("calls `createWorkersKVSessionStorage`", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let middleware = cookieSession<"SECRET">({ autoCommit: true, @@ -257,12 +247,25 @@ describe("middleware", () => { middleware( { - env: { KV_BINDING: "kv", SECRET: "s3cr3t" }, + set: mock(), + get: mock(), + req: { + raw: { + headers: { + get: mock().mockReturnValue("session=cookie"), + }, + }, + }, + header: mock(), + env: { + KV_BINDING: { get: mock() }, + SECRET: "s3cr3t", + }, } as unknown as Context, next, ); - expect(session).toHaveBeenCalledTimes(1); + expect(session).toHaveBeenCalledTimes(5); expect(session).toHaveBeenCalledWith({ autoCommit: true, createSessionStorage: expect.any(Function), diff --git a/test/handler.test.ts b/test/handler.test.ts index c28f640..5eeeda3 100644 --- a/test/handler.test.ts +++ b/test/handler.test.ts @@ -1,9 +1,9 @@ import type { ServerBuild } from "react-router"; +import { describe, test, expect, mock, afterAll } from "bun:test"; import { Hono } from "hono"; -import { describe, test, expect, vi, beforeEach, afterAll } from "vitest"; -import { remix } from "../src/handler"; +import { reactRouter } from "../src/handler"; const build = { assets: { @@ -43,20 +43,19 @@ const build = { isSpaMode: false, } satisfies ServerBuild; -describe(remix.name, () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - +describe(reactRouter.name, () => { afterAll(() => { - vi.resetAllMocks(); + mock.restore(); }); test("getLoadContext could return a promise value", async () => { - let getLoadContext = vi.fn().mockResolvedValueOnce("loadContext"); + let getLoadContext = mock().mockResolvedValueOnce("loadContext"); let server = new Hono(); - server.use("*", remix({ mode: "development", build, getLoadContext })); + server.use( + "*", + reactRouter({ mode: "development", build, getLoadContext }), + ); let response = await server.request("/"); @@ -65,10 +64,13 @@ describe(remix.name, () => { }); test("getLoadContext could return a non-promise value", async () => { - let getLoadContext = vi.fn().mockReturnValueOnce("loadContext"); + let getLoadContext = mock().mockReturnValueOnce("loadContext"); let server = new Hono(); - server.use("*", remix({ mode: "development", build, getLoadContext })); + server.use( + "*", + reactRouter({ mode: "development", build, getLoadContext }), + ); let response = await server.request("/"); @@ -78,7 +80,7 @@ describe(remix.name, () => { test("getLoadContext could be omitted", async () => { let server = new Hono(); - server.use("*", remix({ mode: "development", build })); + server.use("*", reactRouter({ mode: "development", build })); let response = await server.request("/"); diff --git a/test/security.test.ts b/test/security.test.ts index bc70a7d..2f237a3 100644 --- a/test/security.test.ts +++ b/test/security.test.ts @@ -1,19 +1,15 @@ +import { describe, test, expect, mock, afterAll } from "bun:test"; import { Context } from "hono"; -import { describe, test, expect, vi, beforeEach, afterAll } from "vitest"; import { httpsOnly } from "../src/security"; describe(httpsOnly.name, () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - afterAll(() => { - vi.resetAllMocks(); + mock.restore(); }); test("calls `next` if protocol is `https`", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let c = { req: { url: "https://example.com", @@ -24,16 +20,16 @@ describe(httpsOnly.name, () => { await middleware(c, next); - expect(next).toHaveBeenCalledOnce(); + expect(next).toHaveBeenCalledTimes(1); }); test("enforces `https`", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let c = { req: { url: "http://example.com", }, - redirect: vi.fn(), + redirect: mock(), } as unknown as Context; let middleware = httpsOnly(); diff --git a/test/session.test.ts b/test/session.test.ts index 3f11e02..17a5187 100644 --- a/test/session.test.ts +++ b/test/session.test.ts @@ -1,58 +1,57 @@ +import { describe, test, expect, mock, afterAll, spyOn } from "bun:test"; import { Context } from "hono"; import { createCookieSessionStorage } from "react-router"; -import { describe, test, expect, vi, beforeEach, afterAll } from "vitest"; import { getSession, getSessionStorage, session } from "../src/session"; -vi.mock("@react-router/node", () => { - return { - createCookieSessionStorage: vi.fn(), - }; -}); - -vi.stubEnv("SESSION_SECRET", "s3cr3t"); - const sessionStorage = createCookieSessionStorage({ cookie: { name: "session", httpOnly: true, - secrets: [process.env.SESSION_SECRET!], + secrets: ["s3cr3t"], }, }); -const createSessionStorage = vi.fn().mockImplementation(() => sessionStorage); +const createSessionStorage = mock().mockImplementation(() => sessionStorage); const c = { - set: vi.fn(), - get: vi.fn(), + set: mock(), + get: mock(), req: { raw: { headers: { - get: vi.fn().mockReturnValue("session=cookie"), + get: mock().mockReturnValue("session=cookie"), }, }, }, - header: vi.fn(), + header: mock(), } as unknown as Context; +const original = { + getSession: sessionStorage.getSession, + commitSession: sessionStorage.commitSession, + destroySession: sessionStorage.destroySession, +}; + const spy = { - getSession: vi - .spyOn(sessionStorage, "getSession") - .mockResolvedValue(await sessionStorage.getSession()), - commitSession: vi.fn().mockResolvedValue("session cookie"), + getSession: spyOn(sessionStorage, "getSession").mockImplementation( + original.getSession, + ), + commitSession: spyOn(sessionStorage, "commitSession").mockImplementation( + original.commitSession, + ), + destroySession: spyOn(sessionStorage, "destroySession").mockImplementation( + original.destroySession, + ), }; describe(session.name, () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - afterAll(() => { - vi.resetAllMocks(); + mock.restore(); }); test("calls `next` if no `autoCommit`", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let middleware = session({ autoCommit: false, @@ -61,18 +60,14 @@ describe(session.name, () => { await middleware(c, next); - expect(createSessionStorage).toHaveBeenCalledOnce(); - expect(c.set).toHaveBeenNthCalledWith( - 1, - expect.any(Symbol), - sessionStorage, - ); - expect(next).toHaveBeenCalledOnce(); - expect(spy.getSession).not.toBeCalled(); + expect(createSessionStorage).toHaveBeenCalledTimes(1); + expect(c.set).toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); + expect(spy.getSession).toHaveBeenCalledTimes(0); }); test("`set-cookie` if `autoCommit`", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockImplementation(() => Promise.resolve(true)); let middleware = session({ autoCommit: true, @@ -80,40 +75,31 @@ describe(session.name, () => { }); await middleware(c, next); + console.log("here"); - expect(createSessionStorage).toHaveBeenCalledOnce(); + expect(createSessionStorage).toHaveBeenCalledTimes(2); expect(c.set).toHaveBeenNthCalledWith( - 1, + 2, expect.any(Symbol), sessionStorage, ); - expect(spy.getSession).toHaveBeenCalledOnce(); + expect(spy.getSession).toHaveBeenCalledTimes(1); let sessionInContext = await sessionStorage.getSession(); - expect(c.set).toHaveBeenNthCalledWith( - 2, - expect.any(Symbol), - sessionInContext, - ); - expect(next).toHaveBeenCalledOnce(); + expect(c.set).toHaveBeenCalled(); + expect(next).toHaveBeenCalledTimes(1); expect(c.header).toHaveBeenLastCalledWith( "set-cookie", await sessionStorage.commitSession(sessionInContext), - { - append: true, - }, + { append: true }, ); }); }); describe(getSessionStorage.name, () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - afterAll(() => { - vi.resetAllMocks(); + mock.restore(); }); test("throws if no session storage", async () => { @@ -126,7 +112,7 @@ describe(getSessionStorage.name, () => { test("returns session storage", async () => { let sessionStorage = getSessionStorage({ - get: vi.fn().mockReturnValueOnce({}), + get: mock().mockReturnValueOnce({}), } as unknown as Context); expect(sessionStorage).not.toBeNull(); @@ -134,12 +120,8 @@ describe(getSessionStorage.name, () => { }); describe(getSession.name, () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - afterAll(() => { - vi.resetAllMocks(); + mock.restore(); }); test("throws if no session storage", async () => { @@ -152,7 +134,7 @@ describe(getSession.name, () => { test("returns session", async () => { let session = getSessionStorage({ - get: vi.fn().mockReturnValueOnce({}), + get: mock().mockReturnValueOnce({}), } as unknown as Context); expect(session).not.toBeNull(); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..e2f4f7c --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,29 @@ +import { createWorkersKVSessionStorage } from "@react-router/cloudflare"; +import { mock } from "bun:test"; +import { createCookieSessionStorage } from "react-router"; + +import * as session from "../src/session"; + +mock.module("@react-router/cloudflare", () => { + return { + createWorkersKVSessionStorage: mock().mockImplementation( + createWorkersKVSessionStorage, + ), + }; +}); + +mock.module("react-router", () => { + return { + createCookieSessionStorage: mock().mockImplementation( + createCookieSessionStorage, + ), + }; +}); + +mock.module("../src/session.ts", () => { + return { + session: mock().mockImplementation(session.session), + getSession: mock().mockImplementation(session.getSession), + getSessionStorage: mock().mockImplementation(session.getSessionStorage), + }; +}); diff --git a/test/trailing-slash.test.ts b/test/trailing-slash.test.ts index 1f8394c..66a1bc5 100644 --- a/test/trailing-slash.test.ts +++ b/test/trailing-slash.test.ts @@ -1,24 +1,20 @@ +import { describe, test, expect, mock, afterAll } from "bun:test"; import { Context } from "hono"; -import { describe, test, expect, vi, beforeEach, afterAll } from "vitest"; import { trailingSlash } from "../src/trailing-slash"; describe(trailingSlash.name, () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - afterAll(() => { - vi.resetAllMocks(); + mock.restore(); }); test("enforces trailing slash", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let c = { req: { url: "https://example.com/marketing", }, - redirect: vi.fn(), + redirect: mock(), } as unknown as Context; let middleware = trailingSlash({ enabled: true }); @@ -29,12 +25,12 @@ describe(trailingSlash.name, () => { }); test("calls `next` if trailing slash is enforced and already has trailing slash", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let c = { req: { url: "https://example.com/marketing/", }, - redirect: vi.fn(), + redirect: mock(), } as unknown as Context; let middleware = trailingSlash({ enabled: true }); @@ -42,16 +38,16 @@ describe(trailingSlash.name, () => { await middleware(c, next); expect(c.redirect).not.toBeCalled(); - expect(next).toHaveBeenCalledOnce(); + expect(next).toHaveBeenCalledTimes(1); }); test("default removes trailing slash if any", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let c = { req: { url: "https://example.com/marketing/", }, - redirect: vi.fn(), + redirect: mock(), } as unknown as Context; let middleware = trailingSlash(); @@ -62,12 +58,12 @@ describe(trailingSlash.name, () => { }); test("default calls `next` if `/`", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let c = { req: { url: "https://example.com/", }, - redirect: vi.fn(), + redirect: mock(), } as unknown as Context; let middleware = trailingSlash(); @@ -75,16 +71,16 @@ describe(trailingSlash.name, () => { await middleware(c, next); expect(c.redirect).not.toBeCalled(); - expect(next).toHaveBeenCalledOnce(); + expect(next).toHaveBeenCalledTimes(1); }); test("default calls `next` if no trailing slash", async () => { - let next = vi.fn().mockResolvedValueOnce(true); + let next = mock().mockResolvedValueOnce(true); let c = { req: { url: "https://example.com/marketing", }, - redirect: vi.fn(), + redirect: mock(), } as unknown as Context; let middleware = trailingSlash(); @@ -92,6 +88,6 @@ describe(trailingSlash.name, () => { await middleware(c, next); expect(c.redirect).not.toBeCalled(); - expect(next).toHaveBeenCalledOnce(); + expect(next).toHaveBeenCalledTimes(1); }); }); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index a445355..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// -/// - -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; - -export default defineConfig({ - plugins: [tsconfigPaths()], - test: { - globals: true, - environment: "edge-runtime", - coverage: { all: true, include: ["src/**"] }, - }, -});