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/server.ts
CHANGED
|
@@ -9,6 +9,27 @@ import {
|
|
|
9
9
|
type RouteDefinition,
|
|
10
10
|
} from "./index.ts"
|
|
11
11
|
import { stripFlamefrontProtocolRequest } from "./fragment-protocol.ts"
|
|
12
|
+
import {
|
|
13
|
+
actionErrorResponse,
|
|
14
|
+
actionResultResponse,
|
|
15
|
+
executeRegisteredAction,
|
|
16
|
+
getRegisteredAction,
|
|
17
|
+
isSameOriginActionRequest,
|
|
18
|
+
parseActionArguments,
|
|
19
|
+
} from "./action.ts"
|
|
20
|
+
|
|
21
|
+
export { action } from "./action.ts"
|
|
22
|
+
export type {
|
|
23
|
+
ActionDataWithResponseInit,
|
|
24
|
+
ActionFunction,
|
|
25
|
+
ActionInput,
|
|
26
|
+
ActionOutput,
|
|
27
|
+
ActionValidationError,
|
|
28
|
+
StandardSchema,
|
|
29
|
+
StandardSchemaIssue,
|
|
30
|
+
StandardSchemaV1,
|
|
31
|
+
} from "./action.ts"
|
|
32
|
+
export { data, redirect } from "@octanejs/remix-router"
|
|
12
33
|
|
|
13
34
|
type LoaderPath<ContextOrPath, PathOrContext> =
|
|
14
35
|
ContextOrPath extends `/${string}`
|
|
@@ -32,12 +53,24 @@ export interface LoaderArgs<ContextOrPath = unknown, PathOrContext = unknown> {
|
|
|
32
53
|
readonly context: LoaderContext<ContextOrPath, PathOrContext>
|
|
33
54
|
}
|
|
34
55
|
|
|
56
|
+
/** Arguments passed to a page module's HTTP action. */
|
|
57
|
+
export interface ActionArgs<
|
|
58
|
+
ContextOrPath = unknown,
|
|
59
|
+
PathOrContext = unknown,
|
|
60
|
+
> extends LoaderArgs<ContextOrPath, PathOrContext> {}
|
|
61
|
+
|
|
35
62
|
export type Loader<
|
|
36
63
|
Data = unknown,
|
|
37
64
|
Context = unknown,
|
|
38
65
|
Path extends string = string,
|
|
39
66
|
> = (args: LoaderArgs<Context, Path>) => Data | Promise<Data>
|
|
40
67
|
|
|
68
|
+
export type RouteAction<
|
|
69
|
+
Data = unknown,
|
|
70
|
+
Context = unknown,
|
|
71
|
+
Path extends string = string,
|
|
72
|
+
> = (args: ActionArgs<Context, Path>) => Data | Promise<Data>
|
|
73
|
+
|
|
41
74
|
export interface RouteModule<
|
|
42
75
|
Data = unknown,
|
|
43
76
|
Context = unknown,
|
|
@@ -45,10 +78,11 @@ export interface RouteModule<
|
|
|
45
78
|
> {
|
|
46
79
|
readonly default: unknown
|
|
47
80
|
readonly loader?: Loader<Data, Context, Path>
|
|
81
|
+
readonly action?: RouteAction<unknown, Context, Path>
|
|
48
82
|
}
|
|
49
83
|
|
|
50
84
|
export type DocumentMode = "shell" | RenderMode
|
|
51
|
-
export type RequestPurpose = "data" | "document"
|
|
85
|
+
export type RequestPurpose = "data" | "document" | "action"
|
|
52
86
|
|
|
53
87
|
/** Inputs for constructing one request-scoped value for route work. */
|
|
54
88
|
export interface RequestContextArgs<
|
|
@@ -71,7 +105,10 @@ export type RouteImporter<
|
|
|
71
105
|
Data = unknown,
|
|
72
106
|
Context = unknown,
|
|
73
107
|
Path extends string = string,
|
|
74
|
-
> = (entry: string) => Promise<RouteModule<Data, Context, Path>>
|
|
108
|
+
> = ((entry: string) => Promise<RouteModule<Data, Context, Path>>) & {
|
|
109
|
+
/** Optional eager import hook used by generated server action registries. */
|
|
110
|
+
readonly loadActions?: () => void | Promise<void>
|
|
111
|
+
}
|
|
75
112
|
|
|
76
113
|
export interface RenderedDocument {
|
|
77
114
|
readonly html: string
|
|
@@ -156,6 +193,7 @@ export interface RouteRuntime<
|
|
|
156
193
|
options?: RouteLoadOptions<Context>,
|
|
157
194
|
) => Promise<LoadedRouteFor<Route, Context> | null>
|
|
158
195
|
readonly loadRouteData: (request: Request) => Promise<Response>
|
|
196
|
+
readonly loadAction: (request: Request) => Promise<Response>
|
|
159
197
|
}
|
|
160
198
|
|
|
161
199
|
export interface RouteRuntimeOptions<
|
|
@@ -164,6 +202,8 @@ export interface RouteRuntimeOptions<
|
|
|
164
202
|
> {
|
|
165
203
|
readonly app: AppDefinition<Route>
|
|
166
204
|
readonly importRoute: RouteImporter<unknown, Context>
|
|
205
|
+
/** Eagerly load generated action modules before direct dispatch. */
|
|
206
|
+
readonly loadActions?: () => void | Promise<void>
|
|
167
207
|
/** Build request context for data requests and document router queries. */
|
|
168
208
|
readonly requestContext?: RequestContextFactory<Context, Route>
|
|
169
209
|
}
|
|
@@ -279,6 +319,71 @@ export function createRouteRuntime<
|
|
|
279
319
|
return Response.json(loaded.loaderData ?? null)
|
|
280
320
|
}
|
|
281
321
|
|
|
322
|
+
const loadAction = async (request: Request): Promise<Response> => {
|
|
323
|
+
const sanitizedRequest = stripFlamefrontProtocolRequest(request)
|
|
324
|
+
const url = new URL(sanitizedRequest.url)
|
|
325
|
+
|
|
326
|
+
if (["GET", "HEAD", "OPTIONS"].includes(sanitizedRequest.method)) {
|
|
327
|
+
return new Response("Method not allowed.", { status: 405 })
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!isSameOriginActionRequest(sanitizedRequest)) {
|
|
331
|
+
return new Response("Forbidden.", { status: 403 })
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const actionId = url.searchParams.get("action")
|
|
335
|
+
|
|
336
|
+
if (actionId) {
|
|
337
|
+
try {
|
|
338
|
+
const args = await parseActionArguments(sanitizedRequest)
|
|
339
|
+
|
|
340
|
+
if (!getRegisteredAction(actionId)) {
|
|
341
|
+
await (options.loadActions ?? options.importRoute.loadActions)?.()
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return await executeRegisteredAction(actionId, args)
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return actionErrorResponse(error)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const match = options.app.match(sanitizedRequest.url)
|
|
351
|
+
|
|
352
|
+
if (!match) {
|
|
353
|
+
return new Response("Not found.", { status: 404 })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (match.data.render === "static") {
|
|
357
|
+
return new Response(
|
|
358
|
+
"Static routes cannot define actions; submit to a server route instead.",
|
|
359
|
+
{ status: 405 },
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const routeModule = await options.importRoute(match.data.entry)
|
|
364
|
+
|
|
365
|
+
if (!routeModule.action) {
|
|
366
|
+
return new Response("Method not allowed.", { status: 405 })
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const context = await createRequestContext(
|
|
371
|
+
sanitizedRequest,
|
|
372
|
+
{ purpose: "action", mode: match.data.render },
|
|
373
|
+
match,
|
|
374
|
+
)
|
|
375
|
+
const value = await routeModule.action({
|
|
376
|
+
request: sanitizedRequest,
|
|
377
|
+
params: match.params as ActionArgs<Context, string>["params"],
|
|
378
|
+
context: context as LoaderContext<Context, string>,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
return actionResultResponse(value)
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return actionErrorResponse(error)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
282
387
|
return {
|
|
283
388
|
app: options.app,
|
|
284
389
|
importRoute: options.importRoute,
|
|
@@ -287,6 +392,7 @@ export function createRouteRuntime<
|
|
|
287
392
|
createRequestContext(request, contextOptions),
|
|
288
393
|
loadRoute: loadRouteForRequest,
|
|
289
394
|
loadRouteData,
|
|
395
|
+
loadAction,
|
|
290
396
|
}
|
|
291
397
|
}
|
|
292
398
|
|
package/src/srvx.ts
CHANGED
|
@@ -133,6 +133,10 @@ export function createSrvxServerEntry<
|
|
|
133
133
|
return next()
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
if (!["GET", "HEAD", "OPTIONS"].includes(request.method)) {
|
|
137
|
+
return next()
|
|
138
|
+
}
|
|
139
|
+
|
|
136
140
|
return serveClientFile(
|
|
137
141
|
staticRequest(request, options.app.routing.basename),
|
|
138
142
|
next,
|
|
@@ -169,6 +173,7 @@ export function createSrvxServerEntry<
|
|
|
169
173
|
middleware: [...(options.middleware ?? []), frameworkMiddleware],
|
|
170
174
|
renderDocument: fetchEntry.renderDocument,
|
|
171
175
|
loadRouteData: fetchEntry.loadRouteData,
|
|
176
|
+
...(fetchEntry.loadAction ? { loadAction: fetchEntry.loadAction } : {}),
|
|
172
177
|
renderFragment: fetchEntry.renderFragment,
|
|
173
178
|
}
|
|
174
179
|
}
|
|
@@ -12,17 +12,17 @@ function isWithin(directory: string, filePath: string): boolean {
|
|
|
12
12
|
)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function
|
|
15
|
+
export function staticRoutePathForPath(
|
|
16
16
|
clientDirectory: string,
|
|
17
|
-
|
|
17
|
+
routePath: string,
|
|
18
18
|
): string {
|
|
19
|
-
if (/[:*]/.test(
|
|
19
|
+
if (/[:*]/.test(routePath)) {
|
|
20
20
|
throw new Error(
|
|
21
|
-
`Cannot prerender parameterized static route ${JSON.stringify(
|
|
21
|
+
`Cannot prerender parameterized static route ${JSON.stringify(routePath)} without concrete paths.`,
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const segments =
|
|
25
|
+
const segments = routePath
|
|
26
26
|
.split("/")
|
|
27
27
|
.filter(Boolean)
|
|
28
28
|
.map((segment) => decodeURIComponent(segment))
|
|
@@ -33,7 +33,7 @@ function staticRoutePath(
|
|
|
33
33
|
)
|
|
34
34
|
) {
|
|
35
35
|
throw new Error(
|
|
36
|
-
`Cannot write unsafe static route path ${JSON.stringify(
|
|
36
|
+
`Cannot write unsafe static route path ${JSON.stringify(routePath)}.`,
|
|
37
37
|
)
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -41,13 +41,20 @@ function staticRoutePath(
|
|
|
41
41
|
|
|
42
42
|
if (!isWithin(clientDirectory, filePath)) {
|
|
43
43
|
throw new Error(
|
|
44
|
-
`Cannot write static route outside the client build: ${JSON.stringify(
|
|
44
|
+
`Cannot write static route outside the client build: ${JSON.stringify(routePath)}.`,
|
|
45
45
|
)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
return filePath
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function staticRoutePath(
|
|
52
|
+
clientDirectory: string,
|
|
53
|
+
route: RouteDefinition,
|
|
54
|
+
): string {
|
|
55
|
+
return staticRoutePathForPath(clientDirectory, route.path)
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
export function staticRouteFile(
|
|
52
59
|
clientDirectory: string,
|
|
53
60
|
route: RouteDefinition,
|
|
@@ -17,6 +17,7 @@ declare module "virtual:flamefront/server-routes" {
|
|
|
17
17
|
import type { RouteModule } from "./server.ts"
|
|
18
18
|
|
|
19
19
|
export function importRoute(entry: string): Promise<RouteModule>
|
|
20
|
+
export function loadActions(): Promise<void>
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
declare module "virtual:flamefront/server-entry" {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PrerenderOptions } from "./prerender.ts"
|
|
2
|
+
|
|
3
|
+
export interface RegisteredFlamefrontOptions {
|
|
4
|
+
readonly prerender?: PrerenderOptions
|
|
5
|
+
readonly markdown?: unknown
|
|
6
|
+
readonly target?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const byRoot = new Map<string, RegisteredFlamefrontOptions>()
|
|
10
|
+
let pending: RegisteredFlamefrontOptions | undefined
|
|
11
|
+
|
|
12
|
+
export function registerFlamefrontOptions(
|
|
13
|
+
options: RegisteredFlamefrontOptions,
|
|
14
|
+
): void {
|
|
15
|
+
pending = options
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerFlamefrontRoot(
|
|
19
|
+
root: string,
|
|
20
|
+
options: RegisteredFlamefrontOptions,
|
|
21
|
+
): void {
|
|
22
|
+
byRoot.set(root, options)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getFlamefrontOptions(
|
|
26
|
+
root: string,
|
|
27
|
+
): RegisteredFlamefrontOptions | undefined {
|
|
28
|
+
return byRoot.get(root) ?? pending
|
|
29
|
+
}
|
package/src/vite.ts
CHANGED
|
@@ -15,12 +15,22 @@ import type {
|
|
|
15
15
|
import { generate, parse, traverse, type Babel } from "./babel.ts"
|
|
16
16
|
import { expandGlob, globDirectory, setGlobRoot } from "./glob.ts"
|
|
17
17
|
import { removeExports } from "./remove-exports.ts"
|
|
18
|
+
import {
|
|
19
|
+
findActionExports,
|
|
20
|
+
generateActionProxyModule,
|
|
21
|
+
transformServerActions,
|
|
22
|
+
} from "./action-transform.ts"
|
|
18
23
|
import { writeRouteImportMap } from "./typegen.ts"
|
|
19
24
|
import {
|
|
20
25
|
resolveFlamefrontOutput,
|
|
21
26
|
type FlamefrontOutputOptions,
|
|
22
27
|
type ResolvedFlamefrontOutput,
|
|
23
28
|
} from "./output.ts"
|
|
29
|
+
import type { PrerenderOptions } from "./prerender.ts"
|
|
30
|
+
import {
|
|
31
|
+
registerFlamefrontOptions,
|
|
32
|
+
registerFlamefrontRoot,
|
|
33
|
+
} from "./vite-options.ts"
|
|
24
34
|
|
|
25
35
|
export type {
|
|
26
36
|
FlamefrontAdapter,
|
|
@@ -37,11 +47,13 @@ const resolvedServerRoutesId = `\0${serverRoutesId}`
|
|
|
37
47
|
|
|
38
48
|
export const serverEntryId = "virtual:flamefront/server-entry"
|
|
39
49
|
const resolvedServerEntryId = `\0${serverEntryId}`
|
|
50
|
+
const actionProxyId = "\0flamefront/action-proxy"
|
|
51
|
+
const actionProxyPrefix = `${actionProxyId}?module=`
|
|
40
52
|
const hydrationRouteId = "/@flamefront/hydration-route.tsrx"
|
|
41
53
|
const resolvedHydrationRoutePrefix = `${hydrationRouteId}?`
|
|
42
54
|
const markdownRouteId = "/@flamefront/markdown-route.tsrx"
|
|
43
55
|
const resolvedMarkdownRoutePrefix = `${markdownRouteId}?`
|
|
44
|
-
const SERVER_ONLY_ROUTE_EXPORTS = ["loader"] as const
|
|
56
|
+
const SERVER_ONLY_ROUTE_EXPORTS = ["loader", "action"] as const
|
|
45
57
|
const serverFilePattern = /\.server(?:\.[cm]?[jt]sx?|\.tsrx)$/
|
|
46
58
|
const serverDirectoryPattern = /\/\.server\//
|
|
47
59
|
|
|
@@ -50,6 +62,8 @@ export interface FlamefrontOptions extends FlamefrontOutputOptions {
|
|
|
50
62
|
readonly routes?: string
|
|
51
63
|
/** Built-in Markdown and MDX compiler configuration. */
|
|
52
64
|
readonly markdown?: false | MarkdownOptions
|
|
65
|
+
/** Configure incremental static rendering and its persistent cache. */
|
|
66
|
+
readonly prerender?: PrerenderOptions
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
export interface MarkdownOptions {
|
|
@@ -210,21 +224,22 @@ function lazyRoute(
|
|
|
210
224
|
routeDefinition.render === "client"
|
|
211
225
|
? `(args) => ${browserLoader}(args, ${JSON.stringify(routing)})`
|
|
212
226
|
: `(args) => ${browserLoader}(args, ${JSON.stringify(routing)}, ${quote(routeDefinition.render)})`
|
|
227
|
+
const browserActionExpression = `(args) => submitRouteAction(args, ${JSON.stringify(routing)})`
|
|
213
228
|
|
|
214
229
|
if (routeDefinition.render !== "client") {
|
|
215
230
|
const componentId = browserRouteModuleId(routeDefinition)
|
|
216
231
|
|
|
217
|
-
return `async () => { if (import.meta.env.SSR) { const [routeModule, componentModule] = await Promise.all([import(${quote(entry)}), import(${quote(componentId)})]); return { Component: createRouteBoundary(componentModule.default, ${JSON.stringify(metadata)}), loader: routeModule.loader }; } const componentModule = await import(${quote(componentId)}); return { Component: createRouteFragmentRoute({ metadata: ${JSON.stringify(metadata)}, routing: ${JSON.stringify(routing)}, policy: ${quote(routeDefinition.render)}, fallbackComponent: componentModule.default }), loader: ${browserLoaderExpression} }; }`
|
|
232
|
+
return `async () => { if (import.meta.env.SSR) { const [routeModule, componentModule] = await Promise.all([import(${quote(entry)}), import(${quote(componentId)})]); return { Component: createRouteBoundary(componentModule.default, ${JSON.stringify(metadata)}), loader: routeModule.loader, action: routeModule.action }; } const componentModule = await import(${quote(componentId)}); return { Component: createRouteFragmentRoute({ metadata: ${JSON.stringify(metadata)}, routing: ${JSON.stringify(routing)}, policy: ${quote(routeDefinition.render)}, fallbackComponent: componentModule.default }), loader: ${browserLoaderExpression}, action: ${browserActionExpression} }; }`
|
|
218
233
|
}
|
|
219
234
|
|
|
220
235
|
if (routeDefinition.render === "client") {
|
|
221
236
|
if (routeDefinition.content === "markdown") {
|
|
222
237
|
const componentId = browserRouteModuleId(routeDefinition)
|
|
223
238
|
|
|
224
|
-
return `async () => { if (import.meta.env.SSR) return {}; const componentModule = await import(${quote(componentId)}); return { Component: createRouteBoundary(componentModule.default, ${JSON.stringify(metadata)}), loader: ${browserLoaderExpression} }; }`
|
|
239
|
+
return `async () => { if (import.meta.env.SSR) return {}; const componentModule = await import(${quote(componentId)}); return { Component: createRouteBoundary(componentModule.default, ${JSON.stringify(metadata)}), loader: ${browserLoaderExpression}, action: ${browserActionExpression} }; }`
|
|
225
240
|
}
|
|
226
241
|
|
|
227
|
-
return `async () => { if (import.meta.env.SSR) return {}; const routeModule = await import(${quote(entry)}); return { Component: createRouteBoundary(routeModule.default, ${JSON.stringify(metadata)}), loader: ${browserLoaderExpression} }; }`
|
|
242
|
+
return `async () => { if (import.meta.env.SSR) return {}; const routeModule = await import(${quote(entry)}); return { Component: createRouteBoundary(routeModule.default, ${JSON.stringify(metadata)}), loader: ${browserLoaderExpression}, action: ${browserActionExpression} }; }`
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
throw new Error(`Unsupported route render mode ${routeDefinition.render}.`)
|
|
@@ -290,6 +305,12 @@ function generateConfigs(
|
|
|
290
305
|
return `${indent}{\n${childIndent}id: ${quote(metadata.id)},\n${childIndent}lazy: ${lazyLayout(config.entry, metadata)},\n${childIndent}handle: { flamefront: ${JSON.stringify(metadata)} },\n${childIndent}children: [\n${generateConfigs(config.children, routing, depth + 2, metadata.id, location)}\n${childIndent}],\n${indent}}`
|
|
291
306
|
}
|
|
292
307
|
|
|
308
|
+
if (/\.(md|mdx)$/.test(cleanModuleId(config.entry))) {
|
|
309
|
+
const lazy = lazyRoute(config, routing, metadata)
|
|
310
|
+
|
|
311
|
+
return `${indent}{\n${childIndent}id: ${quote(metadata.id)},\n${childIndent}path: ${quote(config.path)},\n${childIndent}lazy: async () => { const [routeModule, resolved] = await Promise.all([import(${quote(config.entry)}), (${lazy})()]); return { ...resolved, handle: { flamefront: ${JSON.stringify(metadata)}, frontmatter: routeModule.frontmatter ?? {} } }; },\n${indent}}`
|
|
312
|
+
}
|
|
313
|
+
|
|
293
314
|
return `${indent}{\n${childIndent}id: ${quote(metadata.id)},\n${childIndent}path: ${quote(config.path)},\n${childIndent}lazy: ${lazyRoute(config, routing, metadata)},\n${childIndent}handle: { flamefront: ${JSON.stringify(metadata)} },\n${indent}}`
|
|
294
315
|
})
|
|
295
316
|
.join(",\n")
|
|
@@ -308,19 +329,31 @@ export function generateRemixRoutes(
|
|
|
308
329
|
app.shellHydration,
|
|
309
330
|
)
|
|
310
331
|
|
|
311
|
-
return `// Generated by Flamefront.\nimport Shell from ${quote(app.shell)};\nimport { RouterDocument } from 'flamefront/octane/router-document';\nimport { createRouteBoundary, createRouteFragmentRoute } from 'flamefront/fragment';\nimport { loadRouteData, loadRouteFragment } from 'flamefront/remix-router/data';\n\nexport { RouterDocument };\nexport const routing = ${JSON.stringify(app.routing)};\nexport const routeMetadata = ${JSON.stringify(routeMetadata)};\n\nexport const routes = [\n\t{\n\t\tid: ${quote(rootId)},\n\t\tComponent: createRouteBoundary(Shell, ${JSON.stringify(routeMetadata[0])}),\n\t\thandle: { flamefront: ${JSON.stringify(routeMetadata[0])} },\n\t\tchildren: [\n${generateConfigs(app.routeTree, app.routing, 3)}\n\t\t],\n\t},\n];\n\n${generateRoutePreloaders(app.routeTree)}`
|
|
332
|
+
return `// Generated by Flamefront.\nimport Shell from ${quote(app.shell)};\nimport { RouterDocument } from 'flamefront/octane/router-document';\nimport { createRouteBoundary, createRouteFragmentRoute } from 'flamefront/fragment';\nimport { loadRouteData, loadRouteFragment } from 'flamefront/remix-router/data';\nimport { submitRouteAction } from 'flamefront/remix-router/data';\n\nexport { RouterDocument };\nexport const routing = ${JSON.stringify(app.routing)};\nexport const routeMetadata = ${JSON.stringify(routeMetadata)};\n\nexport const routes = [\n\t{\n\t\tid: ${quote(rootId)},\n\t\tComponent: createRouteBoundary(Shell, ${JSON.stringify(routeMetadata[0])}),\n\t\thandle: { flamefront: ${JSON.stringify(routeMetadata[0])} },\n\t\tchildren: [\n${generateConfigs(app.routeTree, app.routing, 3)}\n\t\t],\n\t},\n];\n\n${generateRoutePreloaders(app.routeTree)}`
|
|
312
333
|
}
|
|
313
334
|
|
|
314
335
|
/** Generate the server-only route-module importer used by loader endpoints. */
|
|
315
336
|
export function generateServerRoutes(
|
|
316
|
-
app: Pick<AppDefinition, "routes">,
|
|
337
|
+
app: Pick<AppDefinition, "routes" | "shell" | "routeTree">,
|
|
317
338
|
): string {
|
|
318
|
-
const entries =
|
|
319
|
-
const
|
|
339
|
+
const entries = new Set<string>([app.shell])
|
|
340
|
+
const collect = (configs: readonly RouteConfig[]) => {
|
|
341
|
+
for (const config of configs) {
|
|
342
|
+
entries.add(config.entry)
|
|
343
|
+
|
|
344
|
+
if ("children" in config) {
|
|
345
|
+
collect(config.children)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
collect(app.routeTree)
|
|
351
|
+
|
|
352
|
+
const imports = [...entries]
|
|
320
353
|
.map((entry) => `\t${quote(entry)}: () => import(${quote(entry)})`)
|
|
321
354
|
.join(",\n")
|
|
322
355
|
|
|
323
|
-
return `// Generated by Flamefront.\nconst routeModules = {\n${imports}\n};\n\nexport async function importRoute(entry) {\n\tconst importModule = routeModules[entry];\n\tif (!importModule) throw new Error(\`No Vite route module was generated for \${entry}.\`);\n\treturn importModule();\n}\n`
|
|
356
|
+
return `// Generated by Flamefront.\nconst routeModules = {\n${imports}\n};\n\nlet actionModulesPromise;\nexport function loadActions() {\n\tactionModulesPromise ??= Promise.all(Object.values(routeModules).map((load) => load())).then(() => undefined);\n\treturn actionModulesPromise;\n}\n\nexport async function importRoute(entry) {\n\tconst importModule = routeModules[entry];\n\tif (!importModule) throw new Error(\`No Vite route module was generated for \${entry}.\`);\n\treturn importModule();\n}\n\nimportRoute.loadActions = loadActions;\n`
|
|
324
357
|
}
|
|
325
358
|
|
|
326
359
|
/** Generate the server-entry import selected by `flamefront({...})`. */
|
|
@@ -556,7 +589,7 @@ function isPathWithinDirectory(directory: string, candidate: string): boolean {
|
|
|
556
589
|
}
|
|
557
590
|
|
|
558
591
|
function isServerEnvironment(
|
|
559
|
-
context: PluginContext,
|
|
592
|
+
context: Pick<PluginContext, "environment">,
|
|
560
593
|
options?: TransformOptions,
|
|
561
594
|
): boolean {
|
|
562
595
|
return (
|
|
@@ -684,6 +717,12 @@ export function removeServerRouteExports(source: string, id = "route.js") {
|
|
|
684
717
|
|
|
685
718
|
export function flamefront(options: FlamefrontOptions = {}) {
|
|
686
719
|
const output = resolveFlamefrontOutput(options)
|
|
720
|
+
|
|
721
|
+
registerFlamefrontOptions({
|
|
722
|
+
prerender: options.prerender,
|
|
723
|
+
markdown: options.markdown,
|
|
724
|
+
target: options.target,
|
|
725
|
+
})
|
|
687
726
|
let root = process.cwd()
|
|
688
727
|
let serverBuild = false
|
|
689
728
|
let appPromise: Promise<AppDefinition> | undefined
|
|
@@ -723,6 +762,11 @@ export function flamefront(options: FlamefrontOptions = {}) {
|
|
|
723
762
|
writeRouteImportMap(await loadApp(), { root })
|
|
724
763
|
const configureRoot = (config: { readonly root: string }) => {
|
|
725
764
|
root = config.root
|
|
765
|
+
registerFlamefrontRoot(root, {
|
|
766
|
+
prerender: options.prerender,
|
|
767
|
+
markdown: options.markdown,
|
|
768
|
+
target: options.target,
|
|
769
|
+
})
|
|
726
770
|
}
|
|
727
771
|
|
|
728
772
|
const frameworkModulesPlugin = {
|
|
@@ -732,7 +776,44 @@ export function flamefront(options: FlamefrontOptions = {}) {
|
|
|
732
776
|
async buildStart() {
|
|
733
777
|
await generateTypes()
|
|
734
778
|
},
|
|
735
|
-
transform(
|
|
779
|
+
transform(
|
|
780
|
+
this: object,
|
|
781
|
+
source: string,
|
|
782
|
+
id: string,
|
|
783
|
+
transformOptions?: TransformOptions,
|
|
784
|
+
) {
|
|
785
|
+
const context = this as Pick<PluginContext, "environment">
|
|
786
|
+
|
|
787
|
+
if (
|
|
788
|
+
isServerEnvironment(context, transformOptions) &&
|
|
789
|
+
!cleanModuleId(id).endsWith(".tsrx") &&
|
|
790
|
+
source.includes("action") &&
|
|
791
|
+
/flamefront(?:\/server)?["']/.test(source)
|
|
792
|
+
) {
|
|
793
|
+
const transformedActions = transformServerActions(source, id)
|
|
794
|
+
|
|
795
|
+
if (transformedActions) {
|
|
796
|
+
return {
|
|
797
|
+
code: transformedActions.code,
|
|
798
|
+
map: transformedActions.map,
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (
|
|
804
|
+
!cleanModuleId(id).endsWith(".tsrx") &&
|
|
805
|
+
source.includes("action") &&
|
|
806
|
+
/flamefront(?:\/server)?["']/.test(source)
|
|
807
|
+
) {
|
|
808
|
+
const clientActions = findActionExports(source, id)
|
|
809
|
+
|
|
810
|
+
if (clientActions.length > 0) {
|
|
811
|
+
throw new Error(
|
|
812
|
+
`Flamefront actions must be declared in a *.server.ts module: ${JSON.stringify(path.relative(root, cleanModuleId(id)) || id)}.`,
|
|
813
|
+
)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
736
817
|
if (cleanModuleId(id) !== manifestPath()) {
|
|
737
818
|
return null
|
|
738
819
|
}
|
|
@@ -836,6 +917,10 @@ export function flamefront(options: FlamefrontOptions = {}) {
|
|
|
836
917
|
return id
|
|
837
918
|
}
|
|
838
919
|
|
|
920
|
+
if (id.startsWith(actionProxyPrefix)) {
|
|
921
|
+
return id
|
|
922
|
+
}
|
|
923
|
+
|
|
839
924
|
if (
|
|
840
925
|
resolveOptions.scan ||
|
|
841
926
|
isServerEnvironment(this, resolveOptions) ||
|
|
@@ -867,10 +952,26 @@ export function flamefront(options: FlamefrontOptions = {}) {
|
|
|
867
952
|
return null
|
|
868
953
|
}
|
|
869
954
|
|
|
955
|
+
let actionSource: string
|
|
956
|
+
|
|
957
|
+
try {
|
|
958
|
+
actionSource = fs.readFileSync(resolvedId, "utf8")
|
|
959
|
+
} catch {
|
|
960
|
+
actionSource = ""
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (actionSource) {
|
|
964
|
+
const actions = findActionExports(actionSource, resolvedId)
|
|
965
|
+
|
|
966
|
+
if (actions.length > 0) {
|
|
967
|
+
return `${actionProxyPrefix}${encodeURIComponent(resolvedId)}`
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
870
971
|
const importerId = cleanModuleId(importer)
|
|
871
972
|
const importerLabel = path.relative(root, importerId) || importerId
|
|
872
973
|
const routeHint = (await loadRouteModuleIds()).has(importerId)
|
|
873
|
-
? " Flamefront removes server imports used exclusively by `loader`, but this import is still referenced by client code."
|
|
974
|
+
? " Flamefront removes server imports used exclusively by `loader` or `action`, but this import is still referenced by client code."
|
|
874
975
|
: ""
|
|
875
976
|
|
|
876
977
|
throw new Error(
|
|
@@ -918,6 +1019,23 @@ export function flamefront(options: FlamefrontOptions = {}) {
|
|
|
918
1019
|
return generateMarkdownRoute(entry)
|
|
919
1020
|
}
|
|
920
1021
|
|
|
1022
|
+
if (id.startsWith(actionProxyPrefix)) {
|
|
1023
|
+
const moduleId = new URLSearchParams(
|
|
1024
|
+
id.slice(actionProxyId.length + 1),
|
|
1025
|
+
).get("module")
|
|
1026
|
+
|
|
1027
|
+
if (!moduleId) {
|
|
1028
|
+
throw new TypeError("Flamefront action proxy is missing its module.")
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const source = fs.readFileSync(moduleId, "utf8")
|
|
1032
|
+
|
|
1033
|
+
return generateActionProxyModule(
|
|
1034
|
+
findActionExports(source, moduleId),
|
|
1035
|
+
(await loadApp()).routing,
|
|
1036
|
+
)
|
|
1037
|
+
}
|
|
1038
|
+
|
|
921
1039
|
return null
|
|
922
1040
|
},
|
|
923
1041
|
}
|