flamefront 0.0.0 → 0.1.0-alpha.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,13 +1,66 @@
1
1
  {
2
2
  "name": "flamefront",
3
- "version": "0.0.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
3
+ "version": "0.1.0-alpha.0",
4
+ "private": false,
5
+ "description": "Typed centralized route manifests for Octane.",
6
+ "license": "FSL-1.1-MIT",
7
+ "files": [
8
+ "LICENSE.md",
9
+ "README.md",
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "engines": {
14
+ "node": ">=22.22.2"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "bin": {
20
+ "ff": "./bin/ff.js"
21
+ },
22
+ "type": "module",
23
+ "exports": {
24
+ ".": "./src/index.ts",
25
+ "./fragment": "./src/fragment.ts",
26
+ "./octane": "./src/octane.ts",
27
+ "./octane/client": "./src/octane-client.ts",
28
+ "./octane/router-document": "./src/octane-router-document.ts",
29
+ "./remix-router": "./src/remix-router.ts",
30
+ "./remix-router/data": "./src/remix-route-data.ts",
31
+ "./server": "./src/server.ts",
32
+ "./srvx": "./src/srvx.ts",
33
+ "./vite": "./src/vite.ts"
34
+ },
35
+ "dependencies": {
36
+ "@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",
41
+ "@remix-run/route-pattern": "0.24.0",
42
+ "babel-dead-code-elimination": "^1.0.12",
43
+ "srvx": "0.12.5"
8
44
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "All rights reserved",
12
- "type": "commonjs"
13
- }
45
+ "devDependencies": {
46
+ "@types/babel__generator": "^7.27.0",
47
+ "@types/babel__traverse": "^7.28.0",
48
+ "@types/node": "26.1.1",
49
+ "typescript": "5.9.3",
50
+ "vite": "latest"
51
+ },
52
+ "peerDependencies": {
53
+ "@octanejs/remix-router": "0.1.36",
54
+ "octane": "0.1.40",
55
+ "vite": "^8.0.16"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@octanejs/remix-router": {
59
+ "optional": true
60
+ }
61
+ },
62
+ "scripts": {
63
+ "test": "node --test test/*.test.ts",
64
+ "typecheck": "tsc -p tsconfig.json"
65
+ }
66
+ }
package/src/babel.ts ADDED
@@ -0,0 +1,22 @@
1
+ import generatorModule from "@babel/generator"
2
+ import { parse, type ParseResult } from "@babel/parser"
3
+ import type { NodePath } from "@babel/traverse"
4
+ import traverseModule from "@babel/traverse"
5
+ import type * as Babel from "@babel/types"
6
+
7
+ type DefaultImport<T> = T | { default: T }
8
+
9
+ function unwrapDefault<T>(value: DefaultImport<T>): T {
10
+ return (value as { default?: T }).default ?? (value as T)
11
+ }
12
+
13
+ // Babel's CommonJS packages have different shapes across Node and bundlers.
14
+ const traverse = unwrapDefault(
15
+ traverseModule as DefaultImport<typeof import("@babel/traverse").default>,
16
+ )
17
+ const generate = unwrapDefault(
18
+ generatorModule as DefaultImport<typeof import("@babel/generator").default>,
19
+ )
20
+
21
+ export { generate, parse, traverse }
22
+ export type { Babel, NodePath, ParseResult }
package/src/cli.ts ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ binary,
5
+ command,
6
+ flag,
7
+ number,
8
+ option,
9
+ optional,
10
+ run,
11
+ subcommands,
12
+ } from "@alloc/cmd-ts"
13
+ import {
14
+ buildProject,
15
+ devProject,
16
+ loadProject,
17
+ previewProject,
18
+ } from "./lifecycle.ts"
19
+
20
+ const routes = command({
21
+ name: "routes",
22
+ aliases: ["route"],
23
+ description: "List the centralized route manifest for the current app.",
24
+ args: {
25
+ json: flag({
26
+ long: "json",
27
+ description: "Print the manifest as JSON.",
28
+ }),
29
+ },
30
+ async handler({ json }) {
31
+ const { app } = await loadProject()
32
+
33
+ if (json) {
34
+ console.log(JSON.stringify(app.routes, null, 2))
35
+ return
36
+ }
37
+
38
+ for (const route of app.routes) {
39
+ const hydration =
40
+ typeof route.hydration === "object"
41
+ ? JSON.stringify(route.hydration)
42
+ : (route.hydration ?? "default")
43
+
44
+ console.log(`${route.path}\t${route.render}\t${hydration}`)
45
+ }
46
+ },
47
+ })
48
+
49
+ const dev = command({
50
+ name: "dev",
51
+ description: "Start the development server for every render mode.",
52
+ args: {
53
+ port: option({
54
+ long: "port",
55
+ type: optional(number),
56
+ description: "Port for the development server.",
57
+ }),
58
+ },
59
+ handler: ({ port }) => devProject(process.cwd(), port),
60
+ })
61
+
62
+ const build = command({
63
+ name: "build",
64
+ description: "Build client and server bundles, then prerender static routes.",
65
+ args: {},
66
+ handler: () => buildProject(),
67
+ })
68
+
69
+ const preview = command({
70
+ name: "preview",
71
+ description: "Serve a production build locally.",
72
+ args: {},
73
+ handler: () => previewProject(),
74
+ })
75
+
76
+ const cli = subcommands({
77
+ name: "ff",
78
+ version: "0.1.0-alpha.0",
79
+ description: "Flamefront, a small compiler-oriented framework for Octane.",
80
+ cmds: { build, dev, preview, routes },
81
+ })
82
+
83
+ await run(binary(cli), process.argv)
@@ -0,0 +1,231 @@
1
+ import type {
2
+ GeneratedHydration,
3
+ GeneratedRouteMetadata,
4
+ HydrationMode,
5
+ RouteBoundaryKind,
6
+ } from "./index.ts"
7
+ import {
8
+ stripFlamefrontProtocolParams,
9
+ withStaticFragmentProtocol,
10
+ } from "./fragment-protocol.ts"
11
+
12
+ export const staticFragmentProtocol = "flamefront-static-fragment-v1" as const
13
+
14
+ export interface StaticFragmentBoundary {
15
+ readonly id: string
16
+ readonly boundary: string
17
+ readonly kind: RouteBoundaryKind
18
+ readonly parent?: string
19
+ readonly html: string
20
+ }
21
+
22
+ export interface StaticFragmentArtifact {
23
+ readonly protocol: typeof staticFragmentProtocol
24
+ readonly route: string
25
+ readonly boundary: string
26
+ readonly html: string
27
+ readonly routeData: unknown
28
+ readonly boundaries: readonly StaticFragmentBoundary[]
29
+ readonly hydration?: HydrationMode
30
+ readonly status?: number
31
+ }
32
+
33
+ export interface StaticFragmentLoadOptions {
34
+ readonly signal?: AbortSignal
35
+ readonly reload?: boolean
36
+ }
37
+
38
+ export interface StaticFragmentRoutingOptions {
39
+ readonly basename?: string
40
+ }
41
+
42
+ export function shouldHydrateStaticFragment(
43
+ hydration: HydrationMode | undefined,
44
+ ): boolean {
45
+ return hydration !== "none"
46
+ }
47
+
48
+ const fragmentCache = new Map<string, Promise<StaticFragmentArtifact>>()
49
+
50
+ function resolveRouteUrl(input: string | URL): URL {
51
+ const browserOrigin =
52
+ typeof location === "undefined" ? undefined : location.origin
53
+
54
+ if (!browserOrigin && typeof input === "string" && !URL.canParse(input)) {
55
+ throw new TypeError(
56
+ "flamefront static fragments require an absolute URL outside the browser.",
57
+ )
58
+ }
59
+
60
+ return new URL(input, browserOrigin)
61
+ }
62
+
63
+ function basenamePath(pathname: string, basename: string): string | null {
64
+ if (basename === "/") {
65
+ return pathname
66
+ }
67
+
68
+ if (pathname === basename) {
69
+ return "/"
70
+ }
71
+
72
+ if (!pathname.startsWith(`${basename}/`)) {
73
+ return null
74
+ }
75
+
76
+ return pathname.slice(basename.length) || "/"
77
+ }
78
+
79
+ function fragmentKey(input: string | URL, basename = "/"): string {
80
+ const url = stripFlamefrontProtocolParams(resolveRouteUrl(input))
81
+ const pathname = basenamePath(url.pathname, basename) ?? url.pathname
82
+
83
+ return `${url.origin}${pathname}`
84
+ }
85
+
86
+ function abortable<Data>(
87
+ pending: Promise<Data>,
88
+ signal: AbortSignal | undefined,
89
+ ): Promise<Data> {
90
+ if (!signal) {
91
+ return pending
92
+ }
93
+
94
+ if (signal.aborted) {
95
+ return Promise.reject(signal.reason)
96
+ }
97
+
98
+ return new Promise<Data>((resolve, reject) => {
99
+ const onAbort = () => reject(signal.reason)
100
+
101
+ signal.addEventListener("abort", onAbort, { once: true })
102
+ pending.then(
103
+ (value) => {
104
+ signal.removeEventListener("abort", onAbort)
105
+ resolve(value)
106
+ },
107
+ (error) => {
108
+ signal.removeEventListener("abort", onAbort)
109
+ reject(error)
110
+ },
111
+ )
112
+ })
113
+ }
114
+
115
+ export function isStaticFragmentArtifact(
116
+ value: unknown,
117
+ ): value is StaticFragmentArtifact {
118
+ if (!value || typeof value !== "object") {
119
+ return false
120
+ }
121
+
122
+ const artifact = value as Partial<StaticFragmentArtifact>
123
+
124
+ return (
125
+ artifact.protocol === staticFragmentProtocol &&
126
+ typeof artifact.route === "string" &&
127
+ typeof artifact.boundary === "string" &&
128
+ typeof artifact.html === "string" &&
129
+ Array.isArray(artifact.boundaries)
130
+ )
131
+ }
132
+
133
+ export function assertStaticFragmentArtifact(
134
+ value: unknown,
135
+ ): StaticFragmentArtifact {
136
+ if (!isStaticFragmentArtifact(value)) {
137
+ throw new Error(
138
+ "flamefront static fragment response has an invalid protocol.",
139
+ )
140
+ }
141
+
142
+ return value
143
+ }
144
+
145
+ export function getStaticFragment(
146
+ url: string | URL,
147
+ routing: StaticFragmentRoutingOptions = {},
148
+ ): StaticFragmentArtifact | undefined {
149
+ const pending = fragmentCache.get(fragmentKey(url, routing.basename ?? "/"))
150
+
151
+ return pending && "value" in pending
152
+ ? (
153
+ pending as Promise<StaticFragmentArtifact> & {
154
+ value?: StaticFragmentArtifact
155
+ }
156
+ ).value
157
+ : undefined
158
+ }
159
+
160
+ function rememberArtifact(
161
+ key: string,
162
+ pending: Promise<StaticFragmentArtifact>,
163
+ ): Promise<StaticFragmentArtifact> {
164
+ const tracked = pending.then((artifact) => {
165
+ ;(
166
+ tracked as Promise<StaticFragmentArtifact> & {
167
+ value?: StaticFragmentArtifact
168
+ }
169
+ ).value = artifact
170
+ return artifact
171
+ })
172
+
173
+ fragmentCache.set(key, tracked)
174
+ void tracked.catch(() => {
175
+ if (fragmentCache.get(key) === tracked) {
176
+ fragmentCache.delete(key)
177
+ }
178
+ })
179
+ return tracked
180
+ }
181
+
182
+ export function loadStaticFragment(
183
+ url: string | URL,
184
+ routing: StaticFragmentRoutingOptions = {},
185
+ options: StaticFragmentLoadOptions = {},
186
+ ): Promise<StaticFragmentArtifact> {
187
+ const routeUrl = resolveRouteUrl(url)
188
+ const key = fragmentKey(routeUrl, routing.basename ?? "/")
189
+
190
+ if (options.reload) {
191
+ fragmentCache.delete(key)
192
+ }
193
+
194
+ const cached = fragmentCache.get(key)
195
+
196
+ if (cached) {
197
+ return abortable(cached, options.signal)
198
+ }
199
+
200
+ const endpoint = withStaticFragmentProtocol(routeUrl)
201
+ const pending = globalThis
202
+ .fetch(endpoint, {
203
+ headers: { Accept: "application/vnd.flamefront.fragment+json" },
204
+ ...(options.signal ? { signal: options.signal } : {}),
205
+ })
206
+ .then(async (response) => {
207
+ if (!response.ok) {
208
+ throw new Error(
209
+ `flamefront static fragment request failed with ${response.status}.`,
210
+ )
211
+ }
212
+
213
+ return assertStaticFragmentArtifact(await response.json())
214
+ })
215
+
216
+ return abortable(rememberArtifact(key, pending), options.signal)
217
+ }
218
+
219
+ export async function prefetchStaticFragment(
220
+ url: string | URL,
221
+ routing: StaticFragmentRoutingOptions = {},
222
+ options?: StaticFragmentLoadOptions,
223
+ ): Promise<void> {
224
+ await loadStaticFragment(url, routing, options)
225
+ }
226
+
227
+ export type {
228
+ GeneratedHydration,
229
+ GeneratedRouteMetadata,
230
+ HydrationMode,
231
+ } from "./index.ts"
@@ -0,0 +1,39 @@
1
+ export const flamefrontFragmentQueryParam = "__flamefront_fragment"
2
+ export const flamefrontShellQueryParam = "__flamefront_shell"
3
+ export const flamefrontFragmentQueryValue = "1"
4
+
5
+ /** Remove framework-only query parameters before a URL reaches app code. */
6
+ export function stripFlamefrontProtocolParams(input: string | URL): URL {
7
+ const url = new URL(input, "http://flamefront.local")
8
+
9
+ url.searchParams.delete(flamefrontFragmentQueryParam)
10
+ url.searchParams.delete(flamefrontShellQueryParam)
11
+ return url
12
+ }
13
+
14
+ export function isStaticFragmentRequest(input: string | URL): boolean {
15
+ const url = new URL(input, "http://flamefront.local")
16
+
17
+ return (
18
+ url.searchParams.get(flamefrontFragmentQueryParam) ===
19
+ flamefrontFragmentQueryValue
20
+ )
21
+ }
22
+
23
+ /** Mark a route URL for the static-fragment transport. */
24
+ export function withStaticFragmentProtocol(input: string | URL): URL {
25
+ const url = stripFlamefrontProtocolParams(input)
26
+
27
+ url.searchParams.set(
28
+ flamefrontFragmentQueryParam,
29
+ flamefrontFragmentQueryValue,
30
+ )
31
+ return url
32
+ }
33
+
34
+ /** Pass a request to a loader without leaking framework protocol parameters. */
35
+ export function stripFlamefrontProtocolRequest(request: Request): Request {
36
+ const url = stripFlamefrontProtocolParams(request.url)
37
+
38
+ return new Request(url, request)
39
+ }