Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Code must pass linting, formatting, and typechecking. Use `bun run check` to run
- `packages/cloudflare-runtime`: Effect-native local runtime for Cloudflare Workers, powered by `workerd`.
- `packages/cloudflare-vite-plugin`: Vite plugin for Cloudflare Workers; composes `cloudflare-rolldown-plugin` and `cloudflare-runtime`.
- `packages/framework-core`: Shared framework-integration core: the `BuildOutput` contract, the `alchemy:build-output` Vite collector plugin, the project module loader, and the `Framework` service contract.
- `packages/nextjs`: Wrangler-free Next.js integration built on the `@opennextjs/cloudflare` build pipeline; implements the framework-core `Framework` service (build + final bundle pass + cloudflare-runtime dev).
- `packages/sveltekit`: Wrangler-free SvelteKit integration implementing the `Framework` service (in-memory kit adapter + rolldown re-bundle for workerd; dev via kit's own Vite server with a stub platform).
- `packages/waku`: Wrangler-free Waku integration implementing the `Framework` service (programmatic build/dev over `cloudflare-vite-plugin`, plus a fork of waku's cloudflare adapter).

Expand Down
48 changes: 48 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions fixtures/nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.next/
.open-next/
next-env.d.ts
48 changes: 48 additions & 0 deletions fixtures/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# fixtures/nextjs

E2e fixture for `@distilled.cloud/nextjs` — the wrangler-free Next.js
(OpenNext-based) framework integration.

The app exercises the OpenNext long tail:

- an SSR app-router page (`/`, `force-dynamic`)
- an API route (`/api/hello`)
- an API route reading a `Text` binding via `getCloudflareContext()`
(`/api/binding`)
- an ISR page (`/isr`) — asserts the prerendered payload serves from the
read-only static-assets incremental cache (revalidation writes are a known
dev-v1 gap; a writable local cache backend is a later phase)
- an edge middleware rewrite (`/mw-rewrite` → `/api/hello`) plus a response
header on `/api/*`
- static assets (`public/static.txt` and `_next/static/*` client chunks)
- a client-interactive page (`/counter`) proving hydration

## Commands

```sh
bun run build # e2e build — OpenNext pipeline + final bundle pass -> dist/build.json
bun run preview # e2e preview — miniflare over dist/build.json
bun run dev # e2e dev --port 3104 — built worker under cloudflare-runtime (workerd)
bun run test # build + playwright (live = miniflare, dev = cloudflare-runtime)
```

## Notes

- `wrangler` resolves to the inert stub in
`packages/nextjs/wrangler-stub` — only its `package.json` version field is
ever read (OpenNext's `ensureNextjsVersionSupported`); no wrangler code is
installed or executed.
- The OpenNext pipeline runs `npx next build` internally (the fixture's own
`build` script is `e2e build`, which would recurse).
- Dev (default `"preview"` mode) is production parity: the built worker
served by workerd with `ASSETS` (run-worker-first), `WORKER_SELF_REFERENCE`
(self service binding), and the same-script SQLite `DOQueueHandler` durable
object. No HMR.
- `test/hmr.test.ts` exercises dev v2 (`dev: { mode: "hmr" }`): the real
`next dev` (Turbopack HMR) in Node with the `TEST_TEXT` binding proxied
from cloudflare-runtime onto OpenNext's `getCloudflareContext()` contract —
no `initOpenNextCloudflareForDev()`, no wrangler. The spec builds its own
`Framework` layer (the harness `dev` fixture stays wired to preview mode);
`scripts/hmr-repro.mjs` is a minimal manual runner for the same path. App
code runs in Node in this mode — CF-specific behavior and ISR/caching
semantics still need preview (see `packages/nextjs/README.md`).
8 changes: 8 additions & 0 deletions fixtures/nextjs/app/api/binding/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getCloudflareContext } from "@opennextjs/cloudflare";

export const dynamic = "force-dynamic";

export function GET() {
const { env } = getCloudflareContext();
return Response.json({ value: env.TEST_TEXT ?? null });
}
9 changes: 9 additions & 0 deletions fixtures/nextjs/app/api/hello/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const dynamic = "force-dynamic";

export function GET(request) {
return Response.json({
hello: "world",
url: request.url,
now: Date.now(),
});
}
15 changes: 15 additions & 0 deletions fixtures/nextjs/app/counter/counter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"use client";

import { useState } from "react";

export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p data-testid="count">count:{count}</p>
<button type="button" data-testid="increment" onClick={() => setCount((c) => c + 1)}>
Increment
</button>
</div>
);
}
12 changes: 12 additions & 0 deletions fixtures/nextjs/app/counter/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Counter from "./counter.jsx";

export const dynamic = "force-dynamic";

export default function CounterPage() {
return (
<main>
<h1>Counter page</h1>
<Counter />
</main>
);
}
16 changes: 16 additions & 0 deletions fixtures/nextjs/app/hmr-probe/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getCloudflareContext } from "@opennextjs/cloudflare";

export const dynamic = "force-dynamic";

// Dedicated page for the hmr-mode dev spec (test/hmr.test.ts): the spec
// temporarily edits the marker below and polls until the dev server serves
// the change, then restores this file. Keep the marker on its own line.
export default function HmrProbe() {
const { env } = getCloudflareContext();
return (
<main>
<h1 data-testid="hmr-marker">hmr-probe marker:v1</h1>
<p data-testid="hmr-binding">hmr-binding:{env.TEST_TEXT ?? "missing"}</p>
</main>
);
}
13 changes: 13 additions & 0 deletions fixtures/nextjs/app/isr/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// A long revalidate window: the tests assert the *prerendered* payload
// serves (same stamp on repeated hits), not a revalidation round-trip —
// the static-assets incremental cache is read-only.
export const revalidate = 3600;

