flamefront 0.1.0-alpha.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/src/vite.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import fs from "node:fs"
2
2
  import path from "node:path"
3
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"
4
7
  import type {
5
8
  AppDefinition,
6
9
  GeneratedHydration,
@@ -9,23 +12,56 @@ import type {
9
12
  RouteConfig,
10
13
  RouteDefinition,
11
14
  } from "./index.ts"
12
- import { generate, parse } from "./babel.ts"
15
+ import { generate, parse, traverse, type Babel } from "./babel.ts"
16
+ import { expandGlob, globDirectory, setGlobRoot } from "./glob.ts"
13
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"
14
31
 
15
32
  export const remixRoutesId = "virtual:flamefront/remix-routes"
16
33
  const resolvedRemixRoutesId = `\0${remixRoutesId}`
17
34
 
18
35
  export const serverRoutesId = "virtual:flamefront/server-routes"
19
36
  const resolvedServerRoutesId = `\0${serverRoutesId}`
37
+
38
+ export const serverEntryId = "virtual:flamefront/server-entry"
39
+ const resolvedServerEntryId = `\0${serverEntryId}`
20
40
  const hydrationRouteId = "/@flamefront/hydration-route.tsrx"
21
41
  const resolvedHydrationRoutePrefix = `${hydrationRouteId}?`
42
+ const markdownRouteId = "/@flamefront/markdown-route.tsrx"
43
+ const resolvedMarkdownRoutePrefix = `${markdownRouteId}?`
22
44
  const SERVER_ONLY_ROUTE_EXPORTS = ["loader"] as const
23
45
  const serverFilePattern = /\.server(?:\.[cm]?[jt]sx?|\.tsrx)$/
24
46
  const serverDirectoryPattern = /\/\.server\//
25
47
 
26
- export interface FlamefrontOptions {
48
+ export interface FlamefrontOptions extends FlamefrontOutputOptions {
27
49
  /** Project-root route manifest module. */
28
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">
29
65
  }
30
66
 
31
67
  function quote(value: string): string {
@@ -61,10 +97,24 @@ function hydrationComponentId(
61
97
  return `${hydrationRouteId}?${parameters}`
62
98
  }
63
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
+
64
109
  function browserRouteModuleId(routeDefinition: RouteDefinition): string {
110
+ const componentEntry =
111
+ routeDefinition.content === "markdown"
112
+ ? markdownComponentId(routeDefinition.entry)
113
+ : routeDefinition.entry
114
+
65
115
  return generatesHydrationBoundary(routeDefinition)
66
- ? hydrationComponentId(routeDefinition.entry, routeDefinition.hydration)
67
- : routeDefinition.entry
116
+ ? hydrationComponentId(componentEntry, routeDefinition.hydration)
117
+ : componentEntry
68
118
  }
69
119
 
70
120
  function generatedRouteId(kind: "layout" | "route", location: string): string {
@@ -99,7 +149,7 @@ function generatedRouteMetadata(
99
149
  parent,
100
150
  path: config.path,
101
151
  render: config.render,
102
- navigation: config.render === "static" ? "fragment" : "router",
152
+ navigation: config.render === "client" ? "router" : "fragment",
103
153
  hydration: config.hydration,
104
154
  }
105
155
  }
@@ -117,7 +167,7 @@ function generateRoutePreloaders(routeTree: readonly RouteConfig[]): string {
117
167
  continue
118
168
  }
119
169
 
120
- if (config.render === "static") {
170
+ if (config.render !== "client") {
121
171
  continue
122
172
  }
123
173
 
@@ -155,33 +205,35 @@ function lazyRoute(
155
205
  ): string {
156
206
  const { entry } = routeDefinition
157
207
  const browserLoader =
158
- routeDefinition.render === "static"
159
- ? "loadStaticRouteFragment"
160
- : "loadRouteData"
161
- const browserLoaderExpression = `(args) => ${browserLoader}(args, ${JSON.stringify(routing)})`
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)})`
162
213
 
163
- if (routeDefinition.render === "static") {
214
+ if (routeDefinition.render !== "client") {
164
215
  const componentId = browserRouteModuleId(routeDefinition)
165
216
 
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} }; }`
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} }; }`
167
218
  }
168
219
 
169
220
  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
- }
221
+ if (routeDefinition.content === "markdown") {
222
+ const componentId = browserRouteModuleId(routeDefinition)
172
223
 
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
- }
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
+ }
176
226
 
177
- const componentId = hydrationComponentId(entry, routeDefinition.hydration)
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
+ }
178
229
 
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} }; }`
230
+ throw new Error(`Unsupported route render mode ${routeDefinition.render}.`)
180
231
  }
