flamefront 0.1.0-alpha.0 → 0.1.1
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 +2 -91
- package/README.md +35 -620
- package/package.json +49 -26
- package/src/cli.ts +16 -1
- package/src/entry.ts +49 -0
- package/src/fetch.ts +280 -0
- package/src/fragment-client.ts +127 -87
- package/src/fragment-hydration-client.tsx +81 -0
- package/src/fragment-protocol.ts +3 -3
- package/src/fragment.tsx +656 -0
- package/src/glob.ts +294 -0
- package/src/identifier-prefix.ts +5 -0
- package/src/index.ts +569 -49
- package/src/lifecycle.ts +7 -6
- package/src/octane-client-core.ts +10 -3
- package/src/octane-client.ts +12 -6
- package/src/octane-compiler.d.ts +19 -0
- package/src/octane-default-renderer.tsx +124 -0
- package/src/octane-router-document.ts +19 -2
- package/src/{octane.ts → octane.tsx} +152 -145
- package/src/output.ts +48 -0
- package/src/remix-route-data.ts +28 -20
- package/src/remix-router.ts +10 -4
- package/src/route-data-client.ts +22 -12
- package/src/route-prefetch.ts +19 -12
- package/src/server.ts +122 -24
- package/src/srvx.ts +43 -183
- package/src/typegen.ts +207 -0
- package/src/virtual-remix-routes.d.ts +16 -1
- package/src/vite.ts +364 -27
- package/src/fragment.ts +0 -260
package/src/lifecycle.ts
CHANGED
|
@@ -23,9 +23,10 @@ import {
|
|
|
23
23
|
staticRouteFragmentDataFile,
|
|
24
24
|
} from "./static-fragment-artifacts.ts"
|
|
25
25
|
import {
|
|
26
|
-
|
|
27
|
-
type
|
|
26
|
+
routeFragmentProtocol,
|
|
27
|
+
type RouteFragmentArtifact,
|
|
28
28
|
} from "./fragment-client.ts"
|
|
29
|
+
import { setGlobRoot } from "./glob.ts"
|
|
29
30
|
|
|
30
31
|
export {
|
|
31
32
|
staticRouteFile,
|
|
@@ -65,6 +66,7 @@ export async function loadProject(
|
|
|
65
66
|
const url = pathToFileURL(routesFile)
|
|
66
67
|
|
|
67
68
|
url.searchParams.set("ff", String(Date.now()))
|
|
69
|
+
setGlobRoot(root)
|
|
68
70
|
const module = (await import(url.href)) as AppModule
|
|
69
71
|
const app = module.app ?? module.default
|
|
70
72
|
|
|
@@ -234,7 +236,6 @@ export async function buildProject(root = process.cwd()): Promise<void> {
|
|
|
234
236
|
configFile: resolve(root, "vite.config.ts"),
|
|
235
237
|
build: {
|
|
236
238
|
outDir: clientDirectory,
|
|
237
|
-
manifest: true,
|
|
238
239
|
},
|
|
239
240
|
})
|
|
240
241
|
await build({
|
|
@@ -320,7 +321,7 @@ export async function prerenderStaticRoutes(
|
|
|
320
321
|
render: (request: Request) => Promise<RenderDocumentResult>,
|
|
321
322
|
loadData?: (request: Request) => Promise<unknown>,
|
|
322
323
|
routing: Pick<NormalizedRoutingOptions, "basename"> = { basename: "/" },
|
|
323
|
-
renderFragment?: (request: Request) => Promise<
|
|
324
|
+
renderFragment?: (request: Request) => Promise<RouteFragmentArtifact>,
|
|
324
325
|
): Promise<void> {
|
|
325
326
|
for (const route of routes) {
|
|
326
327
|
const outputFile = staticRouteFile(clientDirectory, route)
|
|
@@ -342,14 +343,14 @@ export async function prerenderStaticRoutes(
|
|
|
342
343
|
const fragment = renderFragment
|
|
343
344
|
? await renderFragment(request)
|
|
344
345
|
: ({
|
|
345
|
-
protocol:
|
|
346
|
+
protocol: routeFragmentProtocol,
|
|
346
347
|
route: route.path,
|
|
347
348
|
boundary: route.entry,
|
|
348
349
|
html: rendered.html,
|
|
349
350
|
routeData: data,
|
|
350
351
|
boundaries: [],
|
|
351
352
|
status: rendered.status,
|
|
352
|
-
} satisfies
|
|
353
|
+
} satisfies RouteFragmentArtifact)
|
|
353
354
|
|
|
354
355
|
await mkdir(dirname(outputFile), { recursive: true })
|
|
355
356
|
await writeFile(outputFile, rendered.html)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AppDefinition, RouteDefinition } from "./index.ts"
|
|
2
|
-
import type { RouterDocument, RouterDocumentProps } from "./octane.
|
|
2
|
+
import type { RouterDocument, RouterDocumentProps } from "./octane.tsx"
|
|
3
|
+
import { shellIdentifierPrefix } from "./identifier-prefix.ts"
|
|
3
4
|
|
|
4
5
|
export type OctaneClientApp<Route extends RouteDefinition = RouteDefinition> =
|
|
5
6
|
Pick<AppDefinition<Route>, "match" | "prefetch">
|
|
@@ -48,11 +49,13 @@ export interface OctaneClientRuntime<
|
|
|
48
49
|
container: Container,
|
|
49
50
|
component: RouterDocument,
|
|
50
51
|
props: RouterDocumentProps,
|
|
52
|
+
options?: { readonly identifierPrefix?: string },
|
|
51
53
|
): Root
|
|
52
54
|
hydrateRoot(
|
|
53
55
|
container: Container,
|
|
54
56
|
component: RouterDocument,
|
|
55
57
|
props: RouterDocumentProps,
|
|
58
|
+
options?: { readonly identifierPrefix?: string },
|
|
56
59
|
): Root
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -127,8 +130,12 @@ export async function startOctaneClientWithRuntime<
|
|
|
127
130
|
const routerDocument = options.routerDocument ?? runtime.routerDocument
|
|
128
131
|
const props: RouterDocumentProps = { router, context: undefined }
|
|
129
132
|
const clientRoot = shouldHydrate
|
|
130
|
-
? runtime.hydrateRoot(root, routerDocument, props
|
|
131
|
-
|
|
133
|
+
? runtime.hydrateRoot(root, routerDocument, props, {
|
|
134
|
+
identifierPrefix: shellIdentifierPrefix,
|
|
135
|
+
})
|
|
136
|
+
: runtime.renderRoot(root, routerDocument, props, {
|
|
137
|
+
identifierPrefix: shellIdentifierPrefix,
|
|
138
|
+
})
|
|
132
139
|
|
|
133
140
|
return { router, root: clientRoot }
|
|
134
141
|
}
|
package/src/octane-client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataRouter } from "@octanejs/remix-router"
|
|
2
|
-
import { createRoot, hydrateRoot, type Root } from "octane"
|
|
2
|
+
import { createRoot, hydrateRoot, type ComponentBody, type Root } from "octane"
|
|
3
3
|
import type { RouteDefinition } from "./index.ts"
|
|
4
|
+
import type { RouterDocumentProps } from "./octane.tsx"
|
|
4
5
|
import {
|
|
5
6
|
startOctaneClientWithRuntime,
|
|
6
7
|
type StartOctaneClientOptions,
|
|
@@ -30,13 +31,18 @@ export function startOctaneClient<Route extends RouteDefinition>(
|
|
|
30
31
|
consumeHydrationData: consumeStaticRouterHydrationData,
|
|
31
32
|
createRoutePrefetcher,
|
|
32
33
|
createClientRouter,
|
|
33
|
-
renderRoot(root, component, props) {
|
|
34
|
-
const clientRoot = createRoot(root)
|
|
34
|
+
renderRoot(root, component, props, options) {
|
|
35
|
+
const clientRoot = createRoot(root, options)
|
|
35
36
|
|
|
36
|
-
clientRoot.render(component as
|
|
37
|
+
clientRoot.render(component as ComponentBody<RouterDocumentProps>, props)
|
|
37
38
|
return clientRoot
|
|
38
39
|
},
|
|
39
|
-
hydrateRoot: (root, component, props) =>
|
|
40
|
-
hydrateRoot(
|
|
40
|
+
hydrateRoot: (root, component, props, options) =>
|
|
41
|
+
hydrateRoot(
|
|
42
|
+
root,
|
|
43
|
+
component as ComponentBody<RouterDocumentProps>,
|
|
44
|
+
props,
|
|
45
|
+
options,
|
|
46
|
+
),
|
|
41
47
|
})
|
|
42
48
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare module "octane/compiler" {
|
|
2
|
+
export interface CompileOptions {
|
|
3
|
+
readonly dev?: boolean
|
|
4
|
+
readonly hmr?: boolean | "vite" | "webpack"
|
|
5
|
+
readonly mode?: "client" | "server"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CompileResult {
|
|
9
|
+
readonly code: string
|
|
10
|
+
readonly map: object | null
|
|
11
|
+
readonly diagnostics: readonly unknown[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function compile(
|
|
15
|
+
source: string,
|
|
16
|
+
filename: string,
|
|
17
|
+
options?: CompileOptions,
|
|
18
|
+
): CompileResult
|
|
19
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StaticRouterProvider,
|
|
3
|
+
type RouteObject,
|
|
4
|
+
createStaticRouter,
|
|
5
|
+
type DataRouter,
|
|
6
|
+
type StaticHandlerContext,
|
|
7
|
+
} from "@octanejs/remix-router"
|
|
8
|
+
import { renderToString } from "octane/server"
|
|
9
|
+
import { routeFragmentBoundaryTarget } from "./fragment.tsx"
|
|
10
|
+
import { RouterDocument } from "./octane-router-document.ts"
|
|
11
|
+
import type { OctaneRenderer } from "./octane.tsx"
|
|
12
|
+
|
|
13
|
+
function findRoute(
|
|
14
|
+
routes: readonly RouteObject[],
|
|
15
|
+
id: string,
|
|
16
|
+
): RouteObject | undefined {
|
|
17
|
+
for (const route of routes) {
|
|
18
|
+
if (route.id === id) {
|
|
19
|
+
return route
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const match = route.children ? findRoute(route.children, id) : undefined
|
|
23
|
+
|
|
24
|
+
if (match) {
|
|
25
|
+
return match
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function errorsForBoundary(
|
|
33
|
+
context: StaticHandlerContext,
|
|
34
|
+
matches: readonly { readonly route: { readonly id: string } }[],
|
|
35
|
+
): StaticHandlerContext["errors"] {
|
|
36
|
+
if (!context.errors) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ids = new Set(matches.map((match) => match.route.id))
|
|
41
|
+
const errors: Record<string, unknown> = {}
|
|
42
|
+
let inheritedError: unknown = undefined
|
|
43
|
+
|
|
44
|
+
for (const [id, error] of Object.entries(context.errors)) {
|
|
45
|
+
if (ids.has(id)) {
|
|
46
|
+
errors[id] = error
|
|
47
|
+
} else if (inheritedError === undefined) {
|
|
48
|
+
inheritedError = error
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (inheritedError !== undefined && matches[0]) {
|
|
53
|
+
errors[matches[0].route.id] ??= inheritedError
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Object.keys(errors).length > 0 ? errors : null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const defaultOctaneRenderer: OctaneRenderer = {
|
|
60
|
+
createStaticRouter: (routes, context) =>
|
|
61
|
+
createStaticRouter(
|
|
62
|
+
routes as RouteObject[],
|
|
63
|
+
context as StaticHandlerContext,
|
|
64
|
+
),
|
|
65
|
+
renderToString: (component, props, options) =>
|
|
66
|
+
renderToString(
|
|
67
|
+
component as Parameters<typeof renderToString>[0],
|
|
68
|
+
props,
|
|
69
|
+
options,
|
|
70
|
+
),
|
|
71
|
+
renderRouteFragment: (router, context, boundary, options) => {
|
|
72
|
+
const dataRouter = router as DataRouter
|
|
73
|
+
const staticContext = context as StaticHandlerContext
|
|
74
|
+
const state = dataRouter.state
|
|
75
|
+
const matchIndex = state.matches.findIndex(
|
|
76
|
+
(match) => match.route?.id === boundary,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if (matchIndex < 0) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`No server router match exists for fragment boundary ${JSON.stringify(boundary)}.`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const outletRoute = findRoute(dataRouter.routes, boundary)
|
|
86
|
+
|
|
87
|
+
if (!outletRoute) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`No server route exists for fragment boundary ${JSON.stringify(boundary)}.`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const matches = staticContext.matches.slice(matchIndex)
|
|
94
|
+
const outletContext = {
|
|
95
|
+
...staticContext,
|
|
96
|
+
matches,
|
|
97
|
+
errors: errorsForBoundary(staticContext, matches),
|
|
98
|
+
}
|
|
99
|
+
const outletRouter = createStaticRouter([outletRoute], outletContext)
|
|
100
|
+
const FragmentBoundaryProvider = routeFragmentBoundaryTarget.Provider
|
|
101
|
+
const FragmentRoot = () => (
|
|
102
|
+
<FragmentBoundaryProvider
|
|
103
|
+
value={options?.includeBoundary ? null : boundary}
|
|
104
|
+
>
|
|
105
|
+
<StaticRouterProvider
|
|
106
|
+
router={outletRouter}
|
|
107
|
+
context={outletContext}
|
|
108
|
+
hydrate={false}
|
|
109
|
+
/>
|
|
110
|
+
</FragmentBoundaryProvider>
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return renderToString(
|
|
114
|
+
FragmentRoot,
|
|
115
|
+
{},
|
|
116
|
+
{
|
|
117
|
+
...(options?.identifierPrefix
|
|
118
|
+
? { identifierPrefix: options.identifierPrefix }
|
|
119
|
+
: {}),
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
},
|
|
123
|
+
defaultRouterDocument: RouterDocument,
|
|
124
|
+
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { RouterProvider } from "@octanejs/remix-router/dom"
|
|
2
|
-
import
|
|
2
|
+
import { createElement } from "octane"
|
|
3
|
+
import { routeOutletHtmlContext } from "./fragment.tsx"
|
|
4
|
+
import type {
|
|
5
|
+
RouterDocument as RouterDocumentComponent,
|
|
6
|
+
RouterDocumentProps,
|
|
7
|
+
} from "./octane.tsx"
|
|
3
8
|
|
|
4
9
|
/** Canonical router root shared by Flamefront's Octane server and client. */
|
|
5
|
-
export const RouterDocument =
|
|
10
|
+
export const RouterDocument: RouterDocumentComponent = (
|
|
11
|
+
props: RouterDocumentProps,
|
|
12
|
+
) => {
|
|
13
|
+
const Router = RouterProvider as unknown as (props: {
|
|
14
|
+
readonly router: unknown
|
|
15
|
+
}) => unknown
|
|
16
|
+
|
|
17
|
+
return createElement(
|
|
18
|
+
routeOutletHtmlContext.Provider,
|
|
19
|
+
{ value: props.outletHtml ?? null },
|
|
20
|
+
createElement(Router, { router: props.router }),
|
|
21
|
+
)
|
|
22
|
+
}
|