export default function IsrPage() {
return (
<main>
<h1>ISR page</h1>
<p data-testid="isr-rendered-at">isr-rendered-at:{Date.now()}</p>
</main>
);
}
11 changes: 11 additions & 0 deletions fixtures/nextjs/app/layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const metadata = {
title: "fixtures-nextjs",
};

export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
15 changes: 15 additions & 0 deletions fixtures/nextjs/app/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from "next/link";

export const dynamic = "force-dynamic";

export default function Home() {
return (
<main>
<h1>fixtures-nextjs SSR page</h1>
<p data-testid="rendered-at">rendered-at:{Date.now()}</p>
<Link href="/counter" data-testid="counter-link">
Counter
</Link>
</main>
);
}
38 changes: 38 additions & 0 deletions fixtures/nextjs/e2e.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Text } from "@distilled.cloud/cloudflare-runtime/bindings";
import * as Options from "@distilled.cloud/e2e/Options";
import { kCurrentWorker } from "miniflare";

export default Options.make({
// The Next.js (OpenNext-based) Framework implementation, resolved from this
// fixture's own node_modules.
framework: "@distilled.cloud/nextjs",
vite: {
compatibilityDate: "2026-05-12",
compatibilityFlags: ["nodejs_compat", "global_fetch_strictly_public"],
worker: {
name: "fixtures-nextjs",
bindings: [Text.local("TEST_TEXT", "hello-from-binding")],
},
},
miniflare: {
name: "fixtures-nextjs",
compatibilityDate: "2026-05-12",
compatibilityFlags: ["nodejs_compat", "global_fetch_strictly_public"],
bindings: { TEST_TEXT: "hello-from-binding" },
serviceBindings: { WORKER_SELF_REFERENCE: kCurrentWorker },
durableObjects: {
NEXT_CACHE_DO_QUEUE: { className: "DOQueueHandler", useSQLite: true },
},
assets: {
binding: "ASSETS",
routerConfig: {
has_user_worker: true,
invoke_user_worker_ahead_of_assets: true,
},
assetConfig: {
html_handling: "none",
not_found_handling: "none",
},
},
},
});
18 changes: 18 additions & 0 deletions fixtures/nextjs/middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";

export function middleware(request) {
const url = new URL(request.url);
if (url.pathname === "/mw-rewrite") {
const rewritten = new URL("/api/hello", request.url);
return NextResponse.rewrite(rewritten, {
headers: { "x-fixture-middleware": "rewrote" },
});
}
const response = NextResponse.next();
response.headers.set("x-fixture-middleware", "passed");
return response;
}

export const config = {
matcher: ["/mw-rewrite", "/api/:path*"],
};
4 changes: 4 additions & 0 deletions fixtures/nextjs/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import("next").NextConfig} */
const nextConfig = {};

export default nextConfig;
13 changes: 13 additions & 0 deletions fixtures/nextjs/open-next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache";
import memoryQueue from "@opennextjs/cloudflare/overrides/queue/memory-queue";

export default defineCloudflareConfig({
// Read-only incremental cache backed by the ASSETS binding — no KV/R2/D1
// required, so the fixture stays fully local. ISR pages serve their
// prerendered payloads; revalidation writes are no-ops (a known dev-v1
// limitation until cloudflare-runtime grows local writable storage).
incrementalCache: staticAssetsIncrementalCache,
// Exercises the WORKER_SELF_REFERENCE service binding for revalidation.
queue: memoryQueue,
});
30 changes: 30 additions & 0 deletions fixtures/nextjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@fixtures/nextjs",
"private": true,
"type": "module",
"scripts": {
"dev": "e2e dev --port 3104",
"build": "e2e build",
"preview": "e2e preview",
"test": "node scripts/e2e.mjs",
"pretest": "playwright install chromium",
"test:e2e": "bun run build && playwright test"
},
"dependencies": {
"next": "16.2.10",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@distilled.cloud/cloudflare-runtime": "workspace:*",
"@distilled.cloud/e2e": "workspace:*",
"@distilled.cloud/framework-core": "workspace:*",
"@distilled.cloud/nextjs": "workspace:*",
"@effect/platform-node": "catalog:effect",
"@opennextjs/cloudflare": "1.20.1",
"@playwright/test": "catalog:",
"effect": "catalog:effect",
"miniflare": "catalog:workers",
"wrangler": "file:../../packages/nextjs/wrangler-stub"
}
}
34 changes: 34 additions & 0 deletions fixtures/nextjs/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineConfig } from "@playwright/test";

export default defineConfig({
testDir: "./test",
// Windows CI runs every fixture e2e concurrently; absorb runner flakiness.
retries: process.env.CI ? 2 : 0,
// The dev worker fixture runs a full OpenNext build on start (preview
// parity — no build.json reuse), so keep generous timeouts.
timeout: 120_000,
// Serialize workers: the dev fixture's OpenNext build rewrites
// `.open-next/assets` on disk, which a concurrently-running live
// (miniflare) worker serves from — parallel workers race on it.
workers: 1,
expect: {
timeout: 10_000,
},
build: {
// The hmr spec runs `next dev` inside the playwright worker process;
// keep playwright's babel require-hook away from Turbopack's dev chunks
// (their sectioned source maps break it with BABEL_GENERATE_ERROR).
external: ["**/.next/**"],
},
projects: [
{
name: "chromium",
use: {
browserName: "chromium",
colorScheme: "light",
deviceScaleFactor: 1,
viewport: { width: 1280, height: 720 },
},
},
],
});
1 change: 1 addition & 0 deletions fixtures/nextjs/public/static.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello from a static asset
Loading