181
232
 
182
233
  function collectRouteMetadata(
183
234
  routeTree: readonly RouteConfig[],
184
235
  shell: string,
236
+ shellHydration: AppDefinition["shellHydration"],
185
237
  ): readonly GeneratedRouteMetadata[] {
186
238
  const rootId = "flamefront:shell:root"
187
239
  const metadata: GeneratedRouteMetadata[] = [
@@ -191,6 +243,7 @@ function collectRouteMetadata(
191
243
  kind: "shell",
192
244
  entry: shell,
193
245
  navigation: "router",
246
+ hydration: shellHydration,
194
247
  },
195
248
  ]
196
249
 
@@ -243,12 +296,19 @@ function generateConfigs(
243
296
  }
244
297
 
245
298
  export function generateRemixRoutes(
246
- app: Pick<AppDefinition, "shell" | "routeTree" | "routing">,
299
+ app: Pick<
300
+ AppDefinition,
301
+ "shell" | "shellHydration" | "routeTree" | "routing"
302
+ >,
247
303
  ): string {
248
304
  const rootId = "flamefront:shell:root"
249
- const routeMetadata = collectRouteMetadata(app.routeTree, app.shell)
305
+ const routeMetadata = collectRouteMetadata(
306
+ app.routeTree,
307
+ app.shell,
308
+ app.shellHydration,
309
+ )
250
310
 
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)}`
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)}`
252
312
  }
253
313
 
254
314
  /** Generate the server-only route-module importer used by loader endpoints. */
