flamefront 0.0.0 → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +110 -0
- package/README.md +643 -0
- package/bin/ff-loader.mjs +18 -0
- package/bin/ff.js +6 -0
- package/package.json +63 -10
- package/src/babel.ts +22 -0
- package/src/cli.ts +83 -0
- package/src/fragment-client.ts +231 -0
- package/src/fragment-protocol.ts +39 -0
- package/src/fragment.ts +260 -0
- package/src/index.ts +648 -0
- package/src/lifecycle.ts +486 -0
- package/src/octane-client-core.ts +134 -0
- package/src/octane-client.ts +42 -0
- package/src/octane-router-document.ts +5 -0
- package/src/octane.ts +654 -0
- package/src/remix-route-data.ts +68 -0
- package/src/remix-router-core.ts +106 -0
- package/src/remix-router.ts +111 -0
- package/src/remove-exports.ts +148 -0
- package/src/route-data-client.ts +224 -0
- package/src/route-prefetch.ts +72 -0
- package/src/server.ts +240 -0
- package/src/srvx.ts +314 -0
- package/src/static-fragment-artifacts.ts +86 -0
- package/src/virtual-remix-routes.d.ts +20 -0
- package/src/vite.ts +693 -0
- package/readme.md +0 -1
package/src/vite.ts
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import type {
|
|
5
|
+
AppDefinition,
|
|
6
|
+
GeneratedHydration,
|
|
7
|
+
GeneratedRouteMetadata,
|
|
8
|
+
NormalizedRoutingOptions,
|
|
9
|
+
RouteConfig,
|
|
10
|
+
RouteDefinition,
|
|
11
|
+
} from "./index.ts"
|
|
12
|
+
import { generate, parse } from "./babel.ts"
|
|
13
|
+
import { removeExports } from "./remove-exports.ts"
|
|
14
|
+
|
|
15
|
+
export const remixRoutesId = "virtual:flamefront/remix-routes"
|
|
16
|
+
const resolvedRemixRoutesId = `\0${remixRoutesId}`
|
|
17
|
+
|
|
18
|
+
export const serverRoutesId = "virtual:flamefront/server-routes"
|
|
19
|
+
const resolvedServerRoutesId = `\0${serverRoutesId}`
|
|
20
|
+
const hydrationRouteId = "/@flamefront/hydration-route.tsrx"
|
|
21
|
+
const resolvedHydrationRoutePrefix = `${hydrationRouteId}?`
|
|
22
|
+
const SERVER_ONLY_ROUTE_EXPORTS = ["loader"] as const
|
|
23
|
+
const serverFilePattern = /\.server(?:\.[cm]?[jt]sx?|\.tsrx)$/
|
|
24
|
+
const serverDirectoryPattern = /\/\.server\//
|
|
25
|
+
|
|
26
|
+
export interface FlamefrontOptions {
|
|
27
|
+
/** Project-root route manifest module. */
|
|
28
|
+
readonly routes?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function quote(value: string): string {
|
|
32
|
+
return JSON.stringify(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function lazyLayout(entry: string, metadata: GeneratedRouteMetadata): string {
|
|
36
|
+
return `async () => { const routeModule = await import(${quote(entry)}); return { Component: createRouteBoundary(routeModule.default, ${JSON.stringify(metadata)}) }; }`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function generatesHydrationBoundary(
|
|
40
|
+
routeDefinition: RouteDefinition,
|
|
41
|
+
): routeDefinition is RouteDefinition & {
|
|
42
|
+
hydration: GeneratedHydration | "none"
|
|
43
|
+
} {
|
|
44
|
+
return (
|
|
45
|
+
(routeDefinition.render === "server" ||
|
|
46
|
+
routeDefinition.render === "static") &&
|
|
47
|
+
(routeDefinition.hydration === "none" ||
|
|
48
|
+
typeof routeDefinition.hydration === "object")
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hydrationComponentId(
|
|
53
|
+
entry: string,
|
|
54
|
+
hydration: GeneratedHydration | "none",
|
|
55
|
+
): string {
|
|
56
|
+
const parameters = new URLSearchParams({
|
|
57
|
+
entry,
|
|
58
|
+
hydration: JSON.stringify(hydration),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return `${hydrationRouteId}?${parameters}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function browserRouteModuleId(routeDefinition: RouteDefinition): string {
|
|
65
|
+
return generatesHydrationBoundary(routeDefinition)
|
|
66
|
+
? hydrationComponentId(routeDefinition.entry, routeDefinition.hydration)
|
|
67
|
+
: routeDefinition.entry
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function generatedRouteId(kind: "layout" | "route", location: string): string {
|
|
71
|
+
return `flamefront:${kind}:${location}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function generatedRouteMetadata(
|
|
75
|
+
config: RouteConfig,
|
|
76
|
+
location: string,
|
|
77
|
+
parent: string,
|
|
78
|
+
): GeneratedRouteMetadata {
|
|
79
|
+
if ("children" in config) {
|
|
80
|
+
const id = generatedRouteId("layout", location)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
boundary: id,
|
|
85
|
+
kind: "layout",
|
|
86
|
+
entry: config.entry,
|
|
87
|
+
parent,
|
|
88
|
+
navigation: "router",
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const id = generatedRouteId("route", location)
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
boundary: id,
|
|
97
|
+
kind: "route",
|
|
98
|
+
entry: config.entry,
|
|
99
|
+
parent,
|
|
100
|
+
path: config.path,
|
|
101
|
+
render: config.render,
|
|
102
|
+
navigation: config.render === "static" ? "fragment" : "router",
|
|
103
|
+
hydration: config.hydration,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function generateRoutePreloaders(routeTree: readonly RouteConfig[]): string {
|
|
108
|
+
const preloaders = new Map<string, string[]>()
|
|
109
|
+
|
|
110
|
+
const visit = (
|
|
111
|
+
configs: readonly RouteConfig[],
|
|
112
|
+
layoutEntries: readonly string[],
|
|
113
|
+
) => {
|
|
114
|
+
for (const config of configs) {
|
|
115
|
+
if ("children" in config) {
|
|
116
|
+
visit(config.children, [...layoutEntries, config.entry])
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (config.render === "static") {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const imports = [...layoutEntries, browserRouteModuleId(config)]
|
|
125
|
+
const existing = preloaders.get(config.entry) ?? []
|
|
126
|
+
|
|
127
|
+
for (const entry of imports) {
|
|
128
|
+
if (!existing.includes(entry)) {
|
|
129
|
+
existing.push(entry)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
preloaders.set(config.entry, existing)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
visit(routeTree, [])
|
|
138
|
+
const entries = [...preloaders.entries()]
|
|
139
|
+
.map(([entry, imports]) => {
|
|
140
|
+
const preload = imports
|
|
141
|
+
.map((moduleId) => `import(${quote(moduleId)})`)
|
|
142
|
+
.join(", ")
|
|
143
|
+
|
|
144
|
+
return `\t${quote(entry)}: () => Promise.all([${preload}])`
|
|
145
|
+
})
|
|
146
|
+
.join(",\n")
|
|
147
|
+
|
|
148
|
+
return `const routePreloaders = {\n${entries}\n};\n\nexport function preloadRoute(entry) {\n\tconst preload = routePreloaders[entry];\n\treturn preload ? preload().then(() => undefined) : Promise.resolve();\n}\n`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function lazyRoute(
|
|
152
|
+
routeDefinition: RouteDefinition,
|
|
153
|
+
routing: NormalizedRoutingOptions,
|
|
154
|
+
metadata: GeneratedRouteMetadata,
|
|
155
|
+
): string {
|
|
156
|
+
const { entry } = routeDefinition
|
|
157
|
+
const browserLoader =
|
|
158
|
+
routeDefinition.render === "static"
|
|
159
|
+
? "loadStaticRouteFragment"
|
|
160
|
+
: "loadRouteData"
|
|
161
|
+
const browserLoaderExpression = `(args) => ${browserLoader}(args, ${JSON.stringify(routing)})`
|
|
162
|
+
|
|
163
|
+
if (routeDefinition.render === "static") {
|
|
164
|
+
const componentId = browserRouteModuleId(routeDefinition)
|
|
165
|
+
|
|
166
|
+
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: createStaticFragmentRoute({ metadata: ${JSON.stringify(metadata)}, routing: ${JSON.stringify(routing)}, fallbackComponent: componentModule.default }), loader: ${browserLoaderExpression} }; }`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (routeDefinition.render === "client") {
|
|
170
|
+
return `async () => { if (import.meta.env.SSR) return {}; const routeModule = await import(${quote(entry)}); return { Component: createRouteBoundary(routeModule.default, ${JSON.stringify(metadata)}), loader: ${browserLoaderExpression} }; }`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!generatesHydrationBoundary(routeDefinition)) {
|
|
174
|
+
return `async () => { const routeModule = await import(${quote(entry)}); return { Component: createRouteBoundary(routeModule.default, ${JSON.stringify(metadata)}), loader: import.meta.env.SSR ? routeModule.loader : ${browserLoaderExpression} }; }`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const componentId = hydrationComponentId(entry, routeDefinition.hydration)
|
|
178
|
+
|
|
179
|
+
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: createRouteBoundary(componentModule.default, ${JSON.stringify(metadata)}), loader: ${browserLoaderExpression} }; }`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function collectRouteMetadata(
|
|
183
|
+
routeTree: readonly RouteConfig[],
|
|
184
|
+
shell: string,
|
|
185
|
+
): readonly GeneratedRouteMetadata[] {
|
|
186
|
+
const rootId = "flamefront:shell:root"
|
|
187
|
+
const metadata: GeneratedRouteMetadata[] = [
|
|
188
|
+
{
|
|
189
|
+
id: rootId,
|
|
190
|
+
boundary: rootId,
|
|
191
|
+
kind: "shell",
|
|
192
|
+
entry: shell,
|
|
193
|
+
navigation: "router",
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
const visit = (
|
|
198
|
+
configs: readonly RouteConfig[],
|
|
199
|
+
parent: string,
|
|
200
|
+
locationPrefix = "",
|
|
201
|
+
) => {
|
|
202
|
+
configs.forEach((config, index) => {
|
|
203
|
+
const location = locationPrefix
|
|
204
|
+
? `${locationPrefix}.${index}`
|
|
205
|
+
: String(index)
|
|
206
|
+
const node = generatedRouteMetadata(config, location, parent)
|
|
207
|
+
|
|
208
|
+
metadata.push(node)
|
|
209
|
+
if ("children" in config) {
|
|
210
|
+
visit(config.children, node.id, location)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
visit(routeTree, rootId)
|
|
216
|
+
return metadata
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function generateConfigs(
|
|
220
|
+
configs: readonly RouteConfig[],
|
|
221
|
+
routing: NormalizedRoutingOptions,
|
|
222
|
+
depth = 1,
|
|
223
|
+
parent = "flamefront:shell:root",
|
|
224
|
+
locationPrefix = "",
|
|
225
|
+
): string {
|
|
226
|
+
const indent = "\t".repeat(depth)
|
|
227
|
+
const childIndent = "\t".repeat(depth + 1)
|
|
228
|
+
|
|
229
|
+
return configs
|
|
230
|
+
.map((config, index) => {
|
|
231
|
+
const location = locationPrefix
|
|
232
|
+
? `${locationPrefix}.${index}`
|
|
233
|
+
: String(index)
|
|
234
|
+
const metadata = generatedRouteMetadata(config, location, parent)
|
|
235
|
+
|
|
236
|
+
if ("children" in config) {
|
|
237
|
+
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}}`
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
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}}`
|
|
241
|
+
})
|
|
242
|
+
.join(",\n")
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function generateRemixRoutes(
|
|
246
|
+
app: Pick<AppDefinition, "shell" | "routeTree" | "routing">,
|
|
247
|
+
): string {
|
|
248
|
+
const rootId = "flamefront:shell:root"
|
|
249
|
+
const routeMetadata = collectRouteMetadata(app.routeTree, app.shell)
|
|
250
|
+
|
|
251
|
+
return `// Generated by Flamefront.\nimport Shell from ${quote(app.shell)};\nimport { RouterDocument } from 'flamefront/octane/router-document';\nimport { createRouteBoundary, createStaticFragmentRoute } from 'flamefront/fragment';\nimport { loadRouteData, loadStaticRouteFragment } 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)}`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Generate the server-only route-module importer used by loader endpoints. */
|
|
255
|
+
export function generateServerRoutes(
|
|
256
|
+
app: Pick<AppDefinition, "routes">,
|
|
257
|
+
): string {
|
|
258
|
+
const entries = [...new Set(app.routes.map((route) => route.entry))]
|
|
259
|
+
const imports = entries
|
|
260
|
+
.map((entry) => `\t${quote(entry)}: () => import(${quote(entry)})`)
|
|
261
|
+
.join(",\n")
|
|
262
|
+
|
|
263
|
+
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`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function hydrationStrategy(hydration: GeneratedHydration | "none"): {
|
|
267
|
+
readonly importName: string
|
|
268
|
+
readonly expression: string
|
|
269
|
+
} {
|
|
270
|
+
if (hydration === "none") {
|
|
271
|
+
return { importName: "never", expression: "never()" }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { when, ...options } = hydration
|
|
275
|
+
|
|
276
|
+
switch (when) {
|
|
277
|
+
case "idle":
|
|
278
|
+
return {
|
|
279
|
+
importName: "idle",
|
|
280
|
+
expression: `idle(${JSON.stringify(options)})`,
|
|
281
|
+
}
|
|
282
|
+
case "visible":
|
|
283
|
+
return {
|
|
284
|
+
importName: "visible",
|
|
285
|
+
expression: `visible(${JSON.stringify(options)})`,
|
|
286
|
+
}
|
|
287
|
+
case "interaction":
|
|
288
|
+
return {
|
|
289
|
+
importName: "interaction",
|
|
290
|
+
expression: `interaction(${JSON.stringify(options)})`,
|
|
291
|
+
}
|
|
292
|
+
case "media":
|
|
293
|
+
return {
|
|
294
|
+
importName: "media",
|
|
295
|
+
expression: `media(${quote(hydration.query)})`,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function generateHydrationRoute(
|
|
301
|
+
entry: string,
|
|
302
|
+
hydration: GeneratedHydration | "none",
|
|
303
|
+
): string {
|
|
304
|
+
const strategy = hydrationStrategy(hydration)
|
|
305
|
+
|
|
306
|
+
return `import { Hydrate } from 'octane';\nimport { ${strategy.importName} } from 'octane/hydration';\nimport Component from ${quote(entry)};\n\nexport default function HydrationRoute(props) @{\n\t<Hydrate when={${strategy.expression}}>\n\t\t<Component {...props} />\n\t</Hydrate>\n}\n`
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
interface TransformOptions {
|
|
310
|
+
readonly ssr?: boolean
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
interface ResolveOptions extends TransformOptions {
|
|
314
|
+
readonly scan?: boolean
|
|
315
|
+
custom?: Record<string, unknown>
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
interface PluginContext {
|
|
319
|
+
readonly environment?: { readonly config?: { readonly consumer?: string } }
|
|
320
|
+
resolve(
|
|
321
|
+
id: string,
|
|
322
|
+
importer: string | undefined,
|
|
323
|
+
options: ResolveOptions,
|
|
324
|
+
): Promise<{ readonly id: string } | null>
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface OutputAsset {
|
|
328
|
+
readonly type: "asset"
|
|
329
|
+
readonly fileName: string
|
|
330
|
+
source: string | Uint8Array
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
interface OutputChunk {
|
|
334
|
+
readonly type: "chunk"
|
|
335
|
+
map?: SourceMapLike | null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
type OutputBundle = Record<string, OutputAsset | OutputChunk>
|
|
339
|
+
|
|
340
|
+
interface SourceMapLike {
|
|
341
|
+
sources?: string[]
|
|
342
|
+
sourcesContent?: (string | null)[]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function cleanModuleId(id: string): string {
|
|
346
|
+
return id.split("?", 1)[0].replaceAll("\\", "/")
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isServerEnvironment(
|
|
350
|
+
context: PluginContext,
|
|
351
|
+
options?: TransformOptions,
|
|
352
|
+
): boolean {
|
|
353
|
+
return (
|
|
354
|
+
options?.ssr === true || context.environment?.config?.consumer === "server"
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function resolveRouteEntry(root: string, entry: string): string {
|
|
359
|
+
const relativeEntry = entry.startsWith("/") ? `.${entry}` : entry
|
|
360
|
+
|
|
361
|
+
return cleanModuleId(path.resolve(root, relativeEntry))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function routeSourceSuffix(entry: string): string {
|
|
365
|
+
return cleanModuleId(entry).replace(/^\.?(?:\/|$)/, "")
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function omitRouteSourceContent(
|
|
369
|
+
bundle: OutputBundle,
|
|
370
|
+
routes: readonly Pick<RouteDefinition, "entry">[],
|
|
371
|
+
): void {
|
|
372
|
+
const routeSuffixes = new Set(
|
|
373
|
+
routes.map((route) => routeSourceSuffix(route.entry)),
|
|
374
|
+
)
|
|
375
|
+
const omitFromSourceMap = (sourceMap: SourceMapLike): boolean => {
|
|
376
|
+
if (!sourceMap.sources || !sourceMap.sourcesContent) {
|
|
377
|
+
return false
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let changed = false
|
|
381
|
+
|
|
382
|
+
for (let index = 0; index < sourceMap.sources.length; index += 1) {
|
|
383
|
+
const source = cleanModuleId(sourceMap.sources[index]).replace(
|
|
384
|
+
/^(?:\.\.\/)+/,
|
|
385
|
+
"",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if (!routeSuffixes.has(source)) {
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (sourceMap.sourcesContent[index] === null) {
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
sourceMap.sourcesContent[index] = null
|
|
397
|
+
changed = true
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return changed
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const output of Object.values(bundle)) {
|
|
404
|
+
if (output.type === "chunk") {
|
|
405
|
+
if (output.map) {
|
|
406
|
+
omitFromSourceMap(output.map)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (output.type !== "asset" || !output.fileName.endsWith(".map")) {
|
|
413
|
+
continue
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const serializedSourceMap =
|
|
417
|
+
typeof output.source === "string"
|
|
418
|
+
? output.source
|
|
419
|
+
: new TextDecoder().decode(output.source)
|
|
420
|
+
const sourceMap = JSON.parse(serializedSourceMap) as SourceMapLike
|
|
421
|
+
|
|
422
|
+
if (omitFromSourceMap(sourceMap)) {
|
|
423
|
+
output.source = JSON.stringify(sourceMap)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function omitRouteSourceContentFromDirectory(
|
|
429
|
+
directory: string,
|
|
430
|
+
routes: readonly Pick<RouteDefinition, "entry">[],
|
|
431
|
+
): void {
|
|
432
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
433
|
+
const entryPath = path.join(directory, entry.name)
|
|
434
|
+
|
|
435
|
+
if (entry.isDirectory()) {
|
|
436
|
+
omitRouteSourceContentFromDirectory(entryPath, routes)
|
|
437
|
+
continue
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!entry.name.endsWith(".map")) {
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const serializedSourceMap = fs.readFileSync(entryPath, "utf8")
|
|
445
|
+
const asset: OutputAsset = {
|
|
446
|
+
type: "asset",
|
|
447
|
+
fileName: entry.name,
|
|
448
|
+
source: serializedSourceMap,
|
|
449
|
+
}
|
|
450
|
+
const bundle = { [entry.name]: asset }
|
|
451
|
+
|
|
452
|
+
omitRouteSourceContent(bundle, routes)
|
|
453
|
+
if (
|
|
454
|
+
typeof asset.source === "string" &&
|
|
455
|
+
asset.source !== serializedSourceMap
|
|
456
|
+
) {
|
|
457
|
+
fs.writeFileSync(entryPath, asset.source)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function removeServerRouteExports(source: string, id = "route.js") {
|
|
463
|
+
const ast = parse(source, { sourceType: "module" })
|
|
464
|
+
|
|
465
|
+
if (!removeExports(ast, SERVER_ONLY_ROUTE_EXPORTS)) {
|
|
466
|
+
return null
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return generate(ast, {
|
|
470
|
+
sourceMaps: true,
|
|
471
|
+
filename: id,
|
|
472
|
+
sourceFileName: cleanModuleId(id),
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function flamefront(options: FlamefrontOptions = {}) {
|
|
477
|
+
let root = process.cwd()
|
|
478
|
+
let serverBuild = false
|
|
479
|
+
let appPromise: Promise<AppDefinition> | undefined
|
|
480
|
+
let manifestRevision = 0
|
|
481
|
+
const manifestId = options.routes ?? "/src/app.ts"
|
|
482
|
+
const manifestPath = () =>
|
|
483
|
+
path.resolve(
|
|
484
|
+
root,
|
|
485
|
+
manifestId.startsWith("/") ? `.${manifestId}` : manifestId,
|
|
486
|
+
)
|
|
487
|
+
const loadApp = async () => {
|
|
488
|
+
const manifestUrl = new URL(pathToFileURL(manifestPath()))
|
|
489
|
+
|
|
490
|
+
manifestUrl.searchParams.set("flamefront", String(manifestRevision))
|
|
491
|
+
appPromise ??= import(manifestUrl.href).then((module) => {
|
|
492
|
+
const app = module.app ?? module.default
|
|
493
|
+
|
|
494
|
+
if (!app?.routeTree || typeof app.shell !== "string") {
|
|
495
|
+
throw new TypeError(
|
|
496
|
+
`Flamefront route manifest ${manifestId} must export an app with a shell.`,
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return app as AppDefinition
|
|
501
|
+
})
|
|
502
|
+
return appPromise
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const loadRoutes = async () => (await loadApp()).routes
|
|
506
|
+
const loadRouteModuleIds = async () =>
|
|
507
|
+
new Set(
|
|
508
|
+
(await loadRoutes()).map((route) => resolveRouteEntry(root, route.entry)),
|
|
509
|
+
)
|
|
510
|
+
const configureRoot = (config: { readonly root: string }) => {
|
|
511
|
+
root = config.root
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const frameworkModulesPlugin = {
|
|
515
|
+
name: "flamefront:framework-modules",
|
|
516
|
+
enforce: "pre" as const,
|
|
517
|
+
configResolved: configureRoot,
|
|
518
|
+
handleHotUpdate(context: {
|
|
519
|
+
file: string
|
|
520
|
+
server: {
|
|
521
|
+
moduleGraph: {
|
|
522
|
+
getModuleById(id: string): unknown
|
|
523
|
+
invalidateModule(module: unknown): void
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}) {
|
|
527
|
+
if (context.file !== manifestPath()) {
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
manifestRevision += 1
|
|
532
|
+
appPromise = undefined
|
|
533
|
+
for (const moduleId of [resolvedRemixRoutesId, resolvedServerRoutesId]) {
|
|
534
|
+
const generatedModule =
|
|
535
|
+
context.server.moduleGraph.getModuleById(moduleId)
|
|
536
|
+
|
|
537
|
+
if (generatedModule) {
|
|
538
|
+
context.server.moduleGraph.invalidateModule(generatedModule)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
async resolveId(
|
|
543
|
+
this: PluginContext,
|
|
544
|
+
id: string,
|
|
545
|
+
importer?: string,
|
|
546
|
+
resolveOptions: ResolveOptions = {},
|
|
547
|
+
) {
|
|
548
|
+
if (id === remixRoutesId) {
|
|
549
|
+
return resolvedRemixRoutesId
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (id === serverRoutesId) {
|
|
553
|
+
return resolvedServerRoutesId
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (
|
|
557
|
+
id === "./hydration-route.tsrx?octane-hydrate=0" &&
|
|
558
|
+
importer?.startsWith(resolvedHydrationRoutePrefix)
|
|
559
|
+
) {
|
|
560
|
+
const parameters = new URLSearchParams(
|
|
561
|
+
importer.slice(importer.indexOf("?") + 1),
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
parameters.set("octane-hydrate", "0")
|
|
565
|
+
return `${hydrationRouteId}?${parameters}`
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (id.startsWith(resolvedHydrationRoutePrefix)) {
|
|
569
|
+
return id
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (
|
|
573
|
+
resolveOptions.scan ||
|
|
574
|
+
isServerEnvironment(this, resolveOptions) ||
|
|
575
|
+
resolveOptions.custom?.["flamefront:server-module"]
|
|
576
|
+
) {
|
|
577
|
+
return null
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const nestedOptions: ResolveOptions = {
|
|
581
|
+
...resolveOptions,
|
|
582
|
+
custom: { ...resolveOptions.custom, "flamefront:server-module": true },
|
|
583
|
+
}
|
|
584
|
+
const resolved = await this.resolve(id, importer, nestedOptions)
|
|
585
|
+
|
|
586
|
+
if (!resolved) {
|
|
587
|
+
return null
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const resolvedId = cleanModuleId(resolved.id)
|
|
591
|
+
|
|
592
|
+
if (
|
|
593
|
+
!serverFilePattern.test(resolvedId) &&
|
|
594
|
+
!serverDirectoryPattern.test(resolvedId)
|
|
595
|
+
) {
|
|
596
|
+
return null
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (!importer || importer.endsWith(".html")) {
|
|
600
|
+
return null
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const importerId = cleanModuleId(importer)
|
|
604
|
+
const importerLabel = path.relative(root, importerId) || importerId
|
|
605
|
+
const routeHint = (await loadRouteModuleIds()).has(importerId)
|
|
606
|
+
? " Flamefront removes server imports used exclusively by `loader`, but this import is still referenced by client code."
|
|
607
|
+
: ""
|
|
608
|
+
|
|
609
|
+
throw new Error(
|
|
610
|
+
`Server-only module ${JSON.stringify(id)} was referenced by client module ${JSON.stringify(importerLabel)}.${routeHint}`,
|
|
611
|
+
)
|
|
612
|
+
},
|
|
613
|
+
async load(id: string) {
|
|
614
|
+
if (id === resolvedRemixRoutesId) {
|
|
615
|
+
return generateRemixRoutes(await loadApp())
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (id === resolvedServerRoutesId) {
|
|
619
|
+
return generateServerRoutes(await loadApp())
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (id.startsWith(resolvedHydrationRoutePrefix)) {
|
|
623
|
+
const parameters = new URLSearchParams(id.slice(id.indexOf("?") + 1))
|
|
624
|
+
const entry = parameters.get("entry")
|
|
625
|
+
const serializedHydration = parameters.get("hydration")
|
|
626
|
+
|
|
627
|
+
if (!entry || !serializedHydration) {
|
|
628
|
+
throw new TypeError(
|
|
629
|
+
"Flamefront hydration route is missing its configuration.",
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return generateHydrationRoute(
|
|
634
|
+
entry,
|
|
635
|
+
JSON.parse(serializedHydration) as GeneratedHydration | "none",
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return null
|
|
640
|
+
},
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const routeModulePlugin = {
|
|
644
|
+
name: "flamefront:route-modules",
|
|
645
|
+
enforce: "post" as const,
|
|
646
|
+
configResolved(config: {
|
|
647
|
+
readonly root: string
|
|
648
|
+
readonly build?: { readonly ssr?: unknown }
|
|
649
|
+
}) {
|
|
650
|
+
configureRoot(config)
|
|
651
|
+
serverBuild = Boolean(config.build?.ssr)
|
|
652
|
+
},
|
|
653
|
+
async generateBundle(_outputOptions: unknown, bundle: OutputBundle) {
|
|
654
|
+
if (!serverBuild) {
|
|
655
|
+
omitRouteSourceContent(bundle, await loadRoutes())
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
async writeBundle(outputOptions: { readonly dir?: string }) {
|
|
659
|
+
// Rollup serializes chunk maps after generateBundle, and other plugins can
|
|
660
|
+
// emit late client chunks. Scrub the completed output as the final guard.
|
|
661
|
+
if (!serverBuild && outputOptions.dir) {
|
|
662
|
+
omitRouteSourceContentFromDirectory(
|
|
663
|
+
outputOptions.dir,
|
|
664
|
+
await loadRoutes(),
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
async transform(
|
|
669
|
+
this: PluginContext,
|
|
670
|
+
source: string,
|
|
671
|
+
id: string,
|
|
672
|
+
transformOptions?: TransformOptions,
|
|
673
|
+
) {
|
|
674
|
+
if (isServerEnvironment(this, transformOptions)) {
|
|
675
|
+
return null
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!(await loadRouteModuleIds()).has(cleanModuleId(id))) {
|
|
679
|
+
return null
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const transformed = removeServerRouteExports(source, id)
|
|
683
|
+
|
|
684
|
+
if (!transformed) {
|
|
685
|
+
return null
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return { code: transformed.code, map: transformed.map }
|
|
689
|
+
},
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return [frameworkModulesPlugin, routeModulePlugin] as const
|
|
693
|
+
}
|
package/readme.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Coming soon...
|