Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
642e81c
ci(ci): move mutation to a release gate on main
systemfsoftware-maker Oct 5, 2026
4924650
ci(ci): run the release gate on github-hosted runners
systemfsoftware-maker Oct 6, 2026
2948df3
fix(ci): refuse a mutation shard whose mutate globs match no files
systemfsoftware-maker Oct 6, 2026
78d23d0
ci(ci): pass the release gate with a notice when there are no decisio…
systemfsoftware-maker Oct 6, 2026
445caba
deps(deps): move to effect 4 stable at exact pins
systemfsoftware-maker Oct 5, 2026
2c50fc8
build(repo): move lint, test and mutation settings to the root
systemfsoftware-maker Oct 5, 2026
330df81
build(repo): declare the effect/http unstable-api opt-in once at the …
systemfsoftware-maker Oct 5, 2026
0f0e072
feat(repo): serve the starter site from one worker
systemfsoftware-maker Oct 6, 2026
9c2cf14
ci(ci): pin the shared release tooling to main, as main does
systemfsoftware-maker Oct 7, 2026
4e03afd
chore(repo): merge lake1/release-gate into lake1/effect4-pins
systemfsoftware-maker Oct 7, 2026
d2df28a
chore(repo): merge lake1/effect4-pins into lake1/shared-configs
systemfsoftware-maker Oct 7, 2026
57771ea
chore(repo): merge lake1/shared-configs into lake1/site
systemfsoftware-maker Oct 7, 2026
6b3c059
ci(ci): run the release gate only on pushes to main
systemfsoftware-maker Oct 7, 2026
0bf9f2f
chore(repo): merge lake1/release-gate into lake1/effect4-pins
systemfsoftware-maker Oct 7, 2026
0c6d6d4
chore(repo): merge lake1/effect4-pins into lake1/shared-configs
systemfsoftware-maker Oct 7, 2026
8592598
chore(repo): merge lake1/shared-configs into lake1/site
systemfsoftware-maker Oct 7, 2026
95bbb06
chore(repo): merge main into lake1/release-gate
systemfsoftware-maker Oct 7, 2026
42bcefb
chore(repo): merge lake1/release-gate into lake1/effect4-pins
systemfsoftware-maker Oct 7, 2026
7b82915
chore(repo): merge lake1/effect4-pins into lake1/shared-configs
systemfsoftware-maker Oct 7, 2026
cd73a2b
chore(repo): merge lake1/shared-configs into lake1/site
systemfsoftware-maker Oct 7, 2026
29d1b80
build(repo): drop darwin from the flake and the toolchain
systemfsoftware-maker Oct 7, 2026
bf35eae
chore(repo): merge lake1/release-gate into lake1/effect4-pins
systemfsoftware-maker Oct 7, 2026
c60d671
chore(repo): merge lake1/effect4-pins into lake1/shared-configs
systemfsoftware-maker Oct 7, 2026
4c03d51
chore(repo): merge lake1/shared-configs into lake1/site
systemfsoftware-maker Oct 7, 2026
b23a8e2
chore(repo): merge main into lake1/release-gate
systemfsoftware-maker Oct 7, 2026
7793d48
chore(repo): merge lake1/release-gate into lake1/effect4-pins
systemfsoftware-maker Oct 7, 2026
06da6d7
deps(deps): move @effect/tsgo to 0.50.0
systemfsoftware-maker Oct 7, 2026
63786d6
chore(repo): merge lake1/effect4-pins into lake1/shared-configs
systemfsoftware-maker Oct 7, 2026
a919416
chore(repo): merge lake1/shared-configs into lake1/site
systemfsoftware-maker Oct 7, 2026
72a10ef
chore(repo): merge main (#35/#36 squashed) into shared-configs
systemfsoftware-maker Oct 7, 2026
b3108ee
chore(repo): merge lake1/shared-configs into lake1/site
systemfsoftware-maker Oct 7, 2026
ca40e7f
chore(repo): merge main (#37 squashed) into site
systemfsoftware-maker Oct 7, 2026
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ reports/
.direnv/
result
result-*
.alchemy/
apps/site/.wrangler/
apps/site/src/routeTree.gen.ts
apps/site/.tanstack/
apps/site-e2e/artifacts/
4 changes: 4 additions & 0 deletions apps/site-e2e/oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineConfig } from 'oxlint'
import shared from '../../oxlint.shared.ts'

export default defineConfig({ extends: [shared] })
26 changes: 26 additions & 0 deletions apps/site-e2e/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"author": "TODO <todo@example.com>",
"description": "Seam-only journeys against the running site Worker",
"devDependencies": {
"@systemfsoftware/effect-gherkin-spec": "catalog:",
"@systemfsoftware/oxlint-config-recommended": "catalog:",
"@systemfsoftware/tsconfig": "catalog:",
"@systemfsoftware/vitest": "catalog:",
"@types/node": "catalog:",
"effect": "catalog:",
"oxlint": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vitest": "catalog:"
},
"license": "Apache-2.0",
"name": "@endgame/site-e2e",
"private": true,
"scripts": {
"journeys": "vitest run",
"lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}",
"typecheck": "tsc --noEmit --incremental"
},
"type": "module",
"version": "0.0.0"
}
77 changes: 77 additions & 0 deletions apps/site-e2e/tests/__fixtures__/site.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Config, ConfigProvider, Data, Duration, Effect } from 'effect'
import { HttpClient, HttpClientError, HttpClientRequest } from 'effect/http'