@@ -263,6 +323,15 @@ export function generateServerRoutes(
263
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`
264
324
  }
265
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
+
266
335
  function hydrationStrategy(hydration: GeneratedHydration | "none"): {
267
336
  readonly importName: string
268
337
  readonly expression: string
@@ -297,6 +366,11 @@ function hydrationStrategy(hydration: GeneratedHydration | "none"): {
297
366
  }
298
367
  }
299
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
+
300
374
  export function generateHydrationRoute(
301
375
  entry: string,
302
376
  hydration: GeneratedHydration | "none",
@@ -316,7 +390,9 @@ interface ResolveOptions extends TransformOptions {
316
390
  }
317
391
 
318
392
  interface PluginContext {
319
- readonly environment?: { readonly config?: { readonly consumer?: string } }
393
+ readonly environment?: {
394
+ readonly config?: { readonly command?: string; readonly consumer?: string }
395
+ }
320
396
  resolve(
321
397
  id: string,
322
398
  importer: string | undefined,
@@ -346,6 +422,139 @@ function cleanModuleId(id: string): string {
346
422
  return id.split("?", 1)[0].replaceAll("\\", "/")
347
423
  }
348
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
+
349
558
  function isServerEnvironment(
350
559
  context: PluginContext,
351
560
  options?: TransformOptions,
@@ -474,10 +683,12 @@ export function removeServerRouteExports(source: string, id = "route.js") {
474
683
  }
475
684
 
476
685
  export function flamefront(options: FlamefrontOptions = {}) {
686
+ const output = resolveFlamefrontOutput(options)
477
687
  let root = process.cwd()
478
688
  let serverBuild = false
479
689
  let appPromise: Promise<AppDefinition> | undefined
480
690
  let manifestRevision = 0
691
+ let manifestGlobDirectories: readonly string[] = []
481
692
  const manifestId = options.routes ?? "/src/app.ts"
482
693
  const manifestPath = () =>
483
694
  path.resolve(
@@ -488,6 +699,7 @@ export function flamefront(options: FlamefrontOptions = {}) {
488
699
  const manifestUrl = new URL(pathToFileURL(manifestPath()))
489
700
 
490
701
  manifestUrl.searchParams.set("flamefront", String(manifestRevision))
702
+ setGlobRoot(root)
491
703
  appPromise ??= import(manifestUrl.href).then((module) => {
492
704
  const app = module.app ?? module.default
493
705
 
@@ -507,6 +719,8 @@ export function flamefront(options: FlamefrontOptions = {}) {
507
719
  new Set(
508
720
  (await loadRoutes()).map((route) => resolveRouteEntry(root, route.entry)),
509
721
  )
722
+ const generateTypes = async () =>
723
+ writeRouteImportMap(await loadApp(), { root })
510
724
  const configureRoot = (config: { readonly root: string }) => {
511
725
  root = config.root
512
726
  }
@@ -515,7 +729,22 @@ export function flamefront(options: FlamefrontOptions = {}) {
515
729
  name: "flamefront:framework-modules",
516
730
  enforce: "pre" as const,
517
731
  configResolved: configureRoot,
518
- handleHotUpdate(context: {
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: {
519
748
  file: string
520
749
  server: {
521
750
  moduleGraph: {
@@ -524,13 +753,39 @@ export function flamefront(options: FlamefrontOptions = {}) {
524
753
  }
525
754
  }
526
755
  }) {
527
- if (context.file !== manifestPath()) {
756
+ const manifestChanged = context.file === manifestPath()
757
+ const globChanged = manifestGlobDirectories.some((directory) =>
758
+ isPathWithinDirectory(directory, context.file),
759
+ )
760
+
761
+ if (!manifestChanged && !globChanged) {
528
762
  return
529
763
  }
530
764
 
531
765
  manifestRevision += 1
532
766
  appPromise = undefined
533
- for (const moduleId of [resolvedRemixRoutesId, resolvedServerRoutesId]) {
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
+ ]) {
534
789
  const generatedModule =
535
790
  context.server.moduleGraph.getModuleById(moduleId)
536
791
 
@@ -553,6 +808,14 @@ export function flamefront(options: FlamefrontOptions = {}) {
553
808
  return resolvedServerRoutesId
554
809
  }
555
810
 
811
+ if (id === serverEntryId) {
812
+ return resolvedServerEntryId
813
+ }
814
+
815
+ if (id === "flamefront/entry") {
816
+ return resolvedServerEntryId
817
+ }
818
+
556
819
  if (
557
820
  id === "./hydration-route.tsrx?octane-hydrate=0" &&
558
821
  importer?.startsWith(resolvedHydrationRoutePrefix)
@@ -569,6 +832,10 @@ export function flamefront(options: FlamefrontOptions = {}) {
569
832
  return id
570
833
  }
571
834
 
835
+ if (id.startsWith(resolvedMarkdownRoutePrefix)) {
836
+ return id
837
+ }
838
+
572
839
  if (
573
840
  resolveOptions.scan ||
574
841
  isServerEnvironment(this, resolveOptions) ||
@@ -619,6 +886,10 @@ export function flamefront(options: FlamefrontOptions = {}) {
619
886
  return generateServerRoutes(await loadApp())
620
887
  }
621
888
 
889
+ if (id === resolvedServerEntryId) {
890
+ return generateServerEntry(output)
891
+ }
892
+
622
893
  if (id.startsWith(resolvedHydrationRoutePrefix)) {
623
894
  const parameters = new URLSearchParams(id.slice(id.indexOf("?") + 1))
624
895
  const entry = parameters.get("entry")
@@ -636,6 +907,17 @@ export function flamefront(options: FlamefrontOptions = {}) {
636
907
  )
637
908
  }
638
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
+
639
921
  return null
640
922
  },
641
923
  }
@@ -689,5 +971,60 @@ export function flamefront(options: FlamefrontOptions = {}) {
689
971
  },
690
972
  }
691
973
 
692
- return [frameworkModulesPlugin, routeModulePlugin] as const
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
693
1030
  }