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/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
+ }
@@ -0,0 +1,5 @@
1
+ /** Namespace used by the application shell's document root. */
2
+ export const shellIdentifierPrefix = "flamefront-shell-"
3
+
4
+ /** Namespace used by the independently-owned routed outlet roots. */
5
+ export const outletIdentifierPrefix = "flamefront-outlet-"