flamefront 0.1.1 → 0.1.3
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/README.md +32 -9
- package/package.json +4 -6
- package/src/action-client.ts +187 -0
- package/src/action-transform.ts +258 -0
- package/src/action.ts +497 -0
- package/src/cli.ts +8 -2
- package/src/fetch.ts +38 -1
- package/src/fragment-client.ts +13 -1
- package/src/fragment-protocol.ts +2 -0
- package/src/fragment.tsx +1 -0
- package/src/index.ts +38 -0
- package/src/lifecycle.ts +397 -83
- package/src/octane.tsx +33 -3
- package/src/prerender-artifacts.ts +185 -0
- package/src/prerender.ts +376 -0
- package/src/remix-route-data.ts +6 -0
- package/src/route-data-client.ts +26 -0
- package/src/server.ts +108 -2
- package/src/srvx.ts +5 -0
- package/src/static-fragment-artifacts.ts +14 -7
- package/src/virtual-remix-routes.d.ts +1 -0
- package/src/vite-options.ts +29 -0
- package/src/vite.ts +130 -12
package/src/index.ts
CHANGED
|
@@ -12,6 +12,18 @@ import type { HydrationInteractionEvents } from "octane/hydration"
|
|
|
12
12
|
import { createRouteDataClient } from "./route-data-client.ts"
|
|
13
13
|
import { stripFlamefrontProtocolParams } from "./fragment-protocol.ts"
|
|
14
14
|
|
|
15
|
+
export { action } from "./action.ts"
|
|
16
|
+
export type {
|
|
17
|
+
ActionDataWithResponseInit,
|
|
18
|
+
ActionFunction,
|
|
19
|
+
ActionInput,
|
|
20
|
+
ActionOutput,
|
|
21
|
+
ActionValidationError,
|
|
22
|
+
StandardSchema,
|
|
23
|
+
StandardSchemaIssue,
|
|
24
|
+
StandardSchemaV1,
|
|
25
|
+
} from "./action.ts"
|
|
26
|
+
|
|
15
27
|
export { glob, joinRoutePath } from "./glob.ts"
|
|
16
28
|
export type { GlobFile } from "./glob.ts"
|
|
17
29
|
|
|
@@ -75,6 +87,13 @@ export type RouteNavigationStrategy = "router" | "fragment"
|
|
|
75
87
|
|
|
76
88
|
export type RouteBoundaryKind = "shell" | "layout" | "route"
|
|
77
89
|
|
|
90
|
+
/** Handle exposed by generated router matches, including content metadata. */
|
|
91
|
+
export interface RouteHandle {
|
|
92
|
+
readonly flamefront: GeneratedRouteMetadata
|
|
93
|
+
/** Markdown/MDX frontmatter; absent on component routes and layouts. */
|
|
94
|
+
readonly frontmatter?: Readonly<Record<string, unknown>>
|
|
95
|
+
}
|
|
96
|
+
|
|
78
97
|
/** Metadata emitted on generated router nodes for route-aware navigation. */
|
|
79
98
|
export interface GeneratedRouteMetadata {
|
|
80
99
|
readonly id: string
|
|
@@ -205,6 +224,25 @@ export type RouteLoaderData<Path extends string = string> =
|
|
|
205
224
|
? Awaited<Result>
|
|
206
225
|
: unknown
|
|
207
226
|
|
|
227
|
+
type RouteActionFunction<Module> = Module extends {
|
|
228
|
+
readonly action?: infer Action
|
|
229
|
+
}
|
|
230
|
+
? Action extends (...args: infer _Args) => infer Result
|
|
231
|
+
? (...args: _Args) => Result
|
|
232
|
+
: never
|
|
233
|
+
: never
|
|
234
|
+
|
|
235
|
+
/** The authored page action function associated with a generated route. */
|
|
236
|
+
export type RouteActionFor<Path extends string = string> = RouteActionFunction<
|
|
237
|
+
RouteModuleFor<Path>
|
|
238
|
+
>
|
|
239
|
+
|
|
240
|
+
/** The awaited result of a generated route's page action. */
|
|
241
|
+
export type RouteActionData<Path extends string = string> =
|
|
242
|
+
RouteActionFor<Path> extends (...args: infer _Args) => infer Result
|
|
243
|
+
? Awaited<Result>
|
|
244
|
+
: unknown
|
|
245
|
+
|
|
208
246
|
type RoutePageParams<Path extends RouteImportMapPath> =
|
|
209
247
|
keyof RouteParams<Path> extends never
|
|
210
248
|
? BroadRouteParams
|
package/src/lifecycle.ts
CHANGED
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
type IncomingMessage,
|
|
4
4
|
type ServerResponse,
|
|
5
5
|
} from "node:http"
|
|
6
|
-
import { access,
|
|
7
|
-
import {
|
|
6
|
+
import { access, readFile, rm, writeFile } from "node:fs/promises"
|
|
7
|
+
import { relative, resolve } from "node:path"
|
|
8
8
|
import { pathToFileURL } from "node:url"
|
|
9
9
|
import { serve } from "srvx"
|
|
10
10
|
import type { ServerMiddleware } from "srvx"
|
|
@@ -16,17 +16,33 @@ import type {
|
|
|
16
16
|
import { joinBasename } from "./index.ts"
|
|
17
17
|
import type { FlamefrontServerEntry } from "./srvx.ts"
|
|
18
18
|
import type { RenderDocumentResult } from "./server.ts"
|
|
19
|
+
import type {
|
|
20
|
+
PrerenderCache,
|
|
21
|
+
PrerenderOptions,
|
|
22
|
+
PrerenderPage,
|
|
23
|
+
} from "./prerender.ts"
|
|
19
24
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
cacheKey as prerenderCacheKey,
|
|
26
|
+
cacheNamespace,
|
|
27
|
+
createFilesystemPrerenderCache,
|
|
28
|
+
deserializePrerenderArtifact,
|
|
29
|
+
hash,
|
|
30
|
+
isParameterizedRoute,
|
|
31
|
+
renderingFingerprint,
|
|
32
|
+
routeSourceFile,
|
|
33
|
+
serializePrerenderArtifact,
|
|
34
|
+
} from "./prerender.ts"
|
|
35
|
+
import { staticRouteFile } from "./static-fragment-artifacts.ts"
|
|
36
|
+
import type { RouteFragmentArtifact } from "./fragment-client.ts"
|
|
25
37
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
assembleStaticRouteArtifact,
|
|
39
|
+
documentParts,
|
|
40
|
+
renderStaticRoute,
|
|
41
|
+
staticRouteRequest,
|
|
42
|
+
writeStaticRouteArtifact,
|
|
43
|
+
} from "./prerender-artifacts.ts"
|
|
29
44
|
import { setGlobRoot } from "./glob.ts"
|
|
45
|
+
import { getFlamefrontOptions } from "./vite-options.ts"
|
|
30
46
|
|
|
31
47
|
export {
|
|
32
48
|
staticRouteFile,
|
|
@@ -35,6 +51,12 @@ export {
|
|
|
35
51
|
staticRouteFragmentDataFile,
|
|
36
52
|
} from "./static-fragment-artifacts.ts"
|
|
37
53
|
|
|
54
|
+
export type {
|
|
55
|
+
PrerenderCache,
|
|
56
|
+
PrerenderOptions,
|
|
57
|
+
PrerenderPage,
|
|
58
|
+
} from "./prerender.ts"
|
|
59
|
+
|
|
38
60
|
interface AppModule {
|
|
39
61
|
app?: AppDefinition
|
|
40
62
|
default?: AppDefinition
|
|
@@ -50,6 +72,38 @@ export interface ProjectContext {
|
|
|
50
72
|
readonly routesFile: string
|
|
51
73
|
}
|
|
52
74
|
|
|
75
|
+
export interface BuildProjectOptions {
|
|
76
|
+
readonly forcePrerender?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PrerenderRouteEntry {
|
|
80
|
+
readonly path: string
|
|
81
|
+
readonly route: RouteDefinition
|
|
82
|
+
readonly key: string | null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface CollectedPrerenderRoute {
|
|
86
|
+
readonly path: string
|
|
87
|
+
readonly route: RouteDefinition
|
|
88
|
+
readonly supplied?: PrerenderPage
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface PrerenderStats {
|
|
92
|
+
readonly rendered: number
|
|
93
|
+
readonly reused: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface PrerenderStaticRouteOptions {
|
|
97
|
+
readonly cache?: PrerenderCache | false
|
|
98
|
+
readonly force?: boolean
|
|
99
|
+
readonly revision?: string
|
|
100
|
+
readonly template?: string
|
|
101
|
+
readonly fingerprint?: (
|
|
102
|
+
route: RouteDefinition,
|
|
103
|
+
path: string,
|
|
104
|
+
) => string | Promise<string>
|
|
105
|
+
}
|
|
106
|
+
|
|
53
107
|
export async function loadProject(
|
|
54
108
|
root = process.cwd(),
|
|
55
109
|
): Promise<ProjectContext> {
|
|
@@ -165,24 +219,6 @@ function requestUrl(request: IncomingMessage): URL {
|
|
|
165
219
|
)
|
|
166
220
|
}
|
|
167
221
|
|
|
168
|
-
function documentParts(document: RenderDocumentResult): {
|
|
169
|
-
readonly html: string
|
|
170
|
-
readonly status: number
|
|
171
|
-
readonly hasRouteData: boolean
|
|
172
|
-
readonly routeData: unknown
|
|
173
|
-
} {
|
|
174
|
-
if (typeof document === "string") {
|
|
175
|
-
return { html: document, status: 200, hasRouteData: false, routeData: null }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
html: document.html,
|
|
180
|
-
status: document.status ?? 200,
|
|
181
|
-
hasRouteData: "routeData" in document,
|
|
182
|
-
routeData: document.routeData,
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
222
|
function concreteRoutePath(path: string): string {
|
|
187
223
|
return (
|
|
188
224
|
path
|
|
@@ -224,8 +260,268 @@ async function listen(
|
|
|
224
260
|
})
|
|
225
261
|
}
|
|
226
262
|
|
|
227
|
-
|
|
263
|
+
async function loadBuildFlamefrontOptions(root: string) {
|
|
264
|
+
const { loadConfigFromFile } = await import("vite")
|
|
265
|
+
|
|
266
|
+
await loadConfigFromFile(
|
|
267
|
+
{ command: "build", mode: "production" },
|
|
268
|
+
resolve(root, "vite.config.ts"),
|
|
269
|
+
root,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return getFlamefrontOptions(root)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function validatePrerenderPath(path: unknown): asserts path is string {
|
|
276
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
277
|
+
throw new TypeError(
|
|
278
|
+
`flamefront prerender page path must start with '/'; received ${JSON.stringify(path)}.`,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (path.includes("?") || path.includes("#")) {
|
|
283
|
+
throw new TypeError(
|
|
284
|
+
`flamefront prerender page path cannot contain a query or fragment: ${JSON.stringify(path)}.`,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (isParameterizedRoute(path)) {
|
|
289
|
+
throw new TypeError(
|
|
290
|
+
`flamefront prerender page path must be concrete: ${JSON.stringify(path)}.`,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizedPrerenderPath(path: string): string {
|
|
296
|
+
return path.length > 1 ? path.replace(/\/+$/, "") : path
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function collectPrerenderRoutes(
|
|
300
|
+
root: string,
|
|
301
|
+
app: AppDefinition,
|
|
302
|
+
options: PrerenderOptions,
|
|
303
|
+
): Promise<readonly CollectedPrerenderRoute[]> {
|
|
304
|
+
const pages = new Map<string, RouteDefinition>()
|
|
305
|
+
const suppliedPages = new Map<string, PrerenderPage>()
|
|
306
|
+
const callbackPaths = new Set<string>()
|
|
307
|
+
const staticRoutes = app.routes.filter((route) => route.render === "static")
|
|
308
|
+
|
|
309
|
+
for (const route of staticRoutes) {
|
|
310
|
+
if (!isParameterizedRoute(route.path)) {
|
|
311
|
+
pages.set(route.path, route)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (options.pages) {
|
|
316
|
+
const supplied = await options.pages({ root, routes: app.routes })
|
|
317
|
+
|
|
318
|
+
for await (const page of supplied) {
|
|
319
|
+
if (!page || typeof page !== "object") {
|
|
320
|
+
throw new TypeError("flamefront prerender pages must contain objects.")
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
validatePrerenderPath(page.path)
|
|
324
|
+
const path = normalizedPrerenderPath(page.path)
|
|
325
|
+
const normalizedPage = path === page.path ? page : { ...page, path }
|
|
326
|
+
|
|
327
|
+
if (callbackPaths.has(path)) {
|
|
328
|
+
throw new TypeError(
|
|
329
|
+
`flamefront prerender page path is duplicated: ${path}`,
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (
|
|
334
|
+
page.key !== undefined &&
|
|
335
|
+
page.key !== null &&
|
|
336
|
+
typeof page.key !== "string"
|
|
337
|
+
) {
|
|
338
|
+
throw new TypeError(
|
|
339
|
+
`flamefront prerender page key must be a string or null: ${path}`,
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const match = app.match(
|
|
344
|
+
new URL(joinRoutePath(app.routing, path), "http://flamefront.build"),
|
|
345
|
+
{ render: "static" },
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if (!match) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`flamefront prerender page ${JSON.stringify(path)} does not match a static route.`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
callbackPaths.add(path)
|
|
355
|
+
pages.set(path, match.data)
|
|
356
|
+
suppliedPages.set(path, normalizedPage)
|
|
357
|
+
}
|
|
358
|
+
} else if (staticRoutes.some((route) => isParameterizedRoute(route.path))) {
|
|
359
|
+
const route = staticRoutes.find((item) => isParameterizedRoute(item.path))!
|
|
360
|
+
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Cannot prerender parameterized static route ${JSON.stringify(route.path)} without concrete paths.`,
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return [...pages].map(([path, route]) => ({
|
|
367
|
+
path,
|
|
368
|
+
route,
|
|
369
|
+
supplied: suppliedPages.get(path),
|
|
370
|
+
}))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function resolvePrerenderKey(
|
|
374
|
+
root: string,
|
|
375
|
+
entry: Omit<PrerenderRouteEntry, "key">,
|
|
376
|
+
supplied: PrerenderPage | undefined,
|
|
377
|
+
): Promise<string | null> {
|
|
378
|
+
if (supplied?.key !== undefined) {
|
|
379
|
+
return supplied.key
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (entry.route.content !== "markdown") {
|
|
383
|
+
return null
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const source = routeSourceFile(root, entry.route)
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
return hash(await readFile(source))
|
|
390
|
+
} catch (error) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Cannot hash Markdown route source ${relative(root, source)}.`,
|
|
393
|
+
{ cause: error },
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function resolvePrerenderEntries(
|
|
399
|
+
root: string,
|
|
400
|
+
app: AppDefinition,
|
|
401
|
+
options: PrerenderOptions,
|
|
402
|
+
): Promise<readonly PrerenderRouteEntry[]> {
|
|
403
|
+
const routes = await collectPrerenderRoutes(root, app, options)
|
|
404
|
+
|
|
405
|
+
return Promise.all(
|
|
406
|
+
routes.map(async (entry) => ({
|
|
407
|
+
...entry,
|
|
408
|
+
key: await resolvePrerenderKey(root, entry, entry.supplied),
|
|
409
|
+
})),
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function prerenderStaticRouteEntries(
|
|
414
|
+
root: string,
|
|
415
|
+
clientDirectory: string,
|
|
416
|
+
entries: readonly PrerenderRouteEntry[],
|
|
417
|
+
render: (request: Request) => Promise<RenderDocumentResult>,
|
|
418
|
+
loadData?: (request: Request) => Promise<unknown>,
|
|
419
|
+
routing: Pick<NormalizedRoutingOptions, "basename"> = { basename: "/" },
|
|
420
|
+
renderFragment?: (request: Request) => Promise<RouteFragmentArtifact>,
|
|
421
|
+
options: PrerenderStaticRouteOptions = {},
|
|
422
|
+
): Promise<PrerenderStats> {
|
|
423
|
+
const cache =
|
|
424
|
+
options.cache === undefined
|
|
425
|
+
? createFilesystemPrerenderCache(root)
|
|
426
|
+
: options.cache
|
|
427
|
+
let rendered = 0
|
|
428
|
+
let reused = 0
|
|
429
|
+
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
const outputRoute =
|
|
432
|
+
entry.route.path === entry.path
|
|
433
|
+
? entry.route
|
|
434
|
+
: ({ ...entry.route, path: entry.path } satisfies RouteDefinition)
|
|
435
|
+
const request = staticRouteRequest(routing, entry.path)
|
|
436
|
+
const fingerprint = options.fingerprint
|
|
437
|
+
? await options.fingerprint(entry.route, entry.path)
|
|
438
|
+
: hash({ route: entry.route, path: entry.path })
|
|
439
|
+
const key =
|
|
440
|
+
entry.key === null
|
|
441
|
+
? undefined
|
|
442
|
+
: prerenderCacheKey(
|
|
443
|
+
entry.path,
|
|
444
|
+
entry.key,
|
|
445
|
+
options.revision,
|
|
446
|
+
fingerprint,
|
|
447
|
+
cacheNamespace(root),
|
|
448
|
+
)
|
|
449
|
+
let artifact = null as Awaited<ReturnType<typeof renderStaticRoute>> | null
|
|
450
|
+
let reusedEntry = false
|
|
451
|
+
|
|
452
|
+
if (cache && key && !options.force) {
|
|
453
|
+
try {
|
|
454
|
+
const cached = await cache.get(key)
|
|
455
|
+
const decoded = cached ? deserializePrerenderArtifact(cached) : null
|
|
456
|
+
|
|
457
|
+
if (decoded?.path === entry.path) {
|
|
458
|
+
artifact = options.template
|
|
459
|
+
? assembleStaticRouteArtifact(decoded.artifact, options.template)
|
|
460
|
+
: decoded.artifact
|
|
461
|
+
reusedEntry = artifact !== null
|
|
462
|
+
|
|
463
|
+
if (artifact && options.template !== undefined && cache) {
|
|
464
|
+
try {
|
|
465
|
+
await cache.put(
|
|
466
|
+
key,
|
|
467
|
+
serializePrerenderArtifact(entry.path, artifact),
|
|
468
|
+
)
|
|
469
|
+
} catch (error) {
|
|
470
|
+
console.warn(
|
|
471
|
+
`Flamefront prerender cache write failed for ${entry.path}; continuing with the build.`,
|
|
472
|
+
error,
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.warn(
|
|
479
|
+
`Flamefront prerender cache read failed for ${entry.path}; rendering it again.`,
|
|
480
|
+
error,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (!artifact) {
|
|
486
|
+
artifact = await renderStaticRoute(
|
|
487
|
+
outputRoute,
|
|
488
|
+
request,
|
|
489
|
+
render,
|
|
490
|
+
loadData,
|
|
491
|
+
renderFragment,
|
|
492
|
+
options.template,
|
|
493
|
+
)
|
|
494
|
+
rendered += 1
|
|
495
|
+
|
|
496
|
+
if (cache && key) {
|
|
497
|
+
try {
|
|
498
|
+
await cache.put(key, serializePrerenderArtifact(entry.path, artifact))
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.warn(
|
|
501
|
+
`Flamefront prerender cache write failed for ${entry.path}; continuing with the build.`,
|
|
502
|
+
error,
|
|
503
|
+
)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} else if (reusedEntry) {
|
|
507
|
+
reused += 1
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
await writeStaticRouteArtifact(clientDirectory, outputRoute, artifact)
|
|
511
|
+
console.log(
|
|
512
|
+
`${reusedEntry ? "Reused" : "Generated"} ${relative(root, staticRouteFile(clientDirectory, outputRoute))}.`,
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { rendered, reused }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export async function buildProject(
|
|
520
|
+
root = process.cwd(),
|
|
521
|
+
buildOptions: BuildProjectOptions = {},
|
|
522
|
+
): Promise<void> {
|
|
228
523
|
const { app } = await loadProject(root)
|
|
524
|
+
const flamefrontOptions = await loadBuildFlamefrontOptions(root)
|
|
229
525
|
const { build } = await import("vite")
|
|
230
526
|
const dist = resolve(root, "dist")
|
|
231
527
|
const clientDirectory = resolve(dist, "client")
|
|
@@ -284,33 +580,72 @@ export async function buildProject(root = process.cwd()): Promise<void> {
|
|
|
284
580
|
return
|
|
285
581
|
}
|
|
286
582
|
|
|
287
|
-
|
|
583
|
+
const render = (request: Request) =>
|
|
584
|
+
serverEntry.renderDocument(clientTemplate, request, { mode: "static" })
|
|
585
|
+
const loadData = async (request: Request) => {
|
|
586
|
+
const endpoint = new URL(app.routing.dataPath, request.url)
|
|
587
|
+
|
|
588
|
+
endpoint.searchParams.set("url", request.url)
|
|
589
|
+
const response = await serverEntry.loadRouteData(
|
|
590
|
+
new Request(endpoint, {
|
|
591
|
+
headers: request.headers,
|
|
592
|
+
signal: request.signal,
|
|
593
|
+
}),
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
if (!response.ok) {
|
|
597
|
+
throw response
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return response.json()
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const renderFragment = serverEntry.renderFragment
|
|
604
|
+
? (request: Request) => serverEntry.renderFragment!(request)
|
|
605
|
+
: undefined
|
|
606
|
+
|
|
607
|
+
if (!flamefrontOptions?.prerender) {
|
|
608
|
+
await prerenderStaticRoutes(
|
|
609
|
+
root,
|
|
610
|
+
clientDirectory,
|
|
611
|
+
staticRoutes,
|
|
612
|
+
render,
|
|
613
|
+
loadData,
|
|
614
|
+
app.routing,
|
|
615
|
+
renderFragment,
|
|
616
|
+
)
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const entries = await resolvePrerenderEntries(
|
|
621
|
+
root,
|
|
622
|
+
app,
|
|
623
|
+
flamefrontOptions.prerender,
|
|
624
|
+
)
|
|
625
|
+
const stats = await prerenderStaticRouteEntries(
|
|
288
626
|
root,
|
|
289
627
|
clientDirectory,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
628
|
+
entries,
|
|
629
|
+
render,
|
|
630
|
+
loadData,
|
|
631
|
+
app.routing,
|
|
632
|
+
renderFragment,
|
|
633
|
+
{
|
|
634
|
+
cache: flamefrontOptions.prerender.cache,
|
|
635
|
+
force: buildOptions.forcePrerender,
|
|
636
|
+
revision: flamefrontOptions.prerender.revision,
|
|
637
|
+
template: clientTemplate,
|
|
638
|
+
fingerprint: (route, path) =>
|
|
639
|
+
renderingFingerprint(root, app, route, path, {
|
|
640
|
+
markdown: flamefrontOptions.markdown,
|
|
641
|
+
target: flamefrontOptions.target,
|
|
642
|
+
template: clientTemplate,
|
|
301
643
|
}),
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
if (!response.ok) {
|
|
305
|
-
throw response
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return response.json()
|
|
309
644
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
console.log(
|
|
648
|
+
`Prerendered ${stats.rendered} pages, reused ${stats.reused} cached pages.`,
|
|
314
649
|
)
|
|
315
650
|
}
|
|
316
651
|
|
|
@@ -324,40 +659,19 @@ export async function prerenderStaticRoutes(
|
|
|
324
659
|
renderFragment?: (request: Request) => Promise<RouteFragmentArtifact>,
|
|
325
660
|
): Promise<void> {
|
|
326
661
|
for (const route of routes) {
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
const outputFragmentFile = staticRouteFragmentFile(clientDirectory, route)
|
|
330
|
-
const outputFragmentDataFile = staticRouteFragmentDataFile(
|
|
331
|
-
clientDirectory,
|
|
662
|
+
const request = staticRouteRequest(routing, route.path)
|
|
663
|
+
const artifact = await renderStaticRoute(
|
|
332
664
|
route,
|
|
665
|
+
request,
|
|
666
|
+
render,
|
|
667
|
+
loadData,
|
|
668
|
+
renderFragment,
|
|
333
669
|
)
|
|
334
|
-
|
|
335
|
-
|
|
670
|
+
|
|
671
|
+
await writeStaticRouteArtifact(clientDirectory, route, artifact)
|
|
672
|
+
console.log(
|
|
673
|
+
`Generated ${relative(root, staticRouteFile(clientDirectory, route))}.`,
|
|
336
674
|
)
|
|
337
|
-
const rendered = documentParts(await render(request))
|
|
338
|
-
const data = rendered.hasRouteData
|
|
339
|
-
? rendered.routeData
|
|
340
|
-
: loadData
|
|
341
|
-
? await loadData(request)
|
|
342
|
-
: null
|
|
343
|
-
const fragment = renderFragment
|
|
344
|
-
? await renderFragment(request)
|
|
345
|
-
: ({
|
|
346
|
-
protocol: routeFragmentProtocol,
|
|
347
|
-
route: route.path,
|
|
348
|
-
boundary: route.entry,
|
|
349
|
-
html: rendered.html,
|
|
350
|
-
routeData: data,
|
|
351
|
-
boundaries: [],
|
|
352
|
-
status: rendered.status,
|
|
353
|
-
} satisfies RouteFragmentArtifact)
|
|
354
|
-
|
|
355
|
-
await mkdir(dirname(outputFile), { recursive: true })
|
|
356
|
-
await writeFile(outputFile, rendered.html)
|
|
357
|
-
await writeFile(outputDataFile, JSON.stringify(data ?? null))
|
|
358
|
-
await writeFile(outputFragmentFile, fragment.html)
|
|
359
|
-
await writeFile(outputFragmentDataFile, JSON.stringify(fragment))
|
|
360
|
-
console.log(`Generated ${relative(root, outputFile)}.`)
|
|
361
675
|
}
|
|
362
676
|
}
|
|
363
677
|
|
package/src/octane.tsx
CHANGED
|
@@ -125,6 +125,7 @@ export interface OctaneDocuments {
|
|
|
125
125
|
options?: RenderDocumentOptions,
|
|
126
126
|
) => Promise<RenderedDocument>
|
|
127
127
|
readonly loadRouteData: (request: Request) => Promise<Response>
|
|
128
|
+
readonly loadAction: (request: Request) => Promise<Response>
|
|
128
129
|
readonly renderFragment: (request: Request) => Promise<RouteFragmentArtifact>
|
|
129
130
|
}
|
|
130
131
|
|
|
@@ -139,6 +140,7 @@ interface StaticDocumentContext {
|
|
|
139
140
|
readonly pathnameBase?: string
|
|
140
141
|
readonly route?: { readonly id?: string }
|
|
141
142
|
}[]
|
|
143
|
+
readonly actionHeaders?: Record<string, Headers>
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
function isRouteErrorResponse(
|
|
@@ -324,6 +326,26 @@ function routeData(context: StaticDocumentContext): unknown {
|
|
|
324
326
|
return routeId ? (context.loaderData?.[routeId] ?? null) : null
|
|
325
327
|
}
|
|
326
328
|
|
|
329
|
+
function actionResponseHeaders(
|
|
330
|
+
context: StaticDocumentContext,
|
|
331
|
+
): Headers | undefined {
|
|
332
|
+
if (!context.actionHeaders) {
|
|
333
|
+
return undefined
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const headers = new Headers()
|
|
337
|
+
let hasHeaders = false
|
|
338
|
+
|
|
339
|
+
for (const value of Object.values(context.actionHeaders)) {
|
|
340
|
+
for (const [name, header] of value) {
|
|
341
|
+
headers.append(name, header)
|
|
342
|
+
hasHeaders = true
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return hasHeaders ? headers : undefined
|
|
347
|
+
}
|
|
348
|
+
|
|
327
349
|
function remapShellRouteError(
|
|
328
350
|
route: RouteDefinition | null,
|
|
329
351
|
context: StaticDocumentContext,
|
|
@@ -520,7 +542,7 @@ export function createOctaneDocuments<
|
|
|
520
542
|
readonly rendered: OctaneRenderResult
|
|
521
543
|
}> => {
|
|
522
544
|
const contextOptions: RouteRuntimeContextOptions = {
|
|
523
|
-
purpose: "document",
|
|
545
|
+
purpose: ["GET", "HEAD"].includes(request.method) ? "document" : "action",
|
|
524
546
|
mode,
|
|
525
547
|
}
|
|
526
548
|
const requestContext = await options.runtime.createRequestContext(
|
|
@@ -624,9 +646,16 @@ export function createOctaneDocuments<
|
|
|
624
646
|
parts.hydrationScript,
|
|
625
647
|
))
|
|
626
648
|
|
|
649
|
+
const headers = actionResponseHeaders(staticContext)
|
|
650
|
+
|
|
627
651
|
return mode === "static"
|
|
628
|
-
? {
|
|
629
|
-
|
|
652
|
+
? {
|
|
653
|
+
html,
|
|
654
|
+
routeData: routeData(staticContext),
|
|
655
|
+
status,
|
|
656
|
+
...(headers ? { headers } : {}),
|
|
657
|
+
}
|
|
658
|
+
: { html, status, ...(headers ? { headers } : {}) }
|
|
630
659
|
}
|
|
631
660
|
|
|
632
661
|
const renderFragment = async (
|
|
@@ -656,6 +685,7 @@ export function createOctaneDocuments<
|
|
|
656
685
|
return {
|
|
657
686
|
renderDocument,
|
|
658
687
|
loadRouteData: options.runtime.loadRouteData,
|
|
688
|
+
loadAction: options.runtime.loadAction,
|
|
659
689
|
renderFragment,
|
|
660
690
|
}
|
|
661
691
|
}
|