flamefront 0.1.0-alpha.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,17 +1,16 @@
1
1
  {
2
2
  "name": "flamefront",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0",
4
4
  "private": false,
5
5
  "description": "Typed centralized route manifests for Octane.",
6
6
  "license": "FSL-1.1-MIT",
7
+ "repository": "https://github.com/aleclarson/flamefront",
7
8
  "files": [
8
- "LICENSE.md",
9
- "README.md",
10
9
  "bin",
11
10
  "src"
12
11
  ],
13
12
  "engines": {
14
- "node": ">=22.22.2"
13
+ "node": ">=26.0.0"
15
14
  },
16
15
  "publishConfig": {
17
16
  "access": "public"
@@ -22,45 +21,69 @@
22
21
  "type": "module",
23
22
  "exports": {
24
23
  ".": "./src/index.ts",
25
- "./fragment": "./src/fragment.ts",
26
- "./octane": "./src/octane.ts",
24
+ "./entry": "./src/entry.ts",
25
+ "./fetch": "./src/fetch.ts",
26
+ "./fragment": "./src/fragment.tsx",
27
+ "./octane": "./src/octane.tsx",
27
28
  "./octane/client": "./src/octane-client.ts",
28
29
  "./octane/router-document": "./src/octane-router-document.ts",
29
30
  "./remix-router": "./src/remix-router.ts",
30
31
  "./remix-router/data": "./src/remix-route-data.ts",
31
32
  "./server": "./src/server.ts",
33
+ "./output": "./src/output.ts",
32
34
  "./srvx": "./src/srvx.ts",
33
35
  "./vite": "./src/vite.ts"
34
36
  },
37
+ "scripts": {
38
+ "dev": "pnpm --filter flamefront-playground dev",
39
+ "build": "pnpm --filter flamefront-playground build",
40
+ "preview": "pnpm --filter flamefront-playground preview",
41
+ "typegen": "pnpm --filter flamefront-playground typegen",
42
+ "routes": "pnpm --filter flamefront-playground routes",
43
+ "typecheck": "tsc -p tsconfig.json && pnpm --filter flamefront-playground typecheck",
44
+ "test": "vitest run",
45
+ "test:e2e": "pnpm --filter flamefront-playground build && vitest run --config vitest.e2e.config.ts",
46
+ "check": "oxlint && prettier --check . && pnpm typecheck && pnpm test && pnpm test:e2e",
47
+ "format": "oxlint --fix && prettier --write ."
48
+ },
35
49
  "dependencies": {
36
50
  "@alloc/cmd-ts": "0.17.1",
37
- "@babel/generator": "^7.29.8",
38
- "@babel/parser": "^7.29.8",
39
- "@babel/traverse": "^7.29.8",
40
- "@babel/types": "^7.29.8",
51
+ "@babel/generator": "^8.0.5",
52
+ "@babel/parser": "^8.0.5",
53
+ "@babel/traverse": "^8.0.5",
54
+ "@babel/types": "^8.0.5",
41
55
  "@remix-run/route-pattern": "0.24.0",
42
- "babel-dead-code-elimination": "^1.0.12",
43
- "srvx": "0.12.5"
56
+ "babel-dead-code-elimination": "^2.0.0",
57
+ "satteri": "^0.10.5",
58
+ "srvx": "1.0.4",
59
+ "vite-plugin-satteri": "^0.3.5"
44
60
  },
45
61
  "devDependencies": {
62
+ "@octanejs/remix-router": "0.1.48",
63
+ "@octanejs/vite-plugin": "0.1.54",
64
+ "@stylistic/eslint-plugin": "^5.10.0",
65
+ "@tsrx/prettier-plugin": "^0.3.137",
46
66
  "@types/babel__generator": "^7.27.0",
47
67
  "@types/babel__traverse": "^7.28.0",
48
- "@types/node": "26.1.1",
68
+ "@types/node": "26.5.1",
69
+ "octane": "0.2.7",
70
+ "oxc-tsrx": "0.6.1",
71
+ "oxlint": "^1.82.0",
72
+ "oxlint-tsgolint": "^7.0.2001",
73
+ "playwright": "1.63.0",
74
+ "prettier": "^3.9.6",
49
75
  "typescript": "5.9.3",
50
- "vite": "latest"
76
+ "vite": "8.3.0",
77
+ "vitest": "^5.0.0"
51
78
  },
52
79
  "peerDependencies": {
53
- "@octanejs/remix-router": "0.1.36",
54
- "octane": "0.1.40",
55
- "vite": "^8.0.16"
80
+ "@octanejs/remix-router": "0.1.48",
81
+ "octane": "0.2.7",
82
+ "vite": "^8.3.0"
56
83
  },
57
84
  "peerDependenciesMeta": {
58
85
  "@octanejs/remix-router": {
59
86
  "optional": true
60
87
  }
61
- },
62
- "scripts": {
63
- "test": "node --test test/*.test.ts",
64
- "typecheck": "tsc -p tsconfig.json"
65
88
  }
66
- }
89
+ }
package/src/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { relative } from "node:path"
3
4
  import {
4
5
  binary,
5
6
  command,
@@ -16,6 +17,7 @@ import {
16
17
  loadProject,
17
18
  previewProject,
18
19
  } from "./lifecycle.ts"
20
+ import { generateProjectTypes } from "./typegen.ts"
19
21
 
20
22
  const routes = command({
21
23
  name: "routes",
@@ -73,11 +75,24 @@ const preview = command({
73
75
  handler: () => previewProject(),
74
76
  })
75
77
 
78
+ const typegen = command({
79
+ name: "typegen",
80
+ description: "Generate route import declarations for the current app.",
81
+ args: {},
82
+ async handler() {
83
+ const result = await generateProjectTypes()
84
+ const status = result.written ? "Generated" : "Unchanged"
85
+ const file = relative(process.cwd(), result.file) || "."
86
+
87
+ console.log(`${status} ${file}.`)
88
+ },
89
+ })
90
+
76
91
  const cli = subcommands({
77
92
  name: "ff",
78
93
  version: "0.1.0-alpha.0",
79
94
  description: "Flamefront, a small compiler-oriented framework for Octane.",
80
- cmds: { build, dev, preview, routes },
95
+ cmds: { build, dev, preview, routes, typegen },
81
96
  })
82
97
 
83
98
  await run(binary(cli), process.argv)
package/src/entry.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { RouteDefinition } from "./index.ts"
2
+
3
+ export type {
4
+ FetchMiddleware,
5
+ FetchServerAssets,
6
+ FetchServerEntryOptions,
7
+ FlamefrontFetchServerEntry,
8
+ ResponseHeaders,
9
+ ResponseHeadersContext,
10
+ ResponseHeadersHook,
11
+ ServerDocuments,
12
+ ServerEntryLifecycle,
13
+ StaticFragmentContext,
14
+ StaticFragmentLoader,
15
+ TemplateContext,
16
+ TemplateLoader,
17
+ } from "./fetch.ts"
18
+ export type {
19
+ FlamefrontServerEntry,
20
+ ServerAssets,
21
+ SrvxMiddleware,
22
+ SrvxServerEntryOptions,
23
+ } from "./srvx.ts"
24
+
25
+ import type {
26
+ FetchServerEntryOptions,
27
+ FlamefrontFetchServerEntry,
28
+ } from "./fetch.ts"
29
+ import type { FlamefrontServerEntry, SrvxServerEntryOptions } from "./srvx.ts"
30
+
31
+ export type ServerEntryOptions<
32
+ Route extends RouteDefinition = RouteDefinition,
33
+ > = FetchServerEntryOptions<Route> | SrvxServerEntryOptions<Route>
34
+
35
+ export type ServerEntry = FlamefrontFetchServerEntry | FlamefrontServerEntry
36
+
37
+ /**
38
+ * Create the server entry selected by the `flamefront` Vite plugin options.
39
+ * Vite replaces this placeholder with the generated adapter module at build
40
+ * time; the direct-import error prevents an accidental unconfigured entry.
41
+ */
42
+ export function createServerEntry<
43
+ Route extends RouteDefinition = RouteDefinition,
44
+ >(options: ServerEntryOptions<Route>): ServerEntry {
45
+ void options
46
+ throw new Error(
47
+ "flamefront/entry must be loaded through the flamefront Vite plugin.",
48
+ )
49
+ }
package/src/fetch.ts ADDED
@@ -0,0 +1,280 @@
1
+ import {
2
+ joinBasename,
3
+ type AppDefinition,
4
+ type RouteDefinition,
5
+ } from "./index.ts"
6
+ import type { OctaneDocuments } from "./octane.tsx"
7
+ import type { DocumentMode, RenderedDocument } from "./server.ts"
8
+ import {
9
+ isRouteFragmentRequest,
10
+ stripFlamefrontProtocolRequest,
11
+ } from "./fragment-protocol.ts"
12
+ import type { RouteFragmentArtifact } from "./fragment-client.ts"
13
+
14
+ export interface TemplateContext<
15
+ Route extends RouteDefinition = RouteDefinition,
16
+ > {
17
+ readonly request: Request
18
+ readonly route: Route | null
19
+ readonly mode: DocumentMode
20
+ }
21
+
22
+ export type TemplateLoader<Route extends RouteDefinition = RouteDefinition> = (
23
+ context: TemplateContext<Route>,
24
+ ) => string | Promise<string>
25
+
26
+ export interface StaticFragmentContext<
27
+ Route extends RouteDefinition = RouteDefinition,
28
+ > {
29
+ readonly request: Request
30
+ readonly route: Route
31
+ }
32
+
33
+ /** Load a pre-rendered static fragment from the host's asset system. */
34
+ export type StaticFragmentLoader<
35
+ Route extends RouteDefinition = RouteDefinition,
36
+ > = (
37
+ context: StaticFragmentContext<Route>,
38
+ ) =>
39
+ RouteFragmentArtifact | undefined | Promise<RouteFragmentArtifact | undefined>
40
+
41
+ export interface ResponseHeadersContext<
42
+ Route extends RouteDefinition = RouteDefinition,
43
+ > {
44
+ readonly request: Request
45
+ readonly route: Route | null
46
+ readonly mode: DocumentMode
47
+ readonly document: RenderedDocument
48
+ }
49
+
50
+ export type ResponseHeaders = HeadersInit
51
+
52
+ /** Add or override response headers after document rendering. */
53
+ export type ResponseHeadersHook<
54
+ Route extends RouteDefinition = RouteDefinition,
55
+ > = (
56
+ context: ResponseHeadersContext<Route>,
57
+ ) => ResponseHeaders | Promise<ResponseHeaders>
58
+
59
+ export type FetchMiddleware = (
60
+ request: Request,
61
+ next: () => Response | Promise<Response>,
62
+ ) => Response | Promise<Response>
63
+
64
+ export type ServerDocuments = Pick<
65
+ OctaneDocuments,
66
+ "renderDocument" | "loadRouteData"
67
+ > &
68
+ Partial<Pick<OctaneDocuments, "renderFragment">>
69
+
70
+ /** Lifecycle operations shared by all Flamefront server adapters. */
71
+ export interface ServerEntryLifecycle {
72
+ readonly renderDocument: OctaneDocuments["renderDocument"]
73
+ readonly loadRouteData: OctaneDocuments["loadRouteData"]
74
+ readonly renderFragment?: OctaneDocuments["renderFragment"]
75
+ }
76
+
77
+ /** A Web Fetch-compatible server entry. */
78
+ export interface FlamefrontFetchServerEntry extends ServerEntryLifecycle {
79
+ readonly fetch: (request: Request) => Response | Promise<Response>
80
+ }
81
+
82
+ export interface FetchServerAssets<
83
+ Route extends RouteDefinition = RouteDefinition,
84
+ > {
85
+ readonly loadTemplate: TemplateLoader<Route>
86
+ readonly loadStaticFragment?: StaticFragmentLoader<Route>
87
+ }
88
+
89
+ export interface FetchServerEntryOptions<
90
+ Route extends RouteDefinition = RouteDefinition,
91
+ > {
92
+ readonly app: AppDefinition<Route>
93
+ readonly documents: ServerDocuments
94
+ readonly assets: FetchServerAssets<Route>
95
+ /** Applied outermost first, in declaration order. */
96
+ readonly middleware?: readonly FetchMiddleware[]
97
+ readonly headers?: ResponseHeadersHook<Route>
98
+ }
99
+
100
+ function mergeHeaders(target: Headers, source: HeadersInit | undefined): void {
101
+ if (source === undefined) {
102
+ return
103
+ }
104
+
105
+ for (const [name, value] of new Headers(source)) {
106
+ target.set(name, value)
107
+ }
108
+ }
109
+
110
+ function composeMiddleware(
111
+ middleware: readonly FetchMiddleware[],
112
+ handler: (request: Request) => Promise<Response>,
113
+ ): (request: Request) => Promise<Response> {
114
+ return middleware.reduceRight<(request: Request) => Promise<Response>>(
115
+ (next, current) => async (request) => current(request, () => next(request)),
116
+ handler,
117
+ )
118
+ }
119
+
120
+ /**
121
+ * Create the transport-neutral Flamefront request handler. Hosts can expose
122
+ * the returned `fetch` function directly or wrap it in their own adapter.
123
+ */
124
+ export function createFetchServerEntry<
125
+ Route extends RouteDefinition = RouteDefinition,
126
+ >(options: FetchServerEntryOptions<Route>): FlamefrontFetchServerEntry {
127
+ const defaultClientRoute = options.app.routes.find(
128
+ (route) => route.render === "client",
129
+ )
130
+
131
+ const handleRequest = async (request: Request): Promise<Response> => {
132
+ const url = new URL(request.url)
133
+ const match = options.app.match(url)
134
+
135
+ try {
136
+ if (
137
+ url.pathname === options.app.routing.basename &&
138
+ !match &&
139
+ defaultClientRoute
140
+ ) {
141
+ return new Response(null, {
142
+ status: 302,
143
+ headers: {
144
+ Location: joinBasename(
145
+ options.app.routing.basename,
146
+ defaultClientRoute.path,
147
+ ),
148
+ },
149
+ })
150
+ }
151
+
152
+ if (url.pathname === options.app.routing.dataPath) {
153
+ return options.documents.loadRouteData(request)
154
+ }
155
+
156
+ if (isRouteFragmentRequest(url)) {
157
+ const sanitizedRequest = stripFlamefrontProtocolRequest(request)
158
+ const fragmentMatch = options.app.match(sanitizedRequest.url)
159
+
160
+ if (!fragmentMatch || fragmentMatch.data.render === "client") {
161
+ return new Response("Not found", { status: 404 })
162
+ }
163
+
164
+ let artifact: RouteFragmentArtifact | undefined
165
+
166
+ if (
167
+ fragmentMatch.data.render === "static" &&
168
+ options.assets.loadStaticFragment
169
+ ) {
170
+ artifact = await options.assets.loadStaticFragment({
171
+ request: sanitizedRequest,
172
+ route: fragmentMatch.data,
173
+ })
174
+ }
175
+
176
+ if (!artifact && options.documents.renderFragment) {
177
+ artifact = await options.documents.renderFragment(sanitizedRequest)
178
+ }
179
+
180
+ if (!artifact) {
181
+ return new Response("Not found", { status: 404 })
182
+ }
183
+
184
+ const responseHeaders = new Headers({
185
+ "Content-Type":
186
+ "application/vnd.flamefront.fragment+json; charset=utf-8",
187
+ })
188
+
189
+ if (options.headers) {
190
+ mergeHeaders(
191
+ responseHeaders,
192
+ await options.headers({
193
+ request: sanitizedRequest,
194
+ route: fragmentMatch.data,
195
+ mode: fragmentMatch.data.render,
196
+ document: {
197
+ html: artifact.html,
198
+ routeData: artifact.routeData,
199
+ status: artifact.status,
200
+ },
201
+ }),
202
+ )
203
+ }
204
+
205
+ return new Response(JSON.stringify(artifact), {
206
+ status: artifact.status ?? 200,
207
+ headers: responseHeaders,
208
+ })
209
+ }
210
+
211
+ if (!match) {
212
+ return new Response("Not found", { status: 404 })
213
+ }
214
+
215
+ const mode: DocumentMode = url.searchParams.has("__flamefront_shell")
216
+ ? "shell"
217
+ : match.data.render
218
+ const template = await options.assets.loadTemplate({
219
+ request,
220
+ route: match.data,
221
+ mode,
222
+ })
223
+ const document = await options.documents.renderDocument(
224
+ template,
225
+ request,
226
+ { mode },
227
+ )
228
+ const responseHeaders = new Headers({
229
+ "Content-Type": "text/html; charset=utf-8",
230
+ })
231
+
232
+ mergeHeaders(responseHeaders, document.headers)
233
+ if (options.headers) {
234
+ mergeHeaders(
235
+ responseHeaders,
236
+ await options.headers({
237
+ request,
238
+ route: match.data,
239
+ mode,
240
+ document,
241
+ }),
242
+ )
243
+ }
244
+
245
+ return new Response(document.html, {
246
+ status: document.status ?? 200,
247
+ headers: responseHeaders,
248
+ })
249
+ } catch (error) {
250
+ if (error instanceof Response) {
251
+ return error
252
+ }
253
+
254
+ throw error
255
+ }
256
+ }
257
+
258
+ const composedFetch = composeMiddleware(
259
+ options.middleware ?? [],
260
+ handleRequest,
261
+ )
262
+ const fetch = async (request: Request): Promise<Response> => {
263
+ try {
264
+ return await composedFetch(request)
265
+ } catch (error) {
266
+ if (error instanceof Response) {
267
+ return error
268
+ }
269
+
270
+ throw error
271
+ }
272
+ }
273
+
274
+ return {
275
+ fetch,
276
+ renderDocument: options.documents.renderDocument,
277
+ loadRouteData: options.documents.loadRouteData,
278
+ renderFragment: options.documents.renderFragment,
279
+ }
280
+ }