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/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...