const REQUEST_TIMEOUT = Duration.seconds(30)

export class SiteRequestTimedOut extends Data.TaggedError('SiteRequestTimedOut')<{
readonly url: string
readonly elapsedMillis: number
}> {
override get message(): string {
return `site request to ${this.url} did not finish within ${this.elapsedMillis}ms`
}
}

export interface FetchedPage {
readonly origin: string
readonly status: number
readonly contentType: string | null
readonly vary: string | null
readonly body: string
}

export interface SiteRequest {
readonly path: string
readonly accept?: string
readonly origin?: string
readonly timeout?: Duration.Input
}

const withDeadline = <A, E, R>(
url: string,
timeout: Duration.Input,
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E | SiteRequestTimedOut, R> =>
Effect.gen(function*() {
const startedAt = performance.now()
return yield* effect.pipe(
Effect.timeoutOrElse({
duration: timeout,
orElse: () =>
Effect.fail(
new SiteRequestTimedOut({ url, elapsedMillis: Math.round(performance.now() - startedAt) }),
),
}),
)
})

export const fetchSite = (
site: SiteRequest,
): Effect.Effect<FetchedPage, SiteRequestTimedOut | HttpClientError.HttpClientError, HttpClient.HttpClient> =>
Effect.gen(function*() {
const configuredOrigin = yield* Effect.orDie(
Config.String('SITE_URL').pipe(
Config.withDefault('http://localhost:1337'),
(config) => config.parse(ConfigProvider.fromEnv()),
),
)
const origin = site.origin ?? configuredOrigin
const client = yield* HttpClient.HttpClient
const base = HttpClientRequest.get(`${origin}${site.path}`)
const request = site.accept === undefined ? base : HttpClientRequest.setHeader(base, 'accept', site.accept)
const timeout = site.timeout ?? REQUEST_TIMEOUT
return yield* withDeadline(
`${origin}${site.path}`,
timeout,
Effect.gen(function*() {
const response = yield* client.execute(request)
return {
origin,
status: response.status,
contentType: response.headers['content-type'] ?? null,
vary: response.headers['vary'] ?? null,
body: yield* Effect.orDie(response.text),
}
}),
)
})
25 changes: 25 additions & 0 deletions apps/site-e2e/tests/home.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Gherkin, it, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec'
import { FetchHttpClient } from 'effect/http'

import { fetchSite } from './__fixtures__/site.fixture'

const Feature = makeFeature({ it })

Feature('Opening the site from the running Worker')
.withLayer(FetchHttpClient.layer)
.live('the Worker runs as a separate process reachable only over a real socket')
.body(({ scenario }) => {
scenario(
'A visitor opening the home page gets the server-rendered page',
Gherkin.Do.pipe(
When('a visitor requests the home page')('response', () => fetchSite({ path: '/' })),
Then('the HTML page carries the starter heading')((s, expect) =>
expect(s.response).toMatchObject({
status: 200,
contentType: expect.stringContaining('text/html'),
body: expect.stringContaining('<h1>Endgame Starter</h1>'),
})
),
),
)
})
30 changes: 30 additions & 0 deletions apps/site-e2e/tests/request-timeout.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Gherkin, it, makeFeature, Then, When } from '@systemfsoftware/effect-gherkin-spec'
import { Effect, Layer } from 'effect'
import { HttpClient } from 'effect/http'

