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/LICENSE.md +110 -0
- package/README.md +643 -0
- package/bin/ff-loader.mjs +18 -0
- package/bin/ff.js +6 -0
- package/package.json +63 -10
- package/src/babel.ts +22 -0
- package/src/cli.ts +83 -0
- package/src/fragment-client.ts +231 -0
- package/src/fragment-protocol.ts +39 -0
- package/src/fragment.ts +260 -0
- package/src/index.ts +648 -0
- package/src/lifecycle.ts +486 -0
- package/src/octane-client-core.ts +134 -0
- package/src/octane-client.ts +42 -0
- package/src/octane-router-document.ts +5 -0
- package/src/octane.ts +654 -0
- package/src/remix-route-data.ts +68 -0
- package/src/remix-router-core.ts +106 -0
- package/src/remix-router.ts +111 -0
- package/src/remove-exports.ts +148 -0
- package/src/route-data-client.ts +224 -0
- package/src/route-prefetch.ts +72 -0
- package/src/server.ts +240 -0
- package/src/srvx.ts +314 -0
- package/src/static-fragment-artifacts.ts +86 -0
- package/src/virtual-remix-routes.d.ts +20 -0
- package/src/vite.ts +693 -0
- package/readme.md +0 -1
package/package.json
CHANGED
|
@@ -1,13 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flamefront",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
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
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|