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/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
|
+
}
|