import { fetchSite } from './__fixtures__/site.fixture'

const HangingClient = Layer.succeed(HttpClient.HttpClient, HttpClient.make(() => Effect.never))

const Feature = makeFeature({ it })

Feature('Reaching a site that never answers')
.withLayer(HangingClient)
.live('the deadline is a real 200 milliseconds against a stalled client')
.body(({ scenario }) => {
scenario(
'A request that never gets a response is abandoned at its deadline',
Gherkin.Do.pipe(
When('an agent requests a page with a short deadline')(
'failure',
() => fetchSite({ path: '/', origin: 'http://site.example', timeout: '200 millis' }).pipe(Effect.flip),
),
Then('the failure names the URL and the time it waited')((s, expect) =>
expect(s.failure.message).toSatisfy(
(message) => message.includes('http://site.example/') && /\d+ms$/.test(message),
'the failure names the URL and the elapsed milliseconds',
)
),
),
)
})
9 changes: 9 additions & 0 deletions apps/site-e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "vite/client"],
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["tests", "vitest.config.ts", "oxlint.config.ts"]
}
5 changes: 5 additions & 0 deletions apps/site-e2e/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: { environment: 'node', globals: true, include: ['tests/**/*.integration.test.ts'] },
})
19 changes: 19 additions & 0 deletions apps/site/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as Alchemy from 'alchemy'
import * as Cloudflare from 'alchemy/Cloudflare'
import * as Effect from 'effect/Effect'

import { siteWorker } from './site-worker.ts'

export const Site = Cloudflare.Website.Vite('Site', {
main: siteWorker.main,
compatibility: { date: siteWorker.compatibilityDate, flags: [...siteWorker.compatibilityFlags] },
})

export default Alchemy.Stack(
'Endgame',
{ providers: Cloudflare.providers(), state: Alchemy.localState() },
Effect.gen(function*() {
const site = yield* Site
return { url: site.url }
}),
)
4 changes: 4 additions & 0 deletions apps/site/oxlint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { defineConfig } from 'oxlint'
import shared from '../../oxlint.shared.ts'

