flamefront 0.0.0 → 0.1.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 +58 -0
- package/bin/ff-loader.mjs +18 -0
- package/bin/ff.js +6 -0
- package/package.json +84 -8
- package/src/babel.ts +22 -0
- package/src/cli.ts +98 -0
- package/src/entry.ts +49 -0
- package/src/fetch.ts +280 -0
- package/src/fragment-client.ts +271 -0
- package/src/fragment-hydration-client.tsx +81 -0
- package/src/fragment-protocol.ts +39 -0
- package/src/fragment.tsx +656 -0
- package/src/glob.ts +294 -0
- package/src/identifier-prefix.ts +5 -0
- package/src/index.ts +1168 -0
- package/src/lifecycle.ts +487 -0
- package/src/octane-client-core.ts +141 -0
- package/src/octane-client.ts +48 -0
- package/src/octane-compiler.d.ts +19 -0
- package/src/octane-default-renderer.tsx +124 -0
- package/src/octane-router-document.ts +22 -0
- package/src/octane.tsx +661 -0
- package/src/output.ts +48 -0
- package/src/remix-route-data.ts +76 -0
- package/src/remix-router-core.ts +106 -0
- package/src/remix-router.ts +117 -0
- package/src/remove-exports.ts +148 -0
- package/src/route-data-client.ts +234 -0
- package/src/route-prefetch.ts +79 -0
- package/src/server.ts +338 -0
- package/src/srvx.ts +174 -0
- package/src/static-fragment-artifacts.ts +86 -0
- package/src/typegen.ts +207 -0
- package/src/virtual-remix-routes.d.ts +35 -0
- package/src/vite.ts +1030 -0
- package/readme.md +0 -1
package/src/vite.ts
ADDED
|
@@ -0,0 +1,1030 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { pathToFileURL } from "node:url"
|
|
4
|
+
import { compile as compileOctane } from "octane/compiler"
|
|
5
|
+
import type { Features, HastPluginList, MdastPluginList } from "satteri"
|
|
6
|
+
import vitePluginSatteri, { type MdxOptions } from "vite-plugin-satteri"
|
|
7
|
+
import type {
|
|
8
|
+
AppDefinition,
|
|
9
|
+
GeneratedHydration,
|
|
10
|
+
GeneratedRouteMetadata,
|
|
11
|
+
NormalizedRoutingOptions,
|
|
12
|
+
RouteConfig,
|
|
13
|
+
RouteDefinition,
|
|
14
|
+
} from "./index.ts"
|
|
15
|
+
import { generate, parse, traverse, type Babel } from "./babel.ts"
|
|
16
|
+
import { expandGlob, globDirectory, setGlobRoot } from "./glob.ts"
|
|
17
|
+
import { removeExports } from "./remove-exports.ts"
|
|
18
|
+
import { writeRouteImportMap } from "./typegen.ts"
|
|
19
|
+
import {
|
|
20
|
+
resolveFlamefrontOutput,
|
|
21
|
+
type FlamefrontOutputOptions,
|
|
22
|
+
type ResolvedFlamefrontOutput,
|
|
23
|
+
} from "./output.ts"
|
|
24
|
+
|
|
25
|
+
export type {
|
|
26
|
+
FlamefrontAdapter,
|
|
27
|
+
FlamefrontOutputOptions,
|
|
28
|
+
FlamefrontTarget,
|
|
29
|
+
ResolvedFlamefrontOutput,
|
|
30
|
+
} from "./output.ts"
|
|
31
|
+
|
|
32
|
+
export const remixRoutesId = "virtual:flamefront/remix-routes"
|
|
33
|
+
const resolvedRemixRoutesId = `\0${remixRoutesId}`
|
|
34
|
+
|
|
35
|
+
export const serverRoutesId = "virtual:flamefront/server-routes"
|
|
36
|
+
const resolvedServerRoutesId = `\0${serverRoutesId}`
|
|
37
|
+
|
|
38
|
+
export const serverEntryId = "virtual:flamefront/server-entry"
|
|
39
|
+
const resolvedServerEntryId = `\0${serverEntryId}`
|
|
40
|
+
const hydrationRouteId = "/@flamefront/hydration-route.tsrx"
|
|
41
|
+
const resolvedHydrationRoutePrefix = `${hydrationRouteId}?`
|
|
42
|
+
const markdownRouteId = "/@flamefront/markdown-route.tsrx"
|
|
43
|
+
const resolvedMarkdownRoutePrefix = `${markdownRouteId}?`
|
|
44
|
+
const SERVER_ONLY_ROUTE_EXPORTS = ["loader"] as const
|
|
45
|
+
const serverFilePattern = /\.server(?:\.[cm]?[jt]sx?|\.tsrx)$/
|
|
46
|
+
const serverDirectoryPattern = /\/\.server\//
|
|
47
|
+
|
|
48
|
+
export interface FlamefrontOptions extends FlamefrontOutputOptions {
|
|
49
|
+
/** Project-root route manifest module. */
|
|
50
|
+
readonly routes?: string
|
|
51
|
+
/** Built-in Markdown and MDX compiler configuration. */
|
|
52
|
+
readonly markdown?: false | MarkdownOptions
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MarkdownOptions {
|
|
56
|
+
/** Parser feature toggles. GFM and frontmatter are enabled by default. */
|
|
57
|
+
readonly features?: Features
|
|
58
|
+
/** MDAST plugins shared by Markdown and MDX entries. */
|
|
59
|
+
readonly mdastPlugins?: MdastPluginList
|
|
60
|
+
/** HAST plugins shared by Markdown and MDX entries. */
|
|
61
|
+
readonly hastPlugins?: HastPluginList
|
|
62
|
+
/** MDX compiler options; its JSX import source is always `octane`. */
|
|
63
|
+
readonly mdx?:
|
|
64
|
+
boolean | Omit<MdxOptions, "jsxImportSource" | "jsx" | "jsxRuntime">
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function quote(value: string): string {
|
|
68
|
+
return JSON.stringify(value)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function lazyLayout(entry: string, metadata: GeneratedRouteMetadata): string {
|
|
72
|
+
return `async () => { const routeModule = await import(${quote(entry)}); return { Component: createRouteBoundary(routeModule.default, ${JSON.stringify(metadata)}) }; }`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function generatesHydrationBoundary(
|
|
76
|
+
routeDefinition: RouteDefinition,
|
|
77
|
+
): routeDefinition is RouteDefinition & {
|
|
78
|
+
hydration: GeneratedHydration | "none"
|
|
79
|
+
} {
|
|
80
|
+
return (
|
|
81
|
+
(routeDefinition.render === "server" ||
|
|
82
|
+
routeDefinition.render === "static") &&
|
|
83
|
+
(routeDefinition.hydration === "none" ||
|
|
84
|
+
typeof routeDefinition.hydration === "object")
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hydrationComponentId(
|
|
89
|
+
entry: string,
|
|
90
|
+
hydration: GeneratedHydration | "none",
|
|
91
|
+
): string {
|
|
92
|
+
const parameters = new URLSearchParams({
|
|
93
|
+
entry,
|
|
94
|
+
hydration: JSON.stringify(hydration),
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return `${hydrationRouteId}?${parameters}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function markdownComponentId(entry: string): string {
|
|
101
|
+
const parameters = new URLSearchParams({
|
|
102
|
+
entry,
|
|
103
|
+
"flamefront-markdown": "1",
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return `${markdownRouteId}?${parameters}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function browserRouteModuleId(routeDefinition: RouteDefinition): string {
|
|
110
|
+
const componentEntry =
|
|
111
|
+
routeDefinition.content === "markdown"
|
|
112
|
+
? markdownComponentId(routeDefinition.entry)
|
|
113
|
+
: routeDefinition.entry
|
|
114
|
+
|
|
115
|
+
return generatesHydrationBoundary(routeDefinition)
|
|
116
|
+
? hydrationComponentId(componentEntry, routeDefinition.hydration)
|
|
117
|
+
: componentEntry
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function generatedRouteId(kind: "layout" | "route", location: string): string {
|
|
121
|
+
return `flamefront:${kind}:${location}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function generatedRouteMetadata(
|
|
125
|
+
config: RouteConfig,
|
|
126
|
+
location: string,
|
|
127
|
+
parent: string,
|
|
128
|
+
): GeneratedRouteMetadata {
|
|
129
|
+
if ("children" in config) {
|
|
130
|
+
const id = generatedRouteId("layout", location)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id,
|
|
134
|
+
boundary: id,
|
|
135
|
+
kind: "layout",
|
|
136
|
+
entry: config.entry,
|
|
137
|
+
parent,
|
|
138
|
+
navigation: "router",
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const id = generatedRouteId("route", location)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
boundary: id,
|
|
147
|
+
kind: "route",
|
|
148
|
+
entry: config.entry,
|
|
149
|
+
parent,
|
|
150
|
+
path: config.path,
|
|
151
|
+
render: config.render,
|
|
152
|
+
navigation: config.render === "client" ? "router" : "fragment",
|
|
153
|
+
hydration: config.hydration,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function generateRoutePreloaders(routeTree: readonly RouteConfig[]): string {
|
|
158
|
+
const preloaders = new Map<string, string[]>()
|
|
159
|
+
|
|
160
|
+
const visit = (
|
|
161
|
+
configs: readonly RouteConfig[],
|
|
162
|
+
layoutEntries: readonly string[],
|
|
163
|
+
) => {
|
|
164
|
+
for (const config of configs) {
|
|
165
|
+
if ("children" in config) {
|
|
166
|
+
visit(config.children, [...layoutEntries, config.entry])
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (config.render !== "client") {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const imports = [...layoutEntries, browserRouteModuleId(config)]
|
|
175
|
+
const existing = preloaders.get(config.entry) ?? []
|
|
176
|
+
|
|
177
|
+
for (const entry of imports) {
|
|
178
|
+
if (!existing.includes(entry)) {
|
|
179
|
+
existing.push(entry)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
preloaders.set(config.entry, existing)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
visit(routeTree, [])
|
|
188
|
+
const entries = [...preloaders.entries()]
|
|
189
|
+
.map(([entry, imports]) => {
|
|
190
|
+
const preload = imports
|
|
191
|
+
.map((moduleId) => `import(${quote(moduleId)})`)
|
|
192
|
+
.join(", ")
|
|
193
|
+
|
|
194
|
+
return `\t${quote(entry)}: () => Promise.all([${preload}])`
|
|
195
|
+
})
|
|
196
|
+
.join(",\n")
|
|
197
|
+
|
|
198
|
+
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`
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function lazyRoute(
|
|
202
|
+
routeDefinition: RouteDefinition,
|
|
203
|
+
routing: NormalizedRoutingOptions,
|
|
204
|
+
metadata: GeneratedRouteMetadata,
|
|
205
|
+
): string {
|
|
206
|
+
const { entry } = routeDefinition
|
|
207
|
+
const browserLoader =
|
|
208
|
+
routeDefinition.render === "client" ? "loadRouteData" : "loadRouteFragment"
|
|
209
|
+
const browserLoaderExpression =
|
|
210
|
+
routeDefinition.render === "client"
|
|
211
|
+
? `(args) => ${browserLoader}(args, ${JSON.stringify(routing)})`
|
|
212
|
+
: `(args) => ${browserLoader}(args, ${JSON.stringify(routing)}, ${quote(routeDefinition.render)})`
|
|
213
|
+
|
|
214
|
+
if (routeDefinition.render !== "client") {
|
|
215
|
+
const componentId = browserRouteModuleId(routeDefinition)
|
|
216
|
+
|
|
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} }; }`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (routeDefinition.render === "client") {
|
|
221
|
+
if (routeDefinition.content === "markdown") {
|
|
222
|
+
const componentId = browserRouteModuleId(routeDefinition)
|
|
223
|
+
|
|
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} }; }`
|
|
225
|
+
}
|
|
226
|
+
|
|
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} }; }`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
throw new Error(`Unsupported route render mode ${routeDefinition.render}.`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function collectRouteMetadata(
|
|
234
|
+
routeTree: readonly RouteConfig[],
|
|
235
|
+
shell: string,
|
|
236
|
+
shellHydration: AppDefinition["shellHydration"],
|
|
237
|
+
): readonly GeneratedRouteMetadata[] {
|
|
238
|
+
const rootId = "flamefront:shell:root"
|
|
239
|
+
const metadata: GeneratedRouteMetadata[] = [
|
|
240
|
+
{
|
|
241
|
+
id: rootId,
|
|
242
|
+
boundary: rootId,
|
|
243
|
+
kind: "shell",
|
|
244
|
+
entry: shell,
|
|
245
|
+
navigation: "router",
|
|
246
|
+
hydration: shellHydration,
|
|
247
|
+
},
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
const visit = (
|
|
251
|
+
configs: readonly RouteConfig[],
|
|
252
|
+
parent: string,
|
|
253
|
+
locationPrefix = "",
|
|
254
|
+
) => {
|
|
255
|
+
configs.forEach((config, index) => {
|
|
256
|
+
const location = locationPrefix
|
|
257
|
+
? `${locationPrefix}.${index}`
|
|
258
|
+
: String(index)
|
|
259
|
+
const node = generatedRouteMetadata(config, location, parent)
|
|
260
|
+
|
|
261
|
+
metadata.push(node)
|
|
262
|
+
if ("children" in config) {
|
|
263
|
+
visit(config.children, node.id, location)
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
visit(routeTree, rootId)
|
|
269
|
+
return metadata
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function generateConfigs(
|
|
273
|
+
configs: readonly RouteConfig[],
|
|
274
|
+
routing: NormalizedRoutingOptions,
|
|
275
|
+
depth = 1,
|
|
276
|
+
parent = "flamefront:shell:root",
|
|
277
|
+
locationPrefix = "",
|
|
278
|
+
): string {
|
|
279
|
+
const indent = "\t".repeat(depth)
|
|
280
|
+
const childIndent = "\t".repeat(depth + 1)
|
|
281
|
+
|
|
282
|
+
return configs
|
|
283
|
+
.map((config, index) => {
|
|
284
|
+
const location = locationPrefix
|
|
285
|
+
? `${locationPrefix}.${index}`
|
|
286
|
+
: String(index)
|
|
287
|
+
const metadata = generatedRouteMetadata(config, location, parent)
|
|
288
|
+
|
|
289
|
+
if ("children" in config) {
|
|
290
|
+
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
|
+
}
|
|
292
|
+
|
|
293
|
+
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
|
+
})
|
|
295
|
+
.join(",\n")
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function generateRemixRoutes(
|
|
299
|
+
app: Pick<
|
|
300
|
+
AppDefinition,
|
|
301
|
+
"shell" | "shellHydration" | "routeTree" | "routing"
|
|
302
|
+
>,
|
|
303
|
+
): string {
|
|
304
|
+
const rootId = "flamefront:shell:root"
|
|
305
|
+
const routeMetadata = collectRouteMetadata(
|
|
306
|
+
app.routeTree,
|
|
307
|
+
app.shell,
|
|
308
|
+
app.shellHydration,
|
|
309
|
+
)
|
|
310
|
+
|
|
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)}`
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Generate the server-only route-module importer used by loader endpoints. */
|
|
315
|
+
export function generateServerRoutes(
|
|
316
|
+
app: Pick<AppDefinition, "routes">,
|
|
317
|
+
): string {
|
|
318
|
+
const entries = [...new Set(app.routes.map((route) => route.entry))]
|
|
319
|
+
const imports = entries
|
|
320
|
+
.map((entry) => `\t${quote(entry)}: () => import(${quote(entry)})`)
|
|
321
|
+
.join(",\n")
|
|
322
|
+
|
|
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`
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Generate the server-entry import selected by `flamefront({...})`. */
|
|
327
|
+
export function generateServerEntry(output: ResolvedFlamefrontOutput): string {
|
|
328
|
+
if (output.adapter === "srvx") {
|
|
329
|
+
return `// Generated by Flamefront.\nexport { createSrvxServerEntry as createServerEntry } from "flamefront/srvx";\n`
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return `// Generated by Flamefront.\nexport { createFetchServerEntry as createServerEntry } from "flamefront/fetch";\n`
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function hydrationStrategy(hydration: GeneratedHydration | "none"): {
|
|
336
|
+
readonly importName: string
|
|
337
|
+
readonly expression: string
|
|
338
|
+
} {
|
|
339
|
+
if (hydration === "none") {
|
|
340
|
+
return { importName: "never", expression: "never()" }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const { when, ...options } = hydration
|
|
344
|
+
|
|
345
|
+
switch (when) {
|
|
346
|
+
case "idle":
|
|
347
|
+
return {
|
|
348
|
+
importName: "idle",
|
|
349
|
+
expression: `idle(${JSON.stringify(options)})`,
|
|
350
|
+
}
|
|
351
|
+
case "visible":
|
|
352
|
+
return {
|
|
353
|
+
importName: "visible",
|
|
354
|
+
expression: `visible(${JSON.stringify(options)})`,
|
|
355
|
+
}
|
|
356
|
+
case "interaction":
|
|
357
|
+
return {
|
|
358
|
+
importName: "interaction",
|
|
359
|
+
expression: `interaction(${JSON.stringify(options)})`,
|
|
360
|
+
}
|
|
361
|
+
case "media":
|
|
362
|
+
return {
|
|
363
|
+
importName: "media",
|
|
364
|
+
expression: `media(${quote(hydration.query)})`,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Generate the component adapter used when a route entry exports HTML. */
|
|
370
|
+
export function generateMarkdownRoute(entry: string): string {
|
|
371
|
+
return `import html from ${quote(entry)};\n\nexport default function MarkdownRoute() @{\n\t<div dangerouslySetInnerHTML={{ __html: html }} />\n}\n`
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function generateHydrationRoute(
|
|
375
|
+
entry: string,
|
|
376
|
+
hydration: GeneratedHydration | "none",
|
|
377
|
+
): string {
|
|
378
|
+
const strategy = hydrationStrategy(hydration)
|
|
379
|
+
|
|
380
|
+
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`
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
interface TransformOptions {
|
|
384
|
+
readonly ssr?: boolean
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
interface ResolveOptions extends TransformOptions {
|
|
388
|
+
readonly scan?: boolean
|
|
389
|
+
custom?: Record<string, unknown>
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface PluginContext {
|
|
393
|
+
readonly environment?: {
|
|
394
|
+
readonly config?: { readonly command?: string; readonly consumer?: string }
|
|
395
|
+
}
|
|
396
|
+
resolve(
|
|
397
|
+
id: string,
|
|
398
|
+
importer: string | undefined,
|
|
399
|
+
options: ResolveOptions,
|
|
400
|
+
): Promise<{ readonly id: string } | null>
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
interface OutputAsset {
|
|
404
|
+
readonly type: "asset"
|
|
405
|
+
readonly fileName: string
|
|
406
|
+
source: string | Uint8Array
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
interface OutputChunk {
|
|
410
|
+
readonly type: "chunk"
|
|
411
|
+
map?: SourceMapLike | null
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
type OutputBundle = Record<string, OutputAsset | OutputChunk>
|
|
415
|
+
|
|
416
|
+
interface SourceMapLike {
|
|
417
|
+
sources?: string[]
|
|
418
|
+
sourcesContent?: (string | null)[]
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function cleanModuleId(id: string): string {
|
|
422
|
+
return id.split("?", 1)[0].replaceAll("\\", "/")
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface ManifestGlobTransform {
|
|
426
|
+
readonly code: string
|
|
427
|
+
readonly map: null
|
|
428
|
+
readonly directories: readonly string[]
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
interface TextReplacement {
|
|
432
|
+
readonly end: number
|
|
433
|
+
readonly start: number
|
|
434
|
+
readonly text: string
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function staticStringArgument(
|
|
438
|
+
argument: Babel.Node | undefined,
|
|
439
|
+
): string | undefined {
|
|
440
|
+
if (!argument) {
|
|
441
|
+
return undefined
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (argument.type === "StringLiteral") {
|
|
445
|
+
return argument.value
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (
|
|
449
|
+
argument.type === "TemplateLiteral" &&
|
|
450
|
+
argument.expressions.length === 0
|
|
451
|
+
) {
|
|
452
|
+
return argument.quasis[0]?.value.cooked ?? ""
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return undefined
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function transformManifestGlobs(
|
|
459
|
+
source: string,
|
|
460
|
+
id: string,
|
|
461
|
+
root: string,
|
|
462
|
+
): ManifestGlobTransform | null {
|
|
463
|
+
const ast = parse(source, {
|
|
464
|
+
sourceFilename: id,
|
|
465
|
+
sourceType: "module",
|
|
466
|
+
plugins: ["typescript", "jsx"],
|
|
467
|
+
})
|
|
468
|
+
const globBindings = new Set<string>()
|
|
469
|
+
|
|
470
|
+
traverse(ast, {
|
|
471
|
+
ImportDeclaration(path) {
|
|
472
|
+
if (path.node.source.value !== "flamefront") {
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
for (const specifier of path.node.specifiers) {
|
|
477
|
+
if (specifier.type !== "ImportSpecifier") {
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const imported = specifier.imported
|
|
482
|
+
const importedName =
|
|
483
|
+
imported.type === "Identifier" ? imported.name : imported.value
|
|
484
|
+
|
|
485
|
+
if (importedName === "glob") {
|
|
486
|
+
globBindings.add(specifier.local.name)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
if (globBindings.size === 0) {
|
|
493
|
+
return null
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const replacements: TextReplacement[] = []
|
|
497
|
+
const directories = new Set<string>()
|
|
498
|
+
|
|
499
|
+
traverse(ast, {
|
|
500
|
+
CallExpression(path) {
|
|
501
|
+
const callee = path.node.callee
|
|
502
|
+
|
|
503
|
+
if (callee.type !== "Identifier" || !globBindings.has(callee.name)) {
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const argument = path.node.arguments[0]
|
|
508
|
+
const pattern = staticStringArgument(argument)
|
|
509
|
+
|
|
510
|
+
if (pattern === undefined) {
|
|
511
|
+
throw new TypeError(
|
|
512
|
+
`flamefront glob() in ${id} requires a string-literal pattern.`,
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (
|
|
517
|
+
typeof argument?.start !== "number" ||
|
|
518
|
+
typeof argument.end !== "number"
|
|
519
|
+
) {
|
|
520
|
+
throw new TypeError(`flamefront glob() in ${id} has no source range.`)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const files = expandGlob(root, pattern)
|
|
524
|
+
|
|
525
|
+
replacements.push({
|
|
526
|
+
start: argument.start,
|
|
527
|
+
end: argument.end,
|
|
528
|
+
text: JSON.stringify(files),
|
|
529
|
+
})
|
|
530
|
+
directories.add(globDirectory(root, pattern))
|
|
531
|
+
},
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
if (replacements.length === 0) {
|
|
535
|
+
return null
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const code = [...replacements]
|
|
539
|
+
.sort((left, right) => right.start - left.start)
|
|
540
|
+
.reduce(
|
|
541
|
+
(result, replacement) =>
|
|
542
|
+
`${result.slice(0, replacement.start)}${replacement.text}${result.slice(replacement.end)}`,
|
|
543
|
+
source,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return { code, map: null, directories: [...directories] }
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isPathWithinDirectory(directory: string, candidate: string): boolean {
|
|
550
|
+
const relative = path.relative(directory, candidate)
|
|
551
|
+
|
|
552
|
+
return (
|
|
553
|
+
relative === "" ||
|
|
554
|
+
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function isServerEnvironment(
|
|
559
|
+
context: PluginContext,
|
|
560
|
+
options?: TransformOptions,
|
|
561
|
+
): boolean {
|
|
562
|
+
return (
|
|
563
|
+
options?.ssr === true || context.environment?.config?.consumer === "server"
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function resolveRouteEntry(root: string, entry: string): string {
|
|
568
|
+
const relativeEntry = entry.startsWith("/") ? `.${entry}` : entry
|
|
569
|
+
|
|
570
|
+
return cleanModuleId(path.resolve(root, relativeEntry))
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function routeSourceSuffix(entry: string): string {
|
|
574
|
+
return cleanModuleId(entry).replace(/^\.?(?:\/|$)/, "")
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function omitRouteSourceContent(
|
|
578
|
+
bundle: OutputBundle,
|
|
579
|
+
routes: readonly Pick<RouteDefinition, "entry">[],
|
|
580
|
+
): void {
|
|
581
|
+
const routeSuffixes = new Set(
|
|
582
|
+
routes.map((route) => routeSourceSuffix(route.entry)),
|
|
583
|
+
)
|
|
584
|
+
const omitFromSourceMap = (sourceMap: SourceMapLike): boolean => {
|
|
585
|
+
if (!sourceMap.sources || !sourceMap.sourcesContent) {
|
|
586
|
+
return false
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let changed = false
|
|
590
|
+
|
|
591
|
+
for (let index = 0; index < sourceMap.sources.length; index += 1) {
|
|
592
|
+
const source = cleanModuleId(sourceMap.sources[index]).replace(
|
|
593
|
+
/^(?:\.\.\/)+/,
|
|
594
|
+
"",
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
if (!routeSuffixes.has(source)) {
|
|
598
|
+
continue
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (sourceMap.sourcesContent[index] === null) {
|
|
602
|
+
continue
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
sourceMap.sourcesContent[index] = null
|
|
606
|
+
changed = true
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return changed
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
for (const output of Object.values(bundle)) {
|
|
613
|
+
if (output.type === "chunk") {
|
|
614
|
+
if (output.map) {
|
|
615
|
+
omitFromSourceMap(output.map)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
continue
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (output.type !== "asset" || !output.fileName.endsWith(".map")) {
|
|
622
|
+
continue
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const serializedSourceMap =
|
|
626
|
+
typeof output.source === "string"
|
|
627
|
+
? output.source
|
|
628
|
+
: new TextDecoder().decode(output.source)
|
|
629
|
+
const sourceMap = JSON.parse(serializedSourceMap) as SourceMapLike
|
|
630
|
+
|
|
631
|
+
if (omitFromSourceMap(sourceMap)) {
|
|
632
|
+
output.source = JSON.stringify(sourceMap)
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function omitRouteSourceContentFromDirectory(
|
|
638
|
+
directory: string,
|
|
639
|
+
routes: readonly Pick<RouteDefinition, "entry">[],
|
|
640
|
+
): void {
|
|
641
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
642
|
+
const entryPath = path.join(directory, entry.name)
|
|
643
|
+
|
|
644
|
+
if (entry.isDirectory()) {
|
|
645
|
+
omitRouteSourceContentFromDirectory(entryPath, routes)
|
|
646
|
+
continue
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!entry.name.endsWith(".map")) {
|
|
650
|
+
continue
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const serializedSourceMap = fs.readFileSync(entryPath, "utf8")
|
|
654
|
+
const asset: OutputAsset = {
|
|
655
|
+
type: "asset",
|
|
656
|
+
fileName: entry.name,
|
|
657
|
+
source: serializedSourceMap,
|
|
658
|
+
}
|
|
659
|
+
const bundle = { [entry.name]: asset }
|
|
660
|
+
|
|
661
|
+
omitRouteSourceContent(bundle, routes)
|
|
662
|
+
if (
|
|
663
|
+
typeof asset.source === "string" &&
|
|
664
|
+
asset.source !== serializedSourceMap
|
|
665
|
+
) {
|
|
666
|
+
fs.writeFileSync(entryPath, asset.source)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function removeServerRouteExports(source: string, id = "route.js") {
|
|
672
|
+
const ast = parse(source, { sourceType: "module" })
|
|
673
|
+
|
|
674
|
+
if (!removeExports(ast, SERVER_ONLY_ROUTE_EXPORTS)) {
|
|
675
|
+
return null
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return generate(ast, {
|
|
679
|
+
sourceMaps: true,
|
|
680
|
+
filename: id,
|
|
681
|
+
sourceFileName: cleanModuleId(id),
|
|
682
|
+
})
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export function flamefront(options: FlamefrontOptions = {}) {
|
|
686
|
+
const output = resolveFlamefrontOutput(options)
|
|
687
|
+
let root = process.cwd()
|
|
688
|
+
let serverBuild = false
|
|
689
|
+
let appPromise: Promise<AppDefinition> | undefined
|
|
690
|
+
let manifestRevision = 0
|
|
691
|
+
let manifestGlobDirectories: readonly string[] = []
|
|
692
|
+
const manifestId = options.routes ?? "/src/app.ts"
|
|
693
|
+
const manifestPath = () =>
|
|
694
|
+
path.resolve(
|
|
695
|
+
root,
|
|
696
|
+
manifestId.startsWith("/") ? `.${manifestId}` : manifestId,
|
|
697
|
+
)
|
|
698
|
+
const loadApp = async () => {
|
|
699
|
+
const manifestUrl = new URL(pathToFileURL(manifestPath()))
|
|
700
|
+
|
|
701
|
+
manifestUrl.searchParams.set("flamefront", String(manifestRevision))
|
|
702
|
+
setGlobRoot(root)
|
|
703
|
+
appPromise ??= import(manifestUrl.href).then((module) => {
|
|
704
|
+
const app = module.app ?? module.default
|
|
705
|
+
|
|
706
|
+
if (!app?.routeTree || typeof app.shell !== "string") {
|
|
707
|
+
throw new TypeError(
|
|
708
|
+
`Flamefront route manifest ${manifestId} must export an app with a shell.`,
|
|
709
|
+
)
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return app as AppDefinition
|
|
713
|
+
})
|
|
714
|
+
return appPromise
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const loadRoutes = async () => (await loadApp()).routes
|
|
718
|
+
const loadRouteModuleIds = async () =>
|
|
719
|
+
new Set(
|
|
720
|
+
(await loadRoutes()).map((route) => resolveRouteEntry(root, route.entry)),
|
|
721
|
+
)
|
|
722
|
+
const generateTypes = async () =>
|
|
723
|
+
writeRouteImportMap(await loadApp(), { root })
|
|
724
|
+
const configureRoot = (config: { readonly root: string }) => {
|
|
725
|
+
root = config.root
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const frameworkModulesPlugin = {
|
|
729
|
+
name: "flamefront:framework-modules",
|
|
730
|
+
enforce: "pre" as const,
|
|
731
|
+
configResolved: configureRoot,
|
|
732
|
+
async buildStart() {
|
|
733
|
+
await generateTypes()
|
|
734
|
+
},
|
|
735
|
+
transform(source: string, id: string) {
|
|
736
|
+
if (cleanModuleId(id) !== manifestPath()) {
|
|
737
|
+
return null
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const transformed = transformManifestGlobs(source, id, root)
|
|
741
|
+
|
|
742
|
+
manifestGlobDirectories = transformed?.directories ?? []
|
|
743
|
+
return transformed
|
|
744
|
+
? { code: transformed.code, map: transformed.map }
|
|
745
|
+
: null
|
|
746
|
+
},
|
|
747
|
+
async handleHotUpdate(context: {
|
|
748
|
+
file: string
|
|
749
|
+
server: {
|
|
750
|
+
moduleGraph: {
|
|
751
|
+
getModuleById(id: string): unknown
|
|
752
|
+
invalidateModule(module: unknown): void
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}) {
|
|
756
|
+
const manifestChanged = context.file === manifestPath()
|
|
757
|
+
const globChanged = manifestGlobDirectories.some((directory) =>
|
|
758
|
+
isPathWithinDirectory(directory, context.file),
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
if (!manifestChanged && !globChanged) {
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
manifestRevision += 1
|
|
766
|
+
appPromise = undefined
|
|
767
|
+
try {
|
|
768
|
+
await generateTypes()
|
|
769
|
+
} catch {
|
|
770
|
+
// Keep the previous declarations while an edited manifest is invalid.
|
|
771
|
+
// Vite will report the manifest error when the virtual route modules
|
|
772
|
+
// are requested, but a stale type file must not block editing.
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (globChanged) {
|
|
776
|
+
const manifestModule =
|
|
777
|
+
context.server.moduleGraph.getModuleById(manifestPath())
|
|
778
|
+
|
|
779
|
+
if (manifestModule) {
|
|
780
|
+
context.server.moduleGraph.invalidateModule(manifestModule)
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
for (const moduleId of [
|
|
785
|
+
resolvedRemixRoutesId,
|
|
786
|
+
resolvedServerRoutesId,
|
|
787
|
+
resolvedServerEntryId,
|
|
788
|
+
]) {
|
|
789
|
+
const generatedModule =
|
|
790
|
+
context.server.moduleGraph.getModuleById(moduleId)
|
|
791
|
+
|
|
792
|
+
if (generatedModule) {
|
|
793
|
+
context.server.moduleGraph.invalidateModule(generatedModule)
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
async resolveId(
|
|
798
|
+
this: PluginContext,
|
|
799
|
+
id: string,
|
|
800
|
+
importer?: string,
|
|
801
|
+
resolveOptions: ResolveOptions = {},
|
|
802
|
+
) {
|
|
803
|
+
if (id === remixRoutesId) {
|
|
804
|
+
return resolvedRemixRoutesId
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (id === serverRoutesId) {
|
|
808
|
+
return resolvedServerRoutesId
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (id === serverEntryId) {
|
|
812
|
+
return resolvedServerEntryId
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (id === "flamefront/entry") {
|
|
816
|
+
return resolvedServerEntryId
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (
|
|
820
|
+
id === "./hydration-route.tsrx?octane-hydrate=0" &&
|
|
821
|
+
importer?.startsWith(resolvedHydrationRoutePrefix)
|
|
822
|
+
) {
|
|
823
|
+
const parameters = new URLSearchParams(
|
|
824
|
+
importer.slice(importer.indexOf("?") + 1),
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
parameters.set("octane-hydrate", "0")
|
|
828
|
+
return `${hydrationRouteId}?${parameters}`
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (id.startsWith(resolvedHydrationRoutePrefix)) {
|
|
832
|
+
return id
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (id.startsWith(resolvedMarkdownRoutePrefix)) {
|
|
836
|
+
return id
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (
|
|
840
|
+
resolveOptions.scan ||
|
|
841
|
+
isServerEnvironment(this, resolveOptions) ||
|
|
842
|
+
resolveOptions.custom?.["flamefront:server-module"]
|
|
843
|
+
) {
|
|
844
|
+
return null
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const nestedOptions: ResolveOptions = {
|
|
848
|
+
...resolveOptions,
|
|
849
|
+
custom: { ...resolveOptions.custom, "flamefront:server-module": true },
|
|
850
|
+
}
|
|
851
|
+
const resolved = await this.resolve(id, importer, nestedOptions)
|
|
852
|
+
|
|
853
|
+
if (!resolved) {
|
|
854
|
+
return null
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const resolvedId = cleanModuleId(resolved.id)
|
|
858
|
+
|
|
859
|
+
if (
|
|
860
|
+
!serverFilePattern.test(resolvedId) &&
|
|
861
|
+
!serverDirectoryPattern.test(resolvedId)
|
|
862
|
+
) {
|
|
863
|
+
return null
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (!importer || importer.endsWith(".html")) {
|
|
867
|
+
return null
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const importerId = cleanModuleId(importer)
|
|
871
|
+
const importerLabel = path.relative(root, importerId) || importerId
|
|
872
|
+
const routeHint = (await loadRouteModuleIds()).has(importerId)
|
|
873
|
+
? " Flamefront removes server imports used exclusively by `loader`, but this import is still referenced by client code."
|
|
874
|
+
: ""
|
|
875
|
+
|
|
876
|
+
throw new Error(
|
|
877
|
+
`Server-only module ${JSON.stringify(id)} was referenced by client module ${JSON.stringify(importerLabel)}.${routeHint}`,
|
|
878
|
+
)
|
|
879
|
+
},
|
|
880
|
+
async load(id: string) {
|
|
881
|
+
if (id === resolvedRemixRoutesId) {
|
|
882
|
+
return generateRemixRoutes(await loadApp())
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
if (id === resolvedServerRoutesId) {
|
|
886
|
+
return generateServerRoutes(await loadApp())
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (id === resolvedServerEntryId) {
|
|
890
|
+
return generateServerEntry(output)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (id.startsWith(resolvedHydrationRoutePrefix)) {
|
|
894
|
+
const parameters = new URLSearchParams(id.slice(id.indexOf("?") + 1))
|
|
895
|
+
const entry = parameters.get("entry")
|
|
896
|
+
const serializedHydration = parameters.get("hydration")
|
|
897
|
+
|
|
898
|
+
if (!entry || !serializedHydration) {
|
|
899
|
+
throw new TypeError(
|
|
900
|
+
"Flamefront hydration route is missing its configuration.",
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return generateHydrationRoute(
|
|
905
|
+
entry,
|
|
906
|
+
JSON.parse(serializedHydration) as GeneratedHydration | "none",
|
|
907
|
+
)
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (id.startsWith(resolvedMarkdownRoutePrefix)) {
|
|
911
|
+
const parameters = new URLSearchParams(id.slice(id.indexOf("?") + 1))
|
|
912
|
+
const entry = parameters.get("entry")
|
|
913
|
+
|
|
914
|
+
if (!entry) {
|
|
915
|
+
throw new TypeError("Flamefront Markdown route is missing its entry.")
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
return generateMarkdownRoute(entry)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return null
|
|
922
|
+
},
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
const routeModulePlugin = {
|
|
926
|
+
name: "flamefront:route-modules",
|
|
927
|
+
enforce: "post" as const,
|
|
928
|
+
configResolved(config: {
|
|
929
|
+
readonly root: string
|
|
930
|
+
readonly build?: { readonly ssr?: unknown }
|
|
931
|
+
}) {
|
|
932
|
+
configureRoot(config)
|
|
933
|
+
serverBuild = Boolean(config.build?.ssr)
|
|
934
|
+
},
|
|
935
|
+
async generateBundle(_outputOptions: unknown, bundle: OutputBundle) {
|
|
936
|
+
if (!serverBuild) {
|
|
937
|
+
omitRouteSourceContent(bundle, await loadRoutes())
|
|
938
|
+
}
|
|
939
|
+
},
|
|
940
|
+
async writeBundle(outputOptions: { readonly dir?: string }) {
|
|
941
|
+
// Rollup serializes chunk maps after generateBundle, and other plugins can
|
|
942
|
+
// emit late client chunks. Scrub the completed output as the final guard.
|
|
943
|
+
if (!serverBuild && outputOptions.dir) {
|
|
944
|
+
omitRouteSourceContentFromDirectory(
|
|
945
|
+
outputOptions.dir,
|
|
946
|
+
await loadRoutes(),
|
|
947
|
+
)
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
async transform(
|
|
951
|
+
this: PluginContext,
|
|
952
|
+
source: string,
|
|
953
|
+
id: string,
|
|
954
|
+
transformOptions?: TransformOptions,
|
|
955
|
+
) {
|
|
956
|
+
if (isServerEnvironment(this, transformOptions)) {
|
|
957
|
+
return null
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (!(await loadRouteModuleIds()).has(cleanModuleId(id))) {
|
|
961
|
+
return null
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const transformed = removeServerRouteExports(source, id)
|
|
965
|
+
|
|
966
|
+
if (!transformed) {
|
|
967
|
+
return null
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return { code: transformed.code, map: transformed.map }
|
|
971
|
+
},
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const markdownPlugin =
|
|
975
|
+
options.markdown === false
|
|
976
|
+
? undefined
|
|
977
|
+
: vitePluginSatteri({
|
|
978
|
+
...(options.markdown ?? {}),
|
|
979
|
+
features: {
|
|
980
|
+
gfm: true,
|
|
981
|
+
frontmatter: true,
|
|
982
|
+
...options.markdown?.features,
|
|
983
|
+
},
|
|
984
|
+
mdx:
|
|
985
|
+
options.markdown?.mdx === false
|
|
986
|
+
? false
|
|
987
|
+
: {
|
|
988
|
+
...(typeof options.markdown?.mdx === "object"
|
|
989
|
+
? options.markdown.mdx
|
|
990
|
+
: {}),
|
|
991
|
+
jsx: true,
|
|
992
|
+
jsxImportSource: "octane",
|
|
993
|
+
jsxRuntime: "automatic",
|
|
994
|
+
},
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
const mdxCompilerPlugin =
|
|
998
|
+
options.markdown === false || options.markdown?.mdx === false
|
|
999
|
+
? undefined
|
|
1000
|
+
: {
|
|
1001
|
+
name: "flamefront:markdown-mdx",
|
|
1002
|
+
async transform(
|
|
1003
|
+
this: PluginContext,
|
|
1004
|
+
source: string,
|
|
1005
|
+
id: string,
|
|
1006
|
+
transformOptions?: TransformOptions,
|
|
1007
|
+
): Promise<{ code: string; map: object | null } | null> {
|
|
1008
|
+
if (!cleanModuleId(id).endsWith(".mdx")) {
|
|
1009
|
+
return null
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const server = isServerEnvironment(this, transformOptions)
|
|
1013
|
+
const command = this.environment?.config?.command
|
|
1014
|
+
const compiled = compileOctane(source, id, {
|
|
1015
|
+
dev: command === "serve",
|
|
1016
|
+
hmr: command === "serve" && !server ? "vite" : false,
|
|
1017
|
+
mode: server ? "server" : "client",
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
return { code: compiled.code, map: compiled.map }
|
|
1021
|
+
},
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return [
|
|
1025
|
+
frameworkModulesPlugin,
|
|
1026
|
+
routeModulePlugin,
|
|
1027
|
+
markdownPlugin,
|
|
1028
|
+
mdxCompilerPlugin,
|
|
1029
|
+
] as const
|
|
1030
|
+
}
|