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/src/typegen.ts ADDED
@@ -0,0 +1,207 @@
1
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import { pathToFileURL } from "node:url"
4
+ import type { AppDefinition } from "./index.ts"
5
+ import { setGlobRoot } from "./glob.ts"
6
+
7
+ /** Directory containing declarations generated for the current application. */
8
+ export const flamefrontTypesDirectory = ".flamefront/types"
9
+
10
+ /** Declaration file containing the authored route-to-module relationship. */
11
+ export const routeImportMapFile = "route-import-map.d.ts"
12
+
13
+ /** Declaration file containing the built-in Markdown module shapes. */
14
+ export const markdownModuleTypesFile = "markdown-modules.d.ts"
15
+
16
+ export interface RouteImportMapGenerationOptions {
17
+ /** Project root used to resolve project-root route module IDs. */
18
+ readonly root?: string
19
+ /** Destination used to calculate relative import specifiers. */
20
+ readonly outputFile?: string
21
+ }
22
+
23
+ export interface TypegenProjectOptions {
24
+ /** Project root containing the route manifest. */
25
+ readonly root?: string
26
+ /** Project-root route manifest module, defaulting to `/src/app.ts`. */
27
+ readonly routes?: string
28
+ /** Optional declaration destination, relative to `root` unless absolute. */
29
+ readonly outputFile?: string
30
+ }
31
+
32
+ export interface RouteImportMapWriteResult {
33
+ readonly file: string
34
+ readonly source: string
35
+ readonly written: boolean
36
+ }
37
+
38
+ export interface TypegenProjectResult extends RouteImportMapWriteResult {
39
+ readonly manifest: string
40
+ }
41
+
42
+ interface AppModule {
43
+ readonly app?: AppDefinition
44
+ readonly default?: AppDefinition
45
+ }
46
+
47
+ let manifestRevision = 0
48
+
49
+ function outputPath(root: string, outputFile: string | undefined): string {
50
+ return path.resolve(
51
+ root,
52
+ outputFile ?? `${flamefrontTypesDirectory}/${routeImportMapFile}`,
53
+ )
54
+ }
55
+
56
+ function manifestPath(root: string, routes: string): string {
57
+ const relativeRoutes = routes.startsWith("/") ? `.${routes}` : routes
58
+
59
+ return path.resolve(root, relativeRoutes)
60
+ }
61
+
62
+ function cleanRouteEntry(entry: string): string {
63
+ return entry.split(/[?#]/, 1)[0] ?? entry
64
+ }
65
+
66
+ function routeImportSpecifier(
67
+ root: string,
68
+ outputFile: string,
69
+ entry: string,
70
+ ): string {
71
+ const routeEntry = cleanRouteEntry(entry)
72
+ const projectEntry = routeEntry.startsWith("/")
73
+ ? routeEntry.slice(1)
74
+ : routeEntry
75
+ const absoluteEntry = path.resolve(root, projectEntry)
76
+ const relativeEntry = path
77
+ .relative(path.dirname(outputFile), absoluteEntry)
78
+ .split(path.sep)
79
+ .join("/")
80
+
81
+ return relativeEntry.startsWith(".") ? relativeEntry : `./${relativeEntry}`
82
+ }
83
+
84
+ /**
85
+ * Generate the declaration-only module augmentation for an authored route
86
+ * manifest. The module values deliberately remain `typeof import(...)` so
87
+ * changes to a route module's exports flow through without regenerating this
88
+ * relationship.
89
+ */
90
+ export function generateRouteImportMap(
91
+ app: Pick<AppDefinition, "routes">,
92
+ options: RouteImportMapGenerationOptions = {},
93
+ ): string {
94
+ const root = path.resolve(options.root ?? process.cwd())
95
+ const file = outputPath(root, options.outputFile)
96
+ const entries = app.routes
97
+ .map(
98
+ (route) =>
99
+ ` ${JSON.stringify(route.path)}: typeof import(${JSON.stringify(
100
+ routeImportSpecifier(root, file, route.entry),
101
+ )})`,
102
+ )
103
+ .join("\n")
104
+
105
+ return `// Generated by Flamefront. Do not edit.\ndeclare module "flamefront" {\n interface RouteImportMap {\n${entries}\n }\n}\n\nexport {}\n`
106
+ }
107
+
108
+ /** Generate ambient declarations for Vite Markdown and MDX imports. */
109
+ export function generateMarkdownModuleTypes(): string {
110
+ return `// Generated by Flamefront. Do not edit.\ndeclare module "*.md" {\n const html: string\n export default html\n export { html }\n export const frontmatter: Record<string, unknown>\n}\n\ndeclare module "*.mdx" {\n const Component: unknown\n export default Component\n export const frontmatter: Record<string, unknown>\n}\n`
111
+ }
112
+
113
+ async function readExisting(file: string): Promise<string | undefined> {
114
+ try {
115
+ return await readFile(file, "utf8")
116
+ } catch (error) {
117
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
118
+ return undefined
119
+ }
120
+
121
+ throw error
122
+ }
123
+ }
124
+
125
+ async function writeAtomically(file: string, source: string): Promise<void> {
126
+ const temporaryFile = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`
127
+
128
+ try {
129
+ await writeFile(temporaryFile, source, "utf8")
130
+ await rename(temporaryFile, file)
131
+ } finally {
132
+ await unlink(temporaryFile).catch(() => undefined)
133
+ }
134
+ }
135
+
136
+ /** Write generated declarations only when their contents have changed. */
137
+ export async function writeRouteImportMap(
138
+ app: Pick<AppDefinition, "routes">,
139
+ options: RouteImportMapGenerationOptions = {},
140
+ ): Promise<RouteImportMapWriteResult> {
141
+ const root = path.resolve(options.root ?? process.cwd())
142
+ const file = outputPath(root, options.outputFile)
143
+ const source = generateRouteImportMap(app, {
144
+ ...options,
145
+ root,
146
+ outputFile: file,
147
+ })
148
+ const existing = await readExisting(file)
149
+
150
+ const markdownTypesFile = path.join(
151
+ path.dirname(file),
152
+ markdownModuleTypesFile,
153
+ )
154
+ const markdownTypesSource = generateMarkdownModuleTypes()
155
+ const existingMarkdownTypes = await readExisting(markdownTypesFile)
156
+ const routeMapChanged = existing !== source
157
+ const markdownTypesChanged = existingMarkdownTypes !== markdownTypesSource
158
+
159
+ if (routeMapChanged) {
160
+ await mkdir(path.dirname(file), { recursive: true })
161
+ await writeAtomically(file, source)
162
+ }
163
+
164
+ if (markdownTypesChanged) {
165
+ await mkdir(path.dirname(markdownTypesFile), { recursive: true })
166
+ await writeAtomically(markdownTypesFile, markdownTypesSource)
167
+ }
168
+
169
+ return { file, source, written: routeMapChanged || markdownTypesChanged }
170
+ }
171
+
172
+ async function loadManifest(
173
+ root: string,
174
+ routes: string,
175
+ ): Promise<{ app: AppDefinition; manifest: string }> {
176
+ const manifest = manifestPath(root, routes)
177
+ const url = pathToFileURL(manifest)
178
+
179
+ manifestRevision += 1
180
+ url.searchParams.set("flamefront-typegen", String(manifestRevision))
181
+ setGlobRoot(root)
182
+ const module = (await import(url.href)) as AppModule
183
+ const app = module.app ?? module.default
184
+
185
+ if (!app || typeof app.shell !== "string" || !Array.isArray(app.routes)) {
186
+ throw new Error(
187
+ `${manifest} must export an app with a shell and routes array.`,
188
+ )
189
+ }
190
+
191
+ return { app, manifest }
192
+ }
193
+
194
+ /** Evaluate the project manifest and generate its route import declarations. */
195
+ export async function generateProjectTypes(
196
+ options: TypegenProjectOptions = {},
197
+ ): Promise<TypegenProjectResult> {
198
+ const root = path.resolve(options.root ?? process.cwd())
199
+ const routes = options.routes ?? "/src/app.ts"
200
+ const loaded = await loadManifest(root, routes)
201
+ const generated = await writeRouteImportMap(loaded.app, {
202
+ root,
203
+ outputFile: options.outputFile,
204
+ })
205
+
206
+ return { ...generated, manifest: loaded.manifest }
207
+ }
@@ -0,0 +1,35 @@
1
+ declare module "virtual:flamefront/remix-routes" {
2
+ import type { RouteObject } from "@octanejs/remix-router"
3
+ import type { RouterDocument as RouterDocumentComponent } from "./octane.tsx"
4
+ import type {
5
+ GeneratedRouteMetadata,
6
+ NormalizedRoutingOptions,
7
+ } from "./index.ts"
8
+
9
+ export const RouterDocument: RouterDocumentComponent
10
+ export const routes: RouteObject[]
11
+ export const routing: NormalizedRoutingOptions
12
+ export const routeMetadata: readonly GeneratedRouteMetadata[]
13
+ export function preloadRoute(entry: string): Promise<void>
14
+ }
15
+
16
+ declare module "virtual:flamefront/server-routes" {
17
+ import type { RouteModule } from "./server.ts"
18
+
19
+ export function importRoute(entry: string): Promise<RouteModule>
20
+ }
21
+
22
+ declare module "virtual:flamefront/server-entry" {
23
+ import type { RouteDefinition } from "./index.ts"
24
+ import type {
25
+ FetchServerEntryOptions,
26
+ FlamefrontFetchServerEntry,
27
+ } from "./fetch.ts"
28
+ import type { FlamefrontServerEntry, SrvxServerEntryOptions } from "./srvx.ts"
29
+
30
+ export function createServerEntry<
31
+ Route extends RouteDefinition = RouteDefinition,
32
+ >(
33
+ options: FetchServerEntryOptions<Route> | SrvxServerEntryOptions<Route>,
34
+ ): FlamefrontFetchServerEntry | FlamefrontServerEntry
35
+ }