flamefront 0.0.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/LICENSE.md +110 -0
- package/README.md +58 -0
- package/bin/ff-loader.mjs +18 -0
- package/bin/ff.js +6 -0
- package/package.json +84 -8
- package/src/babel.ts +22 -0
- package/src/cli.ts +98 -0
- package/src/entry.ts +49 -0
- package/src/fetch.ts +280 -0
- package/src/fragment-client.ts +271 -0
- package/src/fragment-hydration-client.tsx +81 -0
- package/src/fragment-protocol.ts +39 -0
- package/src/fragment.tsx +656 -0
- package/src/glob.ts +294 -0
- package/src/identifier-prefix.ts +5 -0
- package/src/index.ts +1168 -0
- package/src/lifecycle.ts +487 -0
- package/src/octane-client-core.ts +141 -0
- package/src/octane-client.ts +48 -0
- package/src/octane-compiler.d.ts +19 -0
- package/src/octane-default-renderer.tsx +124 -0
- package/src/octane-router-document.ts +22 -0
- package/src/octane.tsx +661 -0
- package/src/output.ts +48 -0
- package/src/remix-route-data.ts +76 -0
- package/src/remix-router-core.ts +106 -0
- package/src/remix-router.ts +117 -0
- package/src/remove-exports.ts +148 -0
- package/src/route-data-client.ts +234 -0
- package/src/route-prefetch.ts +79 -0
- package/src/server.ts +338 -0
- package/src/srvx.ts +174 -0
- package/src/static-fragment-artifacts.ts +86 -0
- package/src/typegen.ts +207 -0
- package/src/virtual-remix-routes.d.ts +35 -0
- package/src/vite.ts +1030 -0
- package/readme.md +0 -1
package/src/glob.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/** A project-root file discovered by a Flamefront route glob. */
|
|
2
|
+
export interface GlobFile {
|
|
3
|
+
/** Vite project-root module ID, such as `/src/docs/guide.md`. */
|
|
4
|
+
readonly path: string
|
|
5
|
+
/** Path relative to the glob's static directory, with POSIX separators. */
|
|
6
|
+
readonly relativePath: string
|
|
7
|
+
/** Extension-stripped route fragment; directory indexes are empty. */
|
|
8
|
+
readonly route: string
|
|
9
|
+
/** Join the route fragment to an app-relative URL prefix. */
|
|
10
|
+
readonly routePath: (prefix: string) => string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NodeProcessLike {
|
|
14
|
+
cwd?: () => string
|
|
15
|
+
getBuiltinModule?: (name: string) => unknown
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface NodeFileSystem {
|
|
19
|
+
globSync(pattern: string): string[]
|
|
20
|
+
statSync(file: string): { isFile(): boolean }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface NodePath {
|
|
24
|
+
resolve(...paths: string[]): string
|
|
25
|
+
relative(from: string, to: string): string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface NodeRuntime {
|
|
29
|
+
readonly fs: NodeFileSystem
|
|
30
|
+
readonly path: NodePath
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let configuredRoot: string | undefined
|
|
34
|
+
|
|
35
|
+
function nodeProcess(): NodeProcessLike | undefined {
|
|
36
|
+
return (globalThis as typeof globalThis & { process?: NodeProcessLike })
|
|
37
|
+
.process
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nodeRuntime(): NodeRuntime {
|
|
41
|
+
const processValue = nodeProcess()
|
|
42
|
+
const getBuiltinModule = processValue?.getBuiltinModule
|
|
43
|
+
|
|
44
|
+
if (typeof getBuiltinModule !== "function") {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"flamefront glob() can only expand files in a Node route manifest.",
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const fs = getBuiltinModule("node:fs") as NodeFileSystem
|
|
51
|
+
const path = getBuiltinModule("node:path") as NodePath
|
|
52
|
+
|
|
53
|
+
if (
|
|
54
|
+
!fs ||
|
|
55
|
+
typeof fs.globSync !== "function" ||
|
|
56
|
+
typeof fs.statSync !== "function" ||
|
|
57
|
+
!path ||
|
|
58
|
+
typeof path.resolve !== "function" ||
|
|
59
|
+
typeof path.relative !== "function"
|
|
60
|
+
) {
|
|
61
|
+
throw new Error("flamefront glob() requires Node's built-in glob support.")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { fs, path }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeProjectPath(value: string): string {
|
|
68
|
+
const normalized = value.replaceAll("\\", "/")
|
|
69
|
+
|
|
70
|
+
return normalized.startsWith("/") ? normalized : `/${normalized}`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasGlobMagic(segment: string): boolean {
|
|
74
|
+
return ["*", "?", "[", "]", "{", "}"].some((character) =>
|
|
75
|
+
segment.includes(character),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function staticGlobDirectory(pattern: string): string {
|
|
80
|
+
const segments = pattern.split("/")
|
|
81
|
+
const firstMagicSegment = segments.findIndex(hasGlobMagic)
|
|
82
|
+
const directorySegments =
|
|
83
|
+
firstMagicSegment === -1
|
|
84
|
+
? segments.slice(0, -1)
|
|
85
|
+
: segments.slice(0, firstMagicSegment)
|
|
86
|
+
const directory = directorySegments.filter(Boolean).join("/")
|
|
87
|
+
|
|
88
|
+
return directory ? `/${directory}` : "/"
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Join a route prefix with a glob-provided route fragment. */
|
|
92
|
+
export function joinRoutePath(prefix: string, suffix: string): string {
|
|
93
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
94
|
+
throw new TypeError("flamefront route prefix must be a non-empty string.")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!prefix.startsWith("/")) {
|
|
98
|
+
throw new TypeError("flamefront route prefix must start with '/'.")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof suffix !== "string") {
|
|
102
|
+
throw new TypeError("flamefront route suffix must be a string.")
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (prefix.includes("?") || prefix.includes("#")) {
|
|
106
|
+
throw new TypeError(
|
|
107
|
+
"flamefront route prefix must be a pathname without a query or hash.",
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (suffix.includes("?") || suffix.includes("#")) {
|
|
112
|
+
throw new TypeError(
|
|
113
|
+
"flamefront route suffix must be a pathname without a query or hash.",
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalizedPrefix = prefix.replace(/\/+$/, "") || "/"
|
|
118
|
+
const normalizedSuffix = suffix.replace(/^\/+|\/+$/g, "")
|
|
119
|
+
|
|
120
|
+
if (!normalizedSuffix) {
|
|
121
|
+
return normalizedPrefix
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return normalizedPrefix === "/"
|
|
125
|
+
? `/${normalizedSuffix}`
|
|
126
|
+
: `${normalizedPrefix}/${normalizedSuffix}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Resolve the static directory watched by a project-root glob. */
|
|
130
|
+
export function globDirectory(root: string, pattern: string): string {
|
|
131
|
+
if (typeof root !== "string" || root.length === 0) {
|
|
132
|
+
throw new TypeError("flamefront glob root must be a non-empty string.")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
136
|
+
throw new TypeError("flamefront glob pattern must be a non-empty string.")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const runtime = nodeRuntime()
|
|
140
|
+
const projectRoot = runtime.path.resolve(root)
|
|
141
|
+
const projectPattern = normalizeProjectPath(pattern)
|
|
142
|
+
|
|
143
|
+
return runtime.path.resolve(
|
|
144
|
+
projectRoot,
|
|
145
|
+
`.${staticGlobDirectory(projectPattern)}`,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function routeFragment(relativePath: string): string {
|
|
150
|
+
const withoutExtension = relativePath.replace(/\.[^./]+$/, "")
|
|
151
|
+
const segments = withoutExtension.split("/")
|
|
152
|
+
|
|
153
|
+
if (segments.at(-1) === "index") {
|
|
154
|
+
segments.pop()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return segments.join("/")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type GlobFileData = Omit<GlobFile, "routePath">
|
|
161
|
+
|
|
162
|
+
function withRoutePath(file: GlobFileData | GlobFile): GlobFile {
|
|
163
|
+
if (typeof (file as Partial<GlobFile>).routePath === "function") {
|
|
164
|
+
return file as GlobFile
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const route = file.route
|
|
168
|
+
const descriptor = { ...file }
|
|
169
|
+
|
|
170
|
+
Object.defineProperty(descriptor, "routePath", {
|
|
171
|
+
configurable: false,
|
|
172
|
+
enumerable: false,
|
|
173
|
+
value: (prefix: string) => joinRoutePath(prefix, route),
|
|
174
|
+
writable: false,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
return Object.freeze(descriptor) as GlobFile
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isWithin(root: string, candidate: string, runtime: NodeRuntime) {
|
|
181
|
+
const relative = runtime.path.relative(root, candidate)
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
relative === "" || (!relative.startsWith("..") && !relative.startsWith("/"))
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Expand a project-root glob into stable, importable file descriptors. */
|
|
189
|
+
export function expandGlob(root: string, pattern: string): readonly GlobFile[] {
|
|
190
|
+
if (typeof root !== "string" || root.length === 0) {
|
|
191
|
+
throw new TypeError("flamefront glob root must be a non-empty string.")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
195
|
+
throw new TypeError("flamefront glob pattern must be a non-empty string.")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const runtime = nodeRuntime()
|
|
199
|
+
const projectPattern = normalizeProjectPath(pattern)
|
|
200
|
+
const projectRoot = runtime.path.resolve(root)
|
|
201
|
+
const absolutePattern = runtime.path.resolve(
|
|
202
|
+
projectRoot,
|
|
203
|
+
`.${projectPattern}`,
|
|
204
|
+
)
|
|
205
|
+
const globDirectory = staticGlobDirectory(projectPattern)
|
|
206
|
+
const absoluteGlobDirectory = runtime.path.resolve(
|
|
207
|
+
projectRoot,
|
|
208
|
+
`.${globDirectory}`,
|
|
209
|
+
)
|
|
210
|
+
const matches = runtime.fs
|
|
211
|
+
.globSync(absolutePattern)
|
|
212
|
+
.map((match) => runtime.path.resolve(match))
|
|
213
|
+
.filter((match) => runtime.fs.statSync(match).isFile())
|
|
214
|
+
.sort()
|
|
215
|
+
|
|
216
|
+
return Object.freeze(
|
|
217
|
+
matches.map((match) => {
|
|
218
|
+
if (!isWithin(projectRoot, match, runtime)) {
|
|
219
|
+
throw new TypeError(
|
|
220
|
+
`flamefront glob match escapes the project root: ${match}`,
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const relativePath = runtime.path
|
|
225
|
+
.relative(absoluteGlobDirectory, match)
|
|
226
|
+
.replaceAll("\\", "/")
|
|
227
|
+
|
|
228
|
+
if (
|
|
229
|
+
!relativePath ||
|
|
230
|
+
relativePath === ".." ||
|
|
231
|
+
relativePath.startsWith("../")
|
|
232
|
+
) {
|
|
233
|
+
throw new TypeError(
|
|
234
|
+
`flamefront glob match is outside its static directory: ${match}`,
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const projectPath = runtime.path
|
|
239
|
+
.relative(projectRoot, match)
|
|
240
|
+
.replaceAll("\\", "/")
|
|
241
|
+
|
|
242
|
+
return withRoutePath({
|
|
243
|
+
path: `/${projectPath}`,
|
|
244
|
+
relativePath,
|
|
245
|
+
route: routeFragment(relativePath),
|
|
246
|
+
})
|
|
247
|
+
}),
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Set the project root used when a route manifest expands a glob directly. */
|
|
252
|
+
export function setGlobRoot(root: string | undefined): void {
|
|
253
|
+
configuredRoot = root
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Expand a project-root glob and map each discovered file into a route config.
|
|
258
|
+
* The array overload is used by Flamefront's Vite transform for browser builds.
|
|
259
|
+
*/
|
|
260
|
+
export function glob<Result>(
|
|
261
|
+
pattern: string,
|
|
262
|
+
map: (file: GlobFile) => Result,
|
|
263
|
+
): readonly Result[]
|
|
264
|
+
export function glob<Result>(
|
|
265
|
+
files: readonly GlobFile[],
|
|
266
|
+
map: (file: GlobFile) => Result,
|
|
267
|
+
): readonly Result[]
|
|
268
|
+
export function glob<Result>(
|
|
269
|
+
patternOrFiles: string | readonly GlobFile[],
|
|
270
|
+
map: (file: GlobFile) => Result,
|
|
271
|
+
): readonly Result[] {
|
|
272
|
+
if (typeof map !== "function") {
|
|
273
|
+
throw new TypeError("flamefront glob mapper must be a function.")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let files: readonly GlobFile[]
|
|
277
|
+
|
|
278
|
+
if (typeof patternOrFiles === "string") {
|
|
279
|
+
const root =
|
|
280
|
+
configuredRoot ??
|
|
281
|
+
nodeProcess()?.cwd?.() ??
|
|
282
|
+
(() => {
|
|
283
|
+
throw new Error(
|
|
284
|
+
"flamefront glob() requires a Node project root when used outside Vite.",
|
|
285
|
+
)
|
|
286
|
+
})()
|
|
287
|
+
|
|
288
|
+
files = expandGlob(root, patternOrFiles)
|
|
289
|
+
} else {
|
|
290
|
+
files = patternOrFiles
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return Object.freeze(files.map((file) => map(withRoutePath(file))))
|
|
294
|
+
}
|