flamefront 0.1.0-alpha.0 → 0.1.1
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 +2 -91
- package/README.md +35 -620
- package/package.json +49 -26
- package/src/cli.ts +16 -1
- package/src/entry.ts +49 -0
- package/src/fetch.ts +280 -0
- package/src/fragment-client.ts +127 -87
- package/src/fragment-hydration-client.tsx +81 -0
- package/src/fragment-protocol.ts +3 -3
- package/src/fragment.tsx +656 -0
- package/src/glob.ts +294 -0
- package/src/identifier-prefix.ts +5 -0
- package/src/index.ts +569 -49
- package/src/lifecycle.ts +7 -6
- package/src/octane-client-core.ts +10 -3
- package/src/octane-client.ts +12 -6
- package/src/octane-compiler.d.ts +19 -0
- package/src/octane-default-renderer.tsx +124 -0
- package/src/octane-router-document.ts +19 -2
- package/src/{octane.ts → octane.tsx} +152 -145
- package/src/output.ts +48 -0
- package/src/remix-route-data.ts +28 -20
- package/src/remix-router.ts +10 -4
- package/src/route-data-client.ts +22 -12
- package/src/route-prefetch.ts +19 -12
- package/src/server.ts +122 -24
- package/src/srvx.ts +43 -183
- package/src/typegen.ts +207 -0
- package/src/virtual-remix-routes.d.ts +16 -1
- package/src/vite.ts +364 -27
- package/src/fragment.ts +0 -260
package/src/index.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createMultiMatcher,
|
|
3
3
|
type Match,
|
|
4
|
+
type MatchParams,
|
|
4
5
|
type MultiMatcher,
|
|
5
6
|
} from "@remix-run/route-pattern/match"
|
|
7
|
+
import {
|
|
8
|
+
createHref as createPatternHref,
|
|
9
|
+
type CreateHrefArgs,
|
|
10
|
+
} from "@remix-run/route-pattern/href"
|
|
6
11
|
import type { HydrationInteractionEvents } from "octane/hydration"
|
|
7
12
|
import { createRouteDataClient } from "./route-data-client.ts"
|
|
8
13
|
import { stripFlamefrontProtocolParams } from "./fragment-protocol.ts"
|
|
9
14
|
|
|
15
|
+
export { glob, joinRoutePath } from "./glob.ts"
|
|
16
|
+
export type { GlobFile } from "./glob.ts"
|
|
17
|
+
|
|
10
18
|
export type RenderMode = "client" | "server" | "static"
|
|
11
19
|
|
|
12
20
|
export interface RoutingOptions {
|
|
@@ -51,6 +59,18 @@ export type GeneratedHydration =
|
|
|
51
59
|
*/
|
|
52
60
|
export type HydrationMode = "full" | "deferred" | "none" | GeneratedHydration
|
|
53
61
|
|
|
62
|
+
/** App-level fallbacks for routes that do not declare a hydration policy. */
|
|
63
|
+
export interface HydrationDefaults {
|
|
64
|
+
readonly server?: HydrationMode
|
|
65
|
+
readonly static?: HydrationMode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Fully resolved app-level hydration fallbacks. */
|
|
69
|
+
export interface NormalizedHydrationDefaults {
|
|
70
|
+
readonly server: HydrationMode
|
|
71
|
+
readonly static: HydrationMode
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
export type RouteNavigationStrategy = "router" | "fragment"
|
|
55
75
|
|
|
56
76
|
export type RouteBoundaryKind = "shell" | "layout" | "route"
|
|
@@ -58,7 +78,7 @@ export type RouteBoundaryKind = "shell" | "layout" | "route"
|
|
|
58
78
|
/** Metadata emitted on generated router nodes for route-aware navigation. */
|
|
59
79
|
export interface GeneratedRouteMetadata {
|
|
60
80
|
readonly id: string
|
|
61
|
-
/** Stable token used by the
|
|
81
|
+
/** Stable token used by the route-fragment boundary pass. */
|
|
62
82
|
readonly boundary: string
|
|
63
83
|
readonly kind: RouteBoundaryKind
|
|
64
84
|
readonly entry: string
|
|
@@ -69,15 +89,22 @@ export interface GeneratedRouteMetadata {
|
|
|
69
89
|
readonly hydration?: HydrationMode
|
|
70
90
|
}
|
|
71
91
|
|
|
92
|
+
export type RouteContent = "component" | "markdown"
|
|
93
|
+
|
|
72
94
|
export interface RouteOptions {
|
|
95
|
+
/** The route entry's authored content shape. Omit for an Octane component. */
|
|
96
|
+
readonly content?: RouteContent
|
|
73
97
|
readonly render?: RenderMode
|
|
74
98
|
readonly hydration?: HydrationMode
|
|
75
99
|
}
|
|
76
100
|
|
|
77
|
-
export interface RouteDefinition
|
|
78
|
-
|
|
101
|
+
export interface RouteDefinition<
|
|
102
|
+
Path extends string = string,
|
|
103
|
+
Entry extends string = string,
|
|
104
|
+
> extends RouteOptions {
|
|
105
|
+
readonly path: Path
|
|
79
106
|
/** Octane/Vite project-root module ID, such as `/src/Home.tsrx`. */
|
|
80
|
-
readonly entry:
|
|
107
|
+
readonly entry: Entry
|
|
81
108
|
readonly render: RenderMode
|
|
82
109
|
}
|
|
83
110
|
|
|
@@ -92,35 +119,358 @@ export interface LayoutDefinition<
|
|
|
92
119
|
|
|
93
120
|
export type RouteConfig = RouteDefinition | LayoutDefinition
|
|
94
121
|
|
|
122
|
+
/** Flatten nested route configuration into the leaf route union. */
|
|
123
|
+
export type RouteLeaf<Config> =
|
|
124
|
+
Config extends LayoutDefinition<infer Children>
|
|
125
|
+
? number extends Children["length"]
|
|
126
|
+
? RouteDefinition
|
|
127
|
+
: RouteLeaf<Children[number]>
|
|
128
|
+
: Config extends RouteDefinition
|
|
129
|
+
? Config
|
|
130
|
+
: never
|
|
131
|
+
|
|
132
|
+
export type RouteLeaves<Configs extends readonly RouteConfig[]> =
|
|
133
|
+
number extends Configs["length"]
|
|
134
|
+
? RouteDefinition
|
|
135
|
+
: RouteLeaf<Configs[number]>
|
|
136
|
+
|
|
95
137
|
export interface MatchRouteOptions {
|
|
96
138
|
readonly render?: RenderMode
|
|
97
139
|
}
|
|
98
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Route modules generated by Flamefront's optional declaration generator.
|
|
143
|
+
* Applications can use the authored route manifest without generated output;
|
|
144
|
+
* the empty base interface keeps that mode conservative.
|
|
145
|
+
*/
|
|
146
|
+
export interface RouteImportMap {}
|
|
147
|
+
|
|
148
|
+
/** Parameters used when a route pattern is not available to the type system. */
|
|
149
|
+
export type BroadRouteParams = Record<string, string | undefined>
|
|
150
|
+
|
|
151
|
+
type RouteImportMapPath = Extract<keyof RouteImportMap, string>
|
|
152
|
+
|
|
153
|
+
/** Route patterns emitted by Flamefront, or `string` before type generation. */
|
|
154
|
+
export type RoutePath = [RouteImportMapPath] extends [never]
|
|
155
|
+
? string
|
|
156
|
+
: RouteImportMapPath
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parameters captured by a generated route pattern. A stale or missing map
|
|
160
|
+
* deliberately keeps the existing broad record behavior.
|
|
161
|
+
*/
|
|
162
|
+
type RouteParamsForKnownPath<Path extends string> = Path extends string
|
|
163
|
+
? Path extends RouteImportMapPath
|
|
164
|
+
? MatchParams<Path> extends infer Params
|
|
165
|
+
? [Params] extends [never]
|
|
166
|
+
? BroadRouteParams
|
|
167
|
+
: Params
|
|
168
|
+
: BroadRouteParams
|
|
169
|
+
: BroadRouteParams
|
|
170
|
+
: BroadRouteParams
|
|
171
|
+
|
|
172
|
+
export type RouteParams<Path extends string = string> = [
|
|
173
|
+
RouteImportMapPath,
|
|
174
|
+
] extends [never]
|
|
175
|
+
? BroadRouteParams
|
|
176
|
+
: string extends Path
|
|
177
|
+
? BroadRouteParams
|
|
178
|
+
: RouteParamsForKnownPath<Path>
|
|
179
|
+
|
|
180
|
+
/** The route module associated with a generated route pattern. */
|
|
181
|
+
export type RouteModuleFor<Path extends string = string> = [
|
|
182
|
+
RouteImportMapPath,
|
|
183
|
+
] extends [never]
|
|
184
|
+
? unknown
|
|
185
|
+
: Path extends RouteImportMapPath
|
|
186
|
+
? RouteImportMap[Path]
|
|
187
|
+
: unknown
|
|
188
|
+
|
|
189
|
+
type RouteLoaderFunction<Module> = Module extends {
|
|
190
|
+
readonly loader?: infer Loader
|
|
191
|
+
}
|
|
192
|
+
? Loader extends (...args: infer _Args) => infer Result
|
|
193
|
+
? (...args: _Args) => Result
|
|
194
|
+
: never
|
|
195
|
+
: never
|
|
196
|
+
|
|
197
|
+
/** The authored loader function associated with a generated route pattern. */
|
|
198
|
+
export type RouteLoaderFor<Path extends string = string> = RouteLoaderFunction<
|
|
199
|
+
RouteModuleFor<Path>
|
|
200
|
+
>
|
|
201
|
+
|
|
202
|
+
/** The awaited result of a generated route's loader, or `unknown` as fallback. */
|
|
203
|
+
export type RouteLoaderData<Path extends string = string> =
|
|
204
|
+
RouteLoaderFor<Path> extends (...args: infer _Args) => infer Result
|
|
205
|
+
? Awaited<Result>
|
|
206
|
+
: unknown
|
|
207
|
+
|
|
208
|
+
type RoutePageParams<Path extends RouteImportMapPath> =
|
|
209
|
+
keyof RouteParams<Path> extends never
|
|
210
|
+
? BroadRouteParams
|
|
211
|
+
: RouteParams<Path> & Record<string, string | undefined>
|
|
212
|
+
|
|
213
|
+
/** Registration shape for router libraries that expose typed route pages. */
|
|
214
|
+
export type RoutePages = [RouteImportMapPath] extends [never]
|
|
215
|
+
? Record<string, { readonly params: BroadRouteParams }>
|
|
216
|
+
: {
|
|
217
|
+
[Path in RouteImportMapPath]: {
|
|
218
|
+
readonly params: RoutePageParams<Path>
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
declare module "@octanejs/remix-router" {
|
|
223
|
+
interface Register {
|
|
224
|
+
pages: RoutePages
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
99
228
|
export interface LoadRouteOptions {
|
|
100
229
|
readonly signal?: AbortSignal
|
|
101
230
|
readonly reload?: boolean
|
|
102
231
|
}
|
|
103
232
|
|
|
233
|
+
type TrimTrailingSlashes<Value extends string> = Value extends "/"
|
|
234
|
+
? Value
|
|
235
|
+
: Value extends `${infer Path}/`
|
|
236
|
+
? TrimTrailingSlashes<Path>
|
|
237
|
+
: Value
|
|
238
|
+
|
|
239
|
+
type RoutePathname<Value extends string> = Value extends `${string}?${string}`
|
|
240
|
+
? Value extends `${infer Path}?${string}`
|
|
241
|
+
? RoutePathname<Path>
|
|
242
|
+
: string
|
|
243
|
+
: Value extends `${string}#${string}`
|
|
244
|
+
? Value extends `${infer Path}#${string}`
|
|
245
|
+
? RoutePathname<Path>
|
|
246
|
+
: string
|
|
247
|
+
: Value extends `/${string}`
|
|
248
|
+
? TrimTrailingSlashes<Value>
|
|
249
|
+
: string
|
|
250
|
+
|
|
251
|
+
type RouteSegments<Value extends string> = Value extends `/${infer Rest}`
|
|
252
|
+
? Rest extends ""
|
|
253
|
+
? []
|
|
254
|
+
: Rest extends `${infer Head}/${infer Tail}`
|
|
255
|
+
? [Head, ...RouteSegments<`/${Tail}`>]
|
|
256
|
+
: [Rest]
|
|
257
|
+
: []
|
|
258
|
+
|
|
259
|
+
type RouteSegmentMatches<
|
|
260
|
+
Pattern extends string,
|
|
261
|
+
Value extends string,
|
|
262
|
+
> = Pattern extends `:${string}`
|
|
263
|
+
? true
|
|
264
|
+
: Pattern extends `*${string}`
|
|
265
|
+
? true
|
|
266
|
+
: Pattern extends `(${infer Optional})`
|
|
267
|
+
? RouteSegmentMatches<Optional, Value>
|
|
268
|
+
: Pattern extends Value
|
|
269
|
+
? true
|
|
270
|
+
: false
|
|
271
|
+
|
|
272
|
+
type RoutePatternMatches<
|
|
273
|
+
Pattern extends readonly string[],
|
|
274
|
+
Value extends readonly string[],
|
|
275
|
+
> = Pattern extends []
|
|
276
|
+
? Value extends []
|
|
277
|
+
? true
|
|
278
|
+
: false
|
|
279
|
+
: Pattern extends [infer Head extends string, ...infer Tail extends string[]]
|
|
280
|
+
? Head extends `*${string}`
|
|
281
|
+
? true
|
|
282
|
+
: Head extends `(${infer Optional})`
|
|
283
|
+
? RoutePatternMatches<[Optional, ...Tail], [...Value]> extends true
|
|
284
|
+
? true
|
|
285
|
+
: RoutePatternMatches<Tail, Value>
|
|
286
|
+
: Value extends [
|
|
287
|
+
infer ValueHead extends string,
|
|
288
|
+
...infer ValueTail extends string[],
|
|
289
|
+
]
|
|
290
|
+
? RouteSegmentMatches<Head, ValueHead> extends true
|
|
291
|
+
? RoutePatternMatches<Tail, ValueTail>
|
|
292
|
+
: false
|
|
293
|
+
: false
|
|
294
|
+
: false
|
|
295
|
+
|
|
296
|
+
type RouteMatchesUrl<
|
|
297
|
+
Pattern extends string,
|
|
298
|
+
Url extends string,
|
|
299
|
+
> = string extends Url
|
|
300
|
+
? true
|
|
301
|
+
: RoutePatternMatches<
|
|
302
|
+
RouteSegments<RoutePathname<Pattern>>,
|
|
303
|
+
RouteSegments<RoutePathname<Url>>
|
|
304
|
+
>
|
|
305
|
+
|
|
306
|
+
type MatchingRoutes<
|
|
307
|
+
Routes extends RouteDefinition,
|
|
308
|
+
Url extends string,
|
|
309
|
+
> = Routes extends RouteDefinition
|
|
310
|
+
? true extends RouteMatchesUrl<Routes["path"], Url>
|
|
311
|
+
? Routes
|
|
312
|
+
: never
|
|
313
|
+
: never
|
|
314
|
+
|
|
315
|
+
type ExactMatchingRoutes<
|
|
316
|
+
Routes extends RouteDefinition,
|
|
317
|
+
Url extends string,
|
|
318
|
+
> = Routes extends RouteDefinition
|
|
319
|
+
? Routes["path"] extends RoutePathname<Url>
|
|
320
|
+
? Routes
|
|
321
|
+
: never
|
|
322
|
+
: never
|
|
323
|
+
|
|
324
|
+
type MatchedRoutes<Routes extends RouteDefinition, Url extends string> = [
|
|
325
|
+
ExactMatchingRoutes<Routes, Url>,
|
|
326
|
+
] extends [never]
|
|
327
|
+
? MatchingRoutes<Routes, Url>
|
|
328
|
+
: ExactMatchingRoutes<Routes, Url>
|
|
329
|
+
|
|
330
|
+
type MatchingImportMapPaths<
|
|
331
|
+
Url extends string,
|
|
332
|
+
Paths extends string = RouteImportMapPath,
|
|
333
|
+
> = Paths extends string
|
|
334
|
+
? true extends RouteMatchesUrl<Paths, Url>
|
|
335
|
+
? Paths
|
|
336
|
+
: never
|
|
337
|
+
: never
|
|
338
|
+
|
|
339
|
+
type ExactImportMapPaths<
|
|
340
|
+
Url extends string,
|
|
341
|
+
Paths extends string = RouteImportMapPath,
|
|
342
|
+
> = Paths extends string
|
|
343
|
+
? Paths extends RoutePathname<Url>
|
|
344
|
+
? Paths
|
|
345
|
+
: never
|
|
346
|
+
: never
|
|
347
|
+
|
|
348
|
+
type ImportMapPathsForUrl<Url extends string> = [
|
|
349
|
+
ExactImportMapPaths<Url>,
|
|
350
|
+
] extends [never]
|
|
351
|
+
? MatchingImportMapPaths<Url>
|
|
352
|
+
: ExactImportMapPaths<Url>
|
|
353
|
+
|
|
354
|
+
type RoutesForUrl<Routes extends RouteDefinition, Url extends string> =
|
|
355
|
+
string extends RoutePathname<Url>
|
|
356
|
+
? Routes
|
|
357
|
+
: [MatchedRoutes<Routes, Url>] extends [never]
|
|
358
|
+
? Routes
|
|
359
|
+
: MatchedRoutes<Routes, Url>
|
|
360
|
+
|
|
361
|
+
/** A matched route whose params retain the authored route pattern. */
|
|
362
|
+
export type RouteMatch<Route extends RouteDefinition = RouteDefinition> = Omit<
|
|
363
|
+
Match<string, Route>,
|
|
364
|
+
"params"
|
|
365
|
+
> & {
|
|
366
|
+
readonly params: RouteParams<Route["path"]>
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
type BroadRouteMatch<Route extends RouteDefinition> = Omit<
|
|
370
|
+
RouteMatch<Route>,
|
|
371
|
+
"params"
|
|
372
|
+
> & {
|
|
373
|
+
readonly params: BroadRouteParams
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Match result selected from a route union and a statically known URL. */
|
|
377
|
+
export type RouteMatchForUrl<
|
|
378
|
+
Routes extends RouteDefinition,
|
|
379
|
+
Url extends string,
|
|
380
|
+
> = string extends Url
|
|
381
|
+
? BroadRouteMatch<Routes>
|
|
382
|
+
: string extends RoutePathname<Url>
|
|
383
|
+
? BroadRouteMatch<Routes>
|
|
384
|
+
: RouteMatch<RoutesForUrl<Routes, Url>>
|
|
385
|
+
|
|
386
|
+
/** Loader data selected from a route union and a statically known URL. */
|
|
387
|
+
export type RouteDataForUrl<
|
|
388
|
+
Routes extends RouteDefinition,
|
|
389
|
+
Url extends string,
|
|
390
|
+
> = string extends Url
|
|
391
|
+
? unknown
|
|
392
|
+
: string extends RoutePathname<Url>
|
|
393
|
+
? unknown
|
|
394
|
+
: [MatchedRoutes<Routes, Url>] extends [never]
|
|
395
|
+
? unknown
|
|
396
|
+
: MatchedRoutes<Routes, Url> extends infer Route
|
|
397
|
+
? Route extends RouteDefinition
|
|
398
|
+
? RouteLoaderData<Route["path"]>
|
|
399
|
+
: unknown
|
|
400
|
+
: unknown
|
|
401
|
+
|
|
402
|
+
/** Loader data selected directly from the generated map and a URL pathname. */
|
|
403
|
+
export type RouteDataForPath<Url extends string> = string extends Url
|
|
404
|
+
? unknown
|
|
405
|
+
: string extends RoutePathname<Url>
|
|
406
|
+
? unknown
|
|
407
|
+
: [RouteImportMapPath] extends [never]
|
|
408
|
+
? unknown
|
|
409
|
+
: [ImportMapPathsForUrl<Url>] extends [never]
|
|
410
|
+
? unknown
|
|
411
|
+
: ImportMapPathsForUrl<Url> extends infer Path
|
|
412
|
+
? Path extends string
|
|
413
|
+
? RouteLoaderData<Path>
|
|
414
|
+
: unknown
|
|
415
|
+
: unknown
|
|
416
|
+
|
|
417
|
+
/** A router destination constrained to generated route patterns when present. */
|
|
418
|
+
export type RouteDestination<Path extends string = RoutePath> =
|
|
419
|
+
| Path
|
|
420
|
+
| {
|
|
421
|
+
readonly pathname: Path
|
|
422
|
+
readonly search?: string
|
|
423
|
+
readonly hash?: string
|
|
424
|
+
readonly state?: unknown
|
|
425
|
+
}
|
|
426
|
+
|
|
104
427
|
export interface AppDefinition<T extends RouteDefinition = RouteDefinition> {
|
|
105
428
|
/** Octane/Vite project-root module ID for the persistent app shell. */
|
|
106
429
|
readonly shell: string
|
|
430
|
+
/** Hydration policy for the persistent shell region. */
|
|
431
|
+
readonly shellHydration: HydrationMode
|
|
107
432
|
readonly routes: readonly T[]
|
|
108
433
|
readonly routeTree: readonly RouteConfig[]
|
|
434
|
+
readonly hydrationDefaults: NormalizedHydrationDefaults
|
|
109
435
|
readonly routing: NormalizedRoutingOptions
|
|
110
|
-
readonly match:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
436
|
+
readonly match: {
|
|
437
|
+
<const Url extends string>(
|
|
438
|
+
url: Url,
|
|
439
|
+
options?: MatchRouteOptions,
|
|
440
|
+
): RouteMatchForUrl<T, Url> | null
|
|
441
|
+
(url: URL, options?: MatchRouteOptions): RouteMatchForUrl<T, string> | null
|
|
442
|
+
(
|
|
443
|
+
url: string,
|
|
444
|
+
options?: MatchRouteOptions,
|
|
445
|
+
): RouteMatchForUrl<T, string> | null
|
|
446
|
+
(
|
|
447
|
+
url: string | URL,
|
|
448
|
+
options?: MatchRouteOptions,
|
|
449
|
+
): RouteMatchForUrl<T, string> | null
|
|
450
|
+
}
|
|
114
451
|
/** Load route data using the route's live or static data source. */
|
|
115
|
-
readonly load:
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
452
|
+
readonly load: {
|
|
453
|
+
<const Url extends string>(
|
|
454
|
+
url: Url,
|
|
455
|
+
options?: LoadRouteOptions,
|
|
456
|
+
): Promise<RouteDataForUrl<T, Url>>
|
|
457
|
+
<Data = unknown>(url: URL, options?: LoadRouteOptions): Promise<Data>
|
|
458
|
+
<Data = unknown>(url: string, options?: LoadRouteOptions): Promise<Data>
|
|
459
|
+
<Data = unknown>(
|
|
460
|
+
url: string | URL,
|
|
461
|
+
options?: LoadRouteOptions,
|
|
462
|
+
): Promise<Data>
|
|
463
|
+
}
|
|
119
464
|
/** Warm the same cache used by generated client route loaders. */
|
|
120
|
-
readonly prefetch:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
465
|
+
readonly prefetch: {
|
|
466
|
+
<const Url extends string>(
|
|
467
|
+
url: Url,
|
|
468
|
+
options?: LoadRouteOptions,
|
|
469
|
+
): Promise<void>
|
|
470
|
+
(url: URL, options?: LoadRouteOptions): Promise<void>
|
|
471
|
+
(url: string, options?: LoadRouteOptions): Promise<void>
|
|
472
|
+
(url: string | URL, options?: LoadRouteOptions): Promise<void>
|
|
473
|
+
}
|
|
124
474
|
}
|
|
125
475
|
|
|
126
476
|
const defaultRoutingOptions: NormalizedRoutingOptions = Object.freeze({
|
|
@@ -128,6 +478,13 @@ const defaultRoutingOptions: NormalizedRoutingOptions = Object.freeze({
|
|
|
128
478
|
dataPath: "/__flamefront/data",
|
|
129
479
|
})
|
|
130
480
|
|
|
481
|
+
const defaultHydrationDefaults: NormalizedHydrationDefaults = Object.freeze({
|
|
482
|
+
server: "full",
|
|
483
|
+
static: "full",
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
const defaultShellHydration: HydrationMode = "full"
|
|
487
|
+
|
|
131
488
|
const renderModes: ReadonlySet<unknown> = new Set<RenderMode>([
|
|
132
489
|
"client",
|
|
133
490
|
"server",
|
|
@@ -193,6 +550,56 @@ function normalizeRoutingPath(
|
|
|
193
550
|
return path.replace(/\/+$/, "") || "/"
|
|
194
551
|
}
|
|
195
552
|
|
|
553
|
+
function validateHydrationDefault(
|
|
554
|
+
hydration: unknown,
|
|
555
|
+
render: "server" | "static",
|
|
556
|
+
): void {
|
|
557
|
+
validateHydrationMode(hydration, render, `hydrationDefaults.${render}`)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function normalizeHydrationDefaults(
|
|
561
|
+
options: HydrationDefaults | undefined = undefined,
|
|
562
|
+
): NormalizedHydrationDefaults {
|
|
563
|
+
if (
|
|
564
|
+
options !== undefined &&
|
|
565
|
+
(!options || typeof options !== "object" || Array.isArray(options))
|
|
566
|
+
) {
|
|
567
|
+
throw new TypeError("flamefront hydrationDefaults must be an object.")
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (options) {
|
|
571
|
+
const unexpected = Object.keys(options).find(
|
|
572
|
+
(key) => key !== "server" && key !== "static",
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
if (unexpected) {
|
|
576
|
+
throw new TypeError(
|
|
577
|
+
`flamefront hydrationDefaults has an unexpected ${JSON.stringify(unexpected)} option.`,
|
|
578
|
+
)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const server = options?.server ?? defaultHydrationDefaults.server
|
|
583
|
+
const staticMode = options?.static ?? defaultHydrationDefaults.static
|
|
584
|
+
|
|
585
|
+
validateHydrationDefault(server, "server")
|
|
586
|
+
validateHydrationDefault(staticMode, "static")
|
|
587
|
+
|
|
588
|
+
return Object.freeze({
|
|
589
|
+
server: freezeHydration(server) as HydrationMode,
|
|
590
|
+
static: freezeHydration(staticMode) as HydrationMode,
|
|
591
|
+
})
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function normalizeShellHydration(
|
|
595
|
+
hydration: HydrationMode | undefined,
|
|
596
|
+
): HydrationMode {
|
|
597
|
+
const normalized = hydration ?? defaultShellHydration
|
|
598
|
+
|
|
599
|
+
validateHydrationMode(normalized, "server", "shellHydration")
|
|
600
|
+
return freezeHydration(normalized) as HydrationMode
|
|
601
|
+
}
|
|
602
|
+
|
|
196
603
|
export function normalizeRoutingOptions(
|
|
197
604
|
options: RoutingOptions | undefined = undefined,
|
|
198
605
|
): NormalizedRoutingOptions {
|
|
@@ -247,6 +654,17 @@ export function joinBasename(basename: string, pathname: string): string {
|
|
|
247
654
|
return `${basename}${pathname.startsWith("/") ? pathname : `/${pathname}`}`
|
|
248
655
|
}
|
|
249
656
|
|
|
657
|
+
/** Build a concrete URL from one generated route pattern. */
|
|
658
|
+
export function routeHref<const Path extends RoutePath>(
|
|
659
|
+
path: Path,
|
|
660
|
+
...args: CreateHrefArgs<Path>
|
|
661
|
+
): string {
|
|
662
|
+
return createPatternHref(path, ...args)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Alias for callers that prefer the `create*` naming convention. */
|
|
666
|
+
export const createRouteHref = routeHref
|
|
667
|
+
|
|
250
668
|
function assertString(value: unknown, name: string): asserts value is string {
|
|
251
669
|
if (typeof value !== "string" || value.length === 0) {
|
|
252
670
|
throw new TypeError(`flamefront ${name} must be a non-empty string.`)
|
|
@@ -355,12 +773,11 @@ function validateGeneratedHydration(
|
|
|
355
773
|
}
|
|
356
774
|
}
|
|
357
775
|
|
|
358
|
-
function
|
|
359
|
-
|
|
776
|
+
function validateHydrationMode(
|
|
777
|
+
hydration: unknown,
|
|
778
|
+
render: RenderMode,
|
|
360
779
|
location: string,
|
|
361
780
|
): void {
|
|
362
|
-
const { hydration, render } = routeDefinition
|
|
363
|
-
|
|
364
781
|
if (hydration === undefined) {
|
|
365
782
|
return
|
|
366
783
|
}
|
|
@@ -396,6 +813,17 @@ function validateHydration(
|
|
|
396
813
|
}
|
|
397
814
|
}
|
|
398
815
|
|
|
816
|
+
function validateHydration(
|
|
817
|
+
routeDefinition: RouteDefinition,
|
|
818
|
+
location: string,
|
|
819
|
+
): void {
|
|
820
|
+
validateHydrationMode(
|
|
821
|
+
routeDefinition.hydration,
|
|
822
|
+
routeDefinition.render,
|
|
823
|
+
location,
|
|
824
|
+
)
|
|
825
|
+
}
|
|
826
|
+
|
|
399
827
|
function freezeHydration(
|
|
400
828
|
hydration: HydrationMode | undefined,
|
|
401
829
|
): HydrationMode | undefined {
|
|
@@ -437,6 +865,16 @@ function validateRoute(
|
|
|
437
865
|
|
|
438
866
|
assertString(routeDefinition.entry, `route ${location} entry`)
|
|
439
867
|
|
|
868
|
+
if (
|
|
869
|
+
routeDefinition.content !== undefined &&
|
|
870
|
+
routeDefinition.content !== "component" &&
|
|
871
|
+
routeDefinition.content !== "markdown"
|
|
872
|
+
) {
|
|
873
|
+
throw new TypeError(
|
|
874
|
+
`flamefront route ${location} content must be 'component' or 'markdown'.`,
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
440
878
|
if (!renderModes.has(routeDefinition.render)) {
|
|
441
879
|
throw new TypeError(
|
|
442
880
|
`flamefront route ${location} render must be 'client', 'server', or 'static'.`,
|
|
@@ -447,12 +885,12 @@ function validateRoute(
|
|
|
447
885
|
}
|
|
448
886
|
|
|
449
887
|
/** Define one explicit route without relying on a filesystem convention. */
|
|
450
|
-
export function route(
|
|
451
|
-
path:
|
|
452
|
-
entry:
|
|
888
|
+
export function route<const Path extends string, const Entry extends string>(
|
|
889
|
+
path: Path,
|
|
890
|
+
entry: Entry,
|
|
453
891
|
options: RouteOptions = {},
|
|
454
|
-
): RouteDefinition {
|
|
455
|
-
const definition: RouteDefinition = {
|
|
892
|
+
): RouteDefinition<Path, Entry> {
|
|
893
|
+
const definition: RouteDefinition<Path, Entry> = {
|
|
456
894
|
path,
|
|
457
895
|
entry,
|
|
458
896
|
...options,
|
|
@@ -464,6 +902,58 @@ export function route(
|
|
|
464
902
|
return Object.freeze(definition)
|
|
465
903
|
}
|
|
466
904
|
|
|
905
|
+
/** Shorthand for a route with `render: "server"`. */
|
|
906
|
+
export function serverRoute<
|
|
907
|
+
const Path extends string,
|
|
908
|
+
const Entry extends string,
|
|
909
|
+
>(
|
|
910
|
+
path: Path,
|
|
911
|
+
entry: Entry,
|
|
912
|
+
options: Omit<RouteOptions, "render"> = {},
|
|
913
|
+
): RouteDefinition<Path, Entry> {
|
|
914
|
+
return route(path, entry, { ...options, render: "server" })
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** Shorthand for a route with `render: "static"`. */
|
|
918
|
+
export function staticRoute<
|
|
919
|
+
const Path extends string,
|
|
920
|
+
const Entry extends string,
|
|
921
|
+
>(
|
|
922
|
+
path: Path,
|
|
923
|
+
entry: Entry,
|
|
924
|
+
options: Omit<RouteOptions, "render"> = {},
|
|
925
|
+
): RouteDefinition<Path, Entry> {
|
|
926
|
+
return route(path, entry, { ...options, render: "static" })
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** Shorthand for a static route backed by a Sätteri `.md` entry. */
|
|
930
|
+
export function markdownRoute<
|
|
931
|
+
const Path extends string,
|
|
932
|
+
const Entry extends string,
|
|
933
|
+
>(
|
|
934
|
+
path: Path,
|
|
935
|
+
entry: Entry,
|
|
936
|
+
options: Omit<RouteOptions, "content"> = {},
|
|
937
|
+
): RouteDefinition<Path, Entry> & { readonly content: "markdown" } {
|
|
938
|
+
return route(path, entry, {
|
|
939
|
+
...options,
|
|
940
|
+
content: "markdown",
|
|
941
|
+
render: options.render ?? "static",
|
|
942
|
+
}) as RouteDefinition<Path, Entry> & { readonly content: "markdown" }
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Shorthand for a route with `render: "client"`. */
|
|
946
|
+
export function clientRoute<
|
|
947
|
+
const Path extends string,
|
|
948
|
+
const Entry extends string,
|
|
949
|
+
>(
|
|
950
|
+
path: Path,
|
|
951
|
+
entry: Entry,
|
|
952
|
+
options: Omit<RouteOptions, "render"> = {},
|
|
953
|
+
): RouteDefinition<Path, Entry> {
|
|
954
|
+
return route(path, entry, { ...options, render: "client" })
|
|
955
|
+
}
|
|
956
|
+
|
|
467
957
|
/** Group routes beneath a shared pathless layout without adding a URL segment. */
|
|
468
958
|
export function layout<const Children extends readonly RouteConfig[]>(
|
|
469
959
|
entry: string,
|
|
@@ -488,6 +978,7 @@ function isLayoutDefinition(config: RouteConfig): config is LayoutDefinition {
|
|
|
488
978
|
function normalizeRouteTree(
|
|
489
979
|
configs: readonly RouteConfig[],
|
|
490
980
|
seenPaths: Set<string>,
|
|
981
|
+
hydrationDefaults: NormalizedHydrationDefaults,
|
|
491
982
|
location = "",
|
|
492
983
|
): { tree: readonly RouteConfig[]; routes: readonly RouteDefinition[] } {
|
|
493
984
|
const routes: RouteDefinition[] = []
|
|
@@ -513,6 +1004,7 @@ function normalizeRouteTree(
|
|
|
513
1004
|
const normalized = normalizeRouteTree(
|
|
514
1005
|
config.children,
|
|
515
1006
|
seenPaths,
|
|
1007
|
+
hydrationDefaults,
|
|
516
1008
|
configLocation,
|
|
517
1009
|
)
|
|
518
1010
|
|
|
@@ -524,19 +1016,24 @@ function normalizeRouteTree(
|
|
|
524
1016
|
})
|
|
525
1017
|
}
|
|
526
1018
|
|
|
527
|
-
|
|
1019
|
+
const hydration =
|
|
1020
|
+
config.hydration ??
|
|
1021
|
+
(config.render === "client" ? "full" : hydrationDefaults[config.render])
|
|
1022
|
+
const normalizedRoute = {
|
|
1023
|
+
...config,
|
|
1024
|
+
hydration: freezeHydration(hydration),
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
validateRoute(normalizedRoute, configLocation)
|
|
528
1028
|
if (seenPaths.has(config.path)) {
|
|
529
1029
|
throw new TypeError(`flamefront route path is duplicated: ${config.path}`)
|
|
530
1030
|
}
|
|
531
1031
|
|
|
532
1032
|
seenPaths.add(config.path)
|
|
533
|
-
const
|
|
534
|
-
...config,
|
|
535
|
-
hydration: freezeHydration(config.hydration),
|
|
536
|
-
})
|
|
1033
|
+
const frozenRoute = Object.freeze(normalizedRoute)
|
|
537
1034
|
|
|
538
|
-
routes.push(
|
|
539
|
-
return
|
|
1035
|
+
routes.push(frozenRoute)
|
|
1036
|
+
return frozenRoute
|
|
540
1037
|
})
|
|
541
1038
|
|
|
542
1039
|
return { tree: Object.freeze(tree), routes: Object.freeze(routes) }
|
|
@@ -562,7 +1059,7 @@ function matchRoutes<T extends RouteDefinition>(
|
|
|
562
1059
|
url: string | URL,
|
|
563
1060
|
options: MatchRouteOptions = {},
|
|
564
1061
|
basename = "/",
|
|
565
|
-
):
|
|
1062
|
+
): RouteMatch<T> | null {
|
|
566
1063
|
let matchers = matcherCache.get(routes)
|
|
567
1064
|
|
|
568
1065
|
if (!matchers) {
|
|
@@ -589,7 +1086,7 @@ function matchRoutes<T extends RouteDefinition>(
|
|
|
589
1086
|
normalizedUrl.pathname = normalizedUrl.pathname.replace(/\/+$/, "")
|
|
590
1087
|
}
|
|
591
1088
|
|
|
592
|
-
return matcher.match(normalizedUrl)
|
|
1089
|
+
return matcher.match(normalizedUrl) as RouteMatch<T> | null
|
|
593
1090
|
}
|
|
594
1091
|
|
|
595
1092
|
/** Normalize and validate the application's explicit route graph. */
|
|
@@ -597,9 +1094,14 @@ export function defineApp<
|
|
|
597
1094
|
const T extends {
|
|
598
1095
|
readonly shell: string
|
|
599
1096
|
readonly routes: readonly RouteConfig[]
|
|
1097
|
+
readonly shellHydration?: HydrationMode
|
|
1098
|
+
readonly hydrationDefaults?: HydrationDefaults
|
|
600
1099
|
readonly routing?: RoutingOptions
|
|
601
1100
|
},
|
|
602
|
-
>(
|
|
1101
|
+
>(
|
|
1102
|
+
options: T,
|
|
1103
|
+
): Omit<T, "routes" | "routing" | "hydrationDefaults" | "shellHydration"> &
|
|
1104
|
+
AppDefinition<RouteLeaves<T["routes"]>> {
|
|
603
1105
|
if (
|
|
604
1106
|
!options ||
|
|
605
1107
|
typeof options !== "object" ||
|
|
@@ -610,32 +1112,50 @@ export function defineApp<
|
|
|
610
1112
|
|
|
611
1113
|
assertString(options.shell, "app shell entry")
|
|
612
1114
|
|
|
613
|
-
const
|
|
614
|
-
|
|
1115
|
+
const hydrationDefaults = normalizeHydrationDefaults(
|
|
1116
|
+
options.hydrationDefaults,
|
|
1117
|
+
)
|
|
1118
|
+
const shellHydration = normalizeShellHydration(options.shellHydration)
|
|
1119
|
+
const normalized = normalizeRouteTree(
|
|
1120
|
+
options.routes,
|
|
1121
|
+
new Set(),
|
|
1122
|
+
hydrationDefaults,
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
type AppRoute = RouteLeaves<T["routes"]>
|
|
1126
|
+
const frozenRoutes = normalized.routes as readonly AppRoute[]
|
|
615
1127
|
const routing = normalizeRoutingOptions(options.routing)
|
|
616
1128
|
const routeDataClient = createRouteDataClient(routing)
|
|
617
|
-
const load =
|
|
618
|
-
url: string | URL,
|
|
619
|
-
loadOptions: LoadRouteOptions = {},
|
|
620
|
-
) => {
|
|
1129
|
+
const load = ((url: string | URL, loadOptions: LoadRouteOptions = {}) => {
|
|
621
1130
|
const match = matchRoutes(frozenRoutes, url, {}, routing.basename)
|
|
622
1131
|
const source = match?.data.render === "static" ? "static" : "live"
|
|
623
1132
|
|
|
624
|
-
return routeDataClient.load
|
|
625
|
-
}
|
|
1133
|
+
return routeDataClient.load(url, source, loadOptions)
|
|
1134
|
+
}) as AppDefinition<AppRoute>["load"]
|
|
626
1135
|
|
|
627
1136
|
const app = Object.freeze({
|
|
628
1137
|
...options,
|
|
1138
|
+
shellHydration,
|
|
1139
|
+
hydrationDefaults,
|
|
629
1140
|
routes: frozenRoutes,
|
|
630
1141
|
routeTree: normalized.tree,
|
|
631
1142
|
routing,
|
|
632
|
-
match: (url: string | URL, matchOptions?: MatchRouteOptions) =>
|
|
633
|
-
matchRoutes(
|
|
1143
|
+
match: ((url: string | URL, matchOptions?: MatchRouteOptions) =>
|
|
1144
|
+
matchRoutes(
|
|
1145
|
+
frozenRoutes,
|
|
1146
|
+
url,
|
|
1147
|
+
matchOptions,
|
|
1148
|
+
routing.basename,
|
|
1149
|
+
)) as AppDefinition<AppRoute>["match"],
|
|
634
1150
|
load,
|
|
635
|
-
prefetch: async (url: string | URL, loadOptions?: LoadRouteOptions) => {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1151
|
+
prefetch: (async (url: string | URL, loadOptions?: LoadRouteOptions) => {
|
|
1152
|
+
const match = matchRoutes(frozenRoutes, url, {}, routing.basename)
|
|
1153
|
+
const source = match?.data.render === "static" ? "static" : "live"
|
|
1154
|
+
|
|
1155
|
+
await routeDataClient.load(url, source, loadOptions)
|
|
1156
|
+
}) as AppDefinition<AppRoute>["prefetch"],
|
|
1157
|
+
}) as Omit<T, "routes" | "routing" | "hydrationDefaults" | "shellHydration"> &
|
|
1158
|
+
AppDefinition<AppRoute>
|
|
639
1159
|
|
|
640
1160
|
matcherCache.set(
|
|
641
1161
|
frozenRoutes,
|