export default defineConfig({ extends: [shared] })
44 changes: 44 additions & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"author": "TODO <todo@example.com>",
"description": "The one Worker: the starter's TanStack Start site",
"dependencies": {
"@tanstack/react-router": "catalog:",
"@tanstack/react-start": "catalog:",
"effect": "catalog:",
"react": "catalog:",
"react-dom": "catalog:"
},
"devDependencies": {
"@alchemy.run/frontend-frameworks": "catalog:",
"@babel/core": "catalog:",
"@cloudflare/vite-plugin": "catalog:",
"@cloudflare/workers-types": "catalog:",
"@rolldown/plugin-babel": "catalog:",
"@systemfsoftware/oxlint-config-recommended": "catalog:",
"@systemfsoftware/tsconfig": "catalog:",
"@tanstack/router-cli": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitejs/plugin-react": "catalog:",
"alchemy": "catalog:",
"babel-plugin-react-compiler": "catalog:",
"oxlint": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"wrangler": "catalog:"
},
"license": "Apache-2.0",
"name": "@endgame/site",
"private": true,
"scripts": {
"build": "tsr generate && vite build",
"dev": "alchemy dev",
"generate": "tsr generate",
"preview": "vite preview",
"lint": "f=${OXLINT_FORMAT:-${AGENT:+agent}}; oxlint . --format=${f:-default}",
"typecheck": "tsr generate && tsc --noEmit --incremental && tsc -p tsconfig.node.json --noEmit"
},
"type": "module",
"version": "0.0.0"
}
6 changes: 6 additions & 0 deletions apps/site/site-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const siteWorker = {
name: 'endgame-site',
main: 'src/worker.ts',
compatibilityDate: '2026-10-05',
compatibilityFlags: ['webcrypto_modern_algorithms'],
} as const
11 changes: 11 additions & 0 deletions apps/site/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { createRouter } from '@tanstack/react-router'

import { routeTree } from './routeTree.gen'

export const getRouter = () => createRouter({ routeTree, scrollRestoration: true })

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>
}
}
28 changes: 28 additions & 0 deletions apps/site/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router'
import type { ReactNode } from 'react'

const Document = (props: Readonly<{ children: ReactNode }>) => (
<html lang='en'>
<head>
<HeadContent />
</head>
<body>
{props.children}
<Scripts />
</body>
</html>
)

export const Route = createRootRoute({
component: () => (
<Document>
<Outlet />
</Document>
),
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ title: 'Endgame Starter' },
],
}),
})
12 changes: 12 additions & 0 deletions apps/site/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/')({ component: Home })

function Home() {
return (
<main>
<h1>Endgame Starter</h1>
<p>One Cloudflare Worker, built the systemfsoftware way.</p>
</main>
)
}
5 changes: 5 additions & 0 deletions apps/site/src/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import startHandler from '@tanstack/react-start/server-entry'

export default {
fetch: (request: Request): Promise<Response> => Promise.resolve(startHandler.fetch(request)),
}
20 changes: 20 additions & 0 deletions apps/site/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"types": [
"@cloudflare/workers-types",
"node",
"vite/client"
],
"customConditions": [
"@systemfsoftware/source"
],
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": [
"src",
"alchemy.run.ts"
]
}
14 changes: 14 additions & 0 deletions apps/site/tsconfig.node.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "@systemfsoftware/tsconfig/node",
"compilerOptions": {
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": [
"vite.config.ts",
"oxlint.config.ts",
"site-worker.ts",
"vite-plugins.ts",
"../../*.shared.ts"
]
}
32 changes: 32 additions & 0 deletions apps/site/vite-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cloudflare } from '@cloudflare/vite-plugin'
import babel from '@rolldown/plugin-babel'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact, { reactCompilerPreset } from '@vitejs/plugin-react'

import { siteWorker } from './site-worker.ts'

export const SITE_DEV_PORT = 1337

const alchemyInjectsItsCloudflarePlugin = (): boolean => process.env['ALCHEMY_CLOUDFLARE_VITE_INJECTED'] === '1'

const workerdPlugins = (main: string) =>
alchemyInjectsItsCloudflarePlugin()
? []
: [
cloudflare({
viteEnvironment: { name: 'ssr' },
config: {
name: siteWorker.name,
main,
compatibility_date: siteWorker.compatibilityDate,
compatibility_flags: [...siteWorker.compatibilityFlags],
},
}),
]

export const sitePlugins = (main: string) => [
...workerdPlugins(main),
tanstackStart(),
viteReact(),
babel({ presets: [reactCompilerPreset()] }),
]
Loading
Loading