@octanejs/docusaurus 0.0.3 → 0.0.4
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/README.md +143 -5
- package/package.json +26 -4
- package/src/bin.js +1 -1
- package/src/client-components.tsrx +134 -0
- package/src/client.js +18 -0
- package/src/document.js +153 -0
- package/src/hydrate.js +52 -0
- package/src/manifest.js +89 -8
- package/src/route-modules.js +56 -0
- package/src/routes.js +133 -0
- package/src/server.js +64 -0
- package/src/theme/DocCategoryGeneratedIndexPage.js +1 -0
- package/src/theme/DocItem.js +1 -0
- package/src/theme/DocRoot.js +1 -0
- package/src/theme/DocTagDocListPage.js +1 -0
- package/src/theme/DocTagsListPage.js +1 -0
- package/src/theme/DocVersionRoot.js +1 -0
- package/src/theme/DocsRoot.js +1 -0
- package/src/theme/Root.js +1 -0
- package/src/theme/styles.css +183 -0
- package/src/theme-components.tsrx +462 -0
- package/src/theme.js +25 -0
- package/src/vite.js +50 -2
- package/types/client.d.ts +58 -0
- package/types/hydrate.d.ts +21 -0
- package/types/index.d.ts +63 -4
- package/types/server.d.ts +80 -0
- package/types/theme.d.ts +8 -0
- package/types/virtual.d.ts +10 -0
- package/types/vite.d.ts +12 -0
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ The manifest contains:
|
|
|
39
39
|
|
|
40
40
|
- nested routes with `component`, `modules`, `props`, and plugin context;
|
|
41
41
|
- generated global data and per-document metadata;
|
|
42
|
+
- serializable site config, locale/document attributes, and plugin HTML tags;
|
|
43
|
+
- client modules discovered through the Docusaurus plugin lifecycle;
|
|
42
44
|
- `@site`, `@generated`, `~docs`, `@theme`, `@theme-original`, and
|
|
43
45
|
`@theme-init` resolution;
|
|
44
46
|
- the exact Docusaurus version and route-path inventory.
|
|
@@ -64,7 +66,141 @@ export default defineConfig({
|
|
|
64
66
|
|
|
65
67
|
The bridge publishes `virtual:octane-docusaurus-manifest` and resolves
|
|
66
68
|
Docusaurus aliases. Its MDX plugin chooses Octane client/server compilation per
|
|
67
|
-
Vite environment and injects metadata discovered by the content plugins.
|
|
69
|
+
Vite environment and injects metadata discovered by the content plugins. The
|
|
70
|
+
route virtual module also imports plugin client modules, so theme CSS and other
|
|
71
|
+
side-effect assets participate in the client and SSR build graphs.
|
|
72
|
+
|
|
73
|
+
## Octane classic theme
|
|
74
|
+
|
|
75
|
+
Add the first-party Octane theme beside the content plugins in
|
|
76
|
+
`docusaurus.config.mjs`:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import octaneClassicTheme from '@octanejs/docusaurus/theme';
|
|
80
|
+
|
|
81
|
+
export default {
|
|
82
|
+
title: 'My documentation',
|
|
83
|
+
url: 'https://docs.example.com',
|
|
84
|
+
baseUrl: '/',
|
|
85
|
+
themes: [octaneClassicTheme],
|
|
86
|
+
plugins: ['@docusaurus/plugin-content-docs'],
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The theme supplies Octane-native `DocsRoot`, `DocVersionRoot`, `DocRoot`,
|
|
91
|
+
`DocItem`, category-index, and tag-page route modules. Its initial classic shell
|
|
92
|
+
renders configured navbar/footer links, recursive documentation sidebars,
|
|
93
|
+
document metadata, canonical links, previous/next navigation, and responsive
|
|
94
|
+
CSS. React-authored theme modules and swizzles still need Octane equivalents.
|
|
95
|
+
|
|
96
|
+
## Client routing
|
|
97
|
+
|
|
98
|
+
The route virtual module turns every component, content module, generated data
|
|
99
|
+
module, and route-context module into a static dynamic import. Only the matched
|
|
100
|
+
branch loads in the browser:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createDocusaurusBrowserRouter } from '@octanejs/docusaurus/client';
|
|
104
|
+
import {
|
|
105
|
+
manifest,
|
|
106
|
+
routeModules,
|
|
107
|
+
} from 'virtual:octane-docusaurus-routes';
|
|
108
|
+
|
|
109
|
+
export const router = createDocusaurusBrowserRouter(manifest, routeModules);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Render it from an Octane component:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { DocusaurusRouterProvider } from '@octanejs/docusaurus/client';
|
|
116
|
+
import { manifest } from 'virtual:octane-docusaurus-routes';
|
|
117
|
+
import { router } from './router';
|
|
118
|
+
|
|
119
|
+
export function App() @{
|
|
120
|
+
<DocusaurusRouterProvider manifest={manifest} router={router} />
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Nested Docusaurus routes render through `children`; route components also
|
|
125
|
+
receive loaded `modules`, static `props`, `route`, `location`, `params`, and
|
|
126
|
+
`navigate`. `useDocusaurusRouteContext()` exposes the inherited plugin identity
|
|
127
|
+
and merged route data. Use `Link` from `@octanejs/remix-router` for client-side
|
|
128
|
+
navigation.
|
|
129
|
+
|
|
130
|
+
## Static rendering and hydration
|
|
131
|
+
|
|
132
|
+
The server entry resolves the requested lazy route branch through Remix's
|
|
133
|
+
static handler, then prerenders fully resolved Octane markup. Use the route API
|
|
134
|
+
when a host owns the outer HTML, or the document API to compose Docusaurus
|
|
135
|
+
plugin tags, metadata, scoped CSS, build assets, and the hydration entry into a
|
|
136
|
+
complete page:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { prerenderDocusaurusDocument } from '@octanejs/docusaurus/server';
|
|
140
|
+
import {
|
|
141
|
+
manifest,
|
|
142
|
+
routeModules,
|
|
143
|
+
} from 'virtual:octane-docusaurus-routes';
|
|
144
|
+
|
|
145
|
+
const rendered = await prerenderDocusaurusDocument(
|
|
146
|
+
new Request('https://docs.example.com/guide/intro'),
|
|
147
|
+
manifest,
|
|
148
|
+
routeModules,
|
|
149
|
+
{
|
|
150
|
+
document: {
|
|
151
|
+
assets: {
|
|
152
|
+
stylesheets: ['assets/site.css'],
|
|
153
|
+
modulePreloads: ['assets/intro.js'],
|
|
154
|
+
},
|
|
155
|
+
hydrate: 'assets/hydrate.js',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (rendered instanceof Response) {
|
|
161
|
+
return rendered;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { html, bodyHtml, head, css, context } = rendered;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`html` is the complete `<!DOCTYPE html>` document. `bodyHtml` remains the
|
|
168
|
+
prerendered router root for integrations that need both forms. Relative asset
|
|
169
|
+
paths resolve against `manifest.baseUrl`; URL attributes are escaped, duplicate
|
|
170
|
+
asset entries are removed without reordering, and `nonce` from the render
|
|
171
|
+
options is carried onto module scripts. Docusaurus `injectHtmlTags` output is
|
|
172
|
+
trusted site configuration and retains upstream ordering around the
|
|
173
|
+
`#__docusaurus` root.
|
|
174
|
+
|
|
175
|
+
`prerenderDocusaurusRoute()` remains available and returns hoisted metadata in
|
|
176
|
+
`head` so an existing static-site host can place it in its own document head.
|
|
177
|
+
`context.statusCode`, `loaderHeaders`, and `actionHeaders` preserve the static
|
|
178
|
+
router result for the surrounding build or request handler. Generate a site by
|
|
179
|
+
calling this function for the paths in `manifest.routesPaths`; each render
|
|
180
|
+
imports only its matched route branch.
|
|
181
|
+
|
|
182
|
+
Hydrate the same root after the browser receives that markup:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { hydrateDocusaurusRoot } from '@octanejs/docusaurus/hydrate';
|
|
186
|
+
import {
|
|
187
|
+
manifest,
|
|
188
|
+
routeModules,
|
|
189
|
+
} from 'virtual:octane-docusaurus-routes';
|
|
190
|
+
|
|
191
|
+
const container = document.getElementById('__docusaurus');
|
|
192
|
+
if (container === null) throw new Error('Missing Docusaurus root.');
|
|
193
|
+
|
|
194
|
+
const { root, router } = await hydrateDocusaurusRoot(
|
|
195
|
+
container,
|
|
196
|
+
manifest,
|
|
197
|
+
routeModules,
|
|
198
|
+
);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Hydration capture starts before lazy imports, waits for the initial matched
|
|
202
|
+
branch, and then adopts the prerendered nodes. Dispose both returned owners when
|
|
203
|
+
the application is torn down with `root.unmount()` and `router.dispose()`.
|
|
68
204
|
|
|
69
205
|
## Docusaurus-aware MDX
|
|
70
206
|
|
|
@@ -94,7 +230,9 @@ remain composable.
|
|
|
94
230
|
|
|
95
231
|
## Current scope
|
|
96
232
|
|
|
97
|
-
Phases 1–
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
233
|
+
Phases 1–6 are implemented here: headless loading, manifest/Vite integration,
|
|
234
|
+
MDX compilation, lazy client routing, static route rendering, hydration,
|
|
235
|
+
document/asset orchestration, and the initial Octane classic documentation
|
|
236
|
+
theme. A complete `start`/`build` CLI workflow and broader classic-theme feature
|
|
237
|
+
parity remain later phases, so the CLI does not present those commands as
|
|
238
|
+
working yet.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/docusaurus",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Docusaurus content and MDX integration for Octane",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,6 +42,26 @@
|
|
|
42
42
|
"import": "./src/mdx.js",
|
|
43
43
|
"default": "./src/mdx.js"
|
|
44
44
|
},
|
|
45
|
+
"./client": {
|
|
46
|
+
"types": "./types/client.d.ts",
|
|
47
|
+
"import": "./src/client.js",
|
|
48
|
+
"default": "./src/client.js"
|
|
49
|
+
},
|
|
50
|
+
"./hydrate": {
|
|
51
|
+
"types": "./types/hydrate.d.ts",
|
|
52
|
+
"import": "./src/hydrate.js",
|
|
53
|
+
"default": "./src/hydrate.js"
|
|
54
|
+
},
|
|
55
|
+
"./server": {
|
|
56
|
+
"types": "./types/server.d.ts",
|
|
57
|
+
"import": "./src/server.js",
|
|
58
|
+
"default": "./src/server.js"
|
|
59
|
+
},
|
|
60
|
+
"./theme": {
|
|
61
|
+
"types": "./types/theme.d.ts",
|
|
62
|
+
"import": "./src/theme.js",
|
|
63
|
+
"default": "./src/theme.js"
|
|
64
|
+
},
|
|
45
65
|
"./vite": {
|
|
46
66
|
"types": "./types/vite.d.ts",
|
|
47
67
|
"import": "./src/vite.js",
|
|
@@ -51,12 +71,14 @@
|
|
|
51
71
|
},
|
|
52
72
|
"dependencies": {
|
|
53
73
|
"github-slugger": "1.5.0",
|
|
54
|
-
"@octanejs/mdx": "0.1.
|
|
74
|
+
"@octanejs/mdx": "0.1.17",
|
|
75
|
+
"@octanejs/remix-router": "0.1.16",
|
|
76
|
+
"@octanejs/seo": "0.0.5"
|
|
55
77
|
},
|
|
56
78
|
"peerDependencies": {
|
|
57
79
|
"@docusaurus/core": "3.10.1",
|
|
58
80
|
"vite": ">=7.0.0",
|
|
59
|
-
"octane": "0.1.
|
|
81
|
+
"octane": "0.1.20"
|
|
60
82
|
},
|
|
61
83
|
"peerDependenciesMeta": {
|
|
62
84
|
"vite": {
|
|
@@ -69,7 +91,7 @@
|
|
|
69
91
|
"@types/node": "^24.13.3",
|
|
70
92
|
"vite": "^8.1.5",
|
|
71
93
|
"vitest": "^4.1.10",
|
|
72
|
-
"octane": "0.1.
|
|
94
|
+
"octane": "0.1.20"
|
|
73
95
|
},
|
|
74
96
|
"scripts": {
|
|
75
97
|
"test": "cd ../.. && vitest run --project docusaurus"
|
package/src/bin.js
CHANGED
|
@@ -10,7 +10,7 @@ function usage() {
|
|
|
10
10
|
octane-docusaurus clear [--site-dir DIR]
|
|
11
11
|
|
|
12
12
|
The phase 1-3 command inspects Docusaurus's headless route/data graph. Static
|
|
13
|
-
site build
|
|
13
|
+
site build and theme commands arrive with the document/theme phase.
|
|
14
14
|
`;
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createContext, useContext, type ComponentBody, type OctaneNode } from 'octane';
|
|
2
|
+
import {
|
|
3
|
+
Outlet,
|
|
4
|
+
RouterProvider,
|
|
5
|
+
type DataRouter,
|
|
6
|
+
useLocation,
|
|
7
|
+
useNavigate,
|
|
8
|
+
useParams,
|
|
9
|
+
} from '@octanejs/remix-router';
|
|
10
|
+
import { Head, Meta, Seo } from '@octanejs/seo';
|
|
11
|
+
|
|
12
|
+
type DocusaurusPluginIdentifier = {
|
|
13
|
+
name: string;
|
|
14
|
+
id: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type DocusaurusRouteContextValue = {
|
|
18
|
+
plugin: DocusaurusPluginIdentifier;
|
|
19
|
+
data: Record<string, unknown>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type DocusaurusManifestValue = {
|
|
23
|
+
baseUrl: string;
|
|
24
|
+
site: {
|
|
25
|
+
title: string;
|
|
26
|
+
url: string;
|
|
27
|
+
favicon?: string;
|
|
28
|
+
noIndex: boolean;
|
|
29
|
+
themeConfig: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type DocusaurusRoute = {
|
|
34
|
+
children: DocusaurusRoute[];
|
|
35
|
+
plugin?: DocusaurusPluginIdentifier;
|
|
36
|
+
props?: Record<string, unknown>;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const ManifestContext = createContext<DocusaurusManifestValue | null>(null);
|
|
41
|
+
const RouteContext = createContext<DocusaurusRouteContextValue | null>(null);
|
|
42
|
+
|
|
43
|
+
function DocusaurusSiteMetadata(props: { manifest: DocusaurusManifestValue }) @{
|
|
44
|
+
const site = props.manifest.site;
|
|
45
|
+
<>
|
|
46
|
+
<Seo
|
|
47
|
+
title={site.title}
|
|
48
|
+
openGraph={{ title: site.title }}
|
|
49
|
+
robots={site.noIndex ? { index: false, follow: false } : undefined}
|
|
50
|
+
/>
|
|
51
|
+
<Meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
52
|
+
</>
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mergeRouteContext(
|
|
56
|
+
parent: DocusaurusRouteContextValue | null,
|
|
57
|
+
route: DocusaurusRoute,
|
|
58
|
+
resolved: Record<string, unknown> | undefined,
|
|
59
|
+
): DocusaurusRouteContextValue {
|
|
60
|
+
const { plugin: resolvedPlugin, data: explicitData, ...ownData } = resolved ?? {};
|
|
61
|
+
const ownPlugin = resolvedPlugin as DocusaurusPluginIdentifier | undefined;
|
|
62
|
+
const plugin = parent?.plugin ?? ownPlugin ?? route.plugin;
|
|
63
|
+
if (plugin === undefined) {
|
|
64
|
+
throw new Error(`[@octanejs/docusaurus] Route ${String(
|
|
65
|
+
route.path,
|
|
66
|
+
)} has no Docusaurus plugin context.`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
plugin,
|
|
70
|
+
data: {
|
|
71
|
+
...parent?.data,
|
|
72
|
+
...ownData,
|
|
73
|
+
...(explicitData !== null && typeof explicitData === 'object'
|
|
74
|
+
? explicitData as Record<string, unknown>
|
|
75
|
+
: {}),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function DocusaurusRouterProvider(props: {
|
|
81
|
+
manifest: DocusaurusManifestValue;
|
|
82
|
+
router: DataRouter;
|
|
83
|
+
}) @{
|
|
84
|
+
<ManifestContext.Provider value={props.manifest}>
|
|
85
|
+
<Head>
|
|
86
|
+
<DocusaurusSiteMetadata manifest={props.manifest} />
|
|
87
|
+
<RouterProvider router={props.router} />
|
|
88
|
+
</Head>
|
|
89
|
+
</ManifestContext.Provider>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function DocusaurusRouteRenderer(props: {
|
|
93
|
+
Component: ComponentBody<Record<string, unknown>>;
|
|
94
|
+
context?: Record<string, unknown>;
|
|
95
|
+
modules: Record<string, unknown>;
|
|
96
|
+
route: DocusaurusRoute;
|
|
97
|
+
}) @{
|
|
98
|
+
const parentContext = useContext(RouteContext);
|
|
99
|
+
const location = useLocation();
|
|
100
|
+
const navigate = useNavigate();
|
|
101
|
+
const params = useParams();
|
|
102
|
+
const context = mergeRouteContext(parentContext, props.route, props.context);
|
|
103
|
+
const route = { ...props.route, routes: props.route.children };
|
|
104
|
+
const Component = props.Component;
|
|
105
|
+
|
|
106
|
+
<RouteContext.Provider value={context}>
|
|
107
|
+
<Component
|
|
108
|
+
{...props.modules}
|
|
109
|
+
{...props.route.props}
|
|
110
|
+
route={route}
|
|
111
|
+
location={location}
|
|
112
|
+
params={params}
|
|
113
|
+
navigate={navigate}
|
|
114
|
+
>
|
|
115
|
+
<Outlet />
|
|
116
|
+
</Component>
|
|
117
|
+
</RouteContext.Provider>
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function useDocusaurusManifest(): DocusaurusManifestValue {
|
|
121
|
+
const manifest = useContext(ManifestContext);
|
|
122
|
+
if (manifest === null) {
|
|
123
|
+
throw new Error('[@octanejs/docusaurus] useDocusaurusManifest() requires DocusaurusRouterProvider.');
|
|
124
|
+
}
|
|
125
|
+
return manifest;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function useDocusaurusRouteContext(): DocusaurusRouteContextValue {
|
|
129
|
+
const context = useContext(RouteContext);
|
|
130
|
+
if (context === null) {
|
|
131
|
+
throw new Error('[@octanejs/docusaurus] useDocusaurusRouteContext() requires a matched Docusaurus route.');
|
|
132
|
+
}
|
|
133
|
+
return context;
|
|
134
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createBrowserRouter, createMemoryRouter } from '@octanejs/remix-router';
|
|
2
|
+
import {
|
|
3
|
+
DocusaurusRouterProvider,
|
|
4
|
+
useDocusaurusManifest,
|
|
5
|
+
useDocusaurusRouteContext,
|
|
6
|
+
} from './client-components.tsrx';
|
|
7
|
+
import { createDocusaurusRoutes } from './routes.js';
|
|
8
|
+
|
|
9
|
+
export { DocusaurusRouterProvider, useDocusaurusManifest, useDocusaurusRouteContext };
|
|
10
|
+
export { createDocusaurusRoutes };
|
|
11
|
+
|
|
12
|
+
export function createDocusaurusBrowserRouter(manifest, registry, options) {
|
|
13
|
+
return createBrowserRouter(createDocusaurusRoutes(manifest, registry), options);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createDocusaurusMemoryRouter(manifest, registry, options) {
|
|
17
|
+
return createMemoryRouter(createDocusaurusRoutes(manifest, registry), options);
|
|
18
|
+
}
|
package/src/document.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { escapeAttr } from 'octane/server';
|
|
2
|
+
|
|
3
|
+
const ATTRIBUTE_NAME = /^[A-Za-z_:][A-Za-z0-9_.:-]*$/;
|
|
4
|
+
const ABSOLUTE_ASSET = /^(?:[a-z][a-z\d+.-]*:|\/\/|\/|#)/i;
|
|
5
|
+
|
|
6
|
+
function escapeDocumentAttribute(value) {
|
|
7
|
+
const escaped = escapeAttr(value);
|
|
8
|
+
return /[<>]/.test(escaped) ? escaped.replace(/</g, '<').replace(/>/g, '>') : escaped;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function attributeString(attributes) {
|
|
12
|
+
let result = '';
|
|
13
|
+
for (const [name, value] of Object.entries(attributes ?? {})) {
|
|
14
|
+
if (!ATTRIBUTE_NAME.test(name)) {
|
|
15
|
+
throw new TypeError(`Invalid Docusaurus document attribute name: ${JSON.stringify(name)}.`);
|
|
16
|
+
}
|
|
17
|
+
if (value === undefined || value === null || value === false) continue;
|
|
18
|
+
result += value === true ? ` ${name}` : ` ${name}="${escapeDocumentAttribute(String(value))}"`;
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assetSource(asset, name) {
|
|
24
|
+
if (typeof asset === 'string') return asset;
|
|
25
|
+
if (asset !== null && typeof asset === 'object' && typeof asset[name] === 'string') {
|
|
26
|
+
return asset[name];
|
|
27
|
+
}
|
|
28
|
+
throw new TypeError(`Expected a Docusaurus ${name} asset.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function assetUrl(baseUrl, value) {
|
|
32
|
+
if (ABSOLUTE_ASSET.test(value)) return value;
|
|
33
|
+
return `${baseUrl}${value.replace(/^\.\//, '')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assetAttributes(asset, sourceName, baseUrl, baseAttributes) {
|
|
37
|
+
if (typeof asset === 'string') {
|
|
38
|
+
return {
|
|
39
|
+
...baseAttributes,
|
|
40
|
+
[sourceName]: assetUrl(baseUrl, asset),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const { integrity, crossOrigin, referrerPolicy, media, async, defer, type } = asset;
|
|
44
|
+
return {
|
|
45
|
+
...baseAttributes,
|
|
46
|
+
[sourceName]: assetUrl(baseUrl, assetSource(asset, sourceName)),
|
|
47
|
+
...(integrity === undefined ? {} : { integrity }),
|
|
48
|
+
...(crossOrigin === undefined ? {} : { crossorigin: crossOrigin }),
|
|
49
|
+
...(referrerPolicy === undefined ? {} : { referrerpolicy: referrerPolicy }),
|
|
50
|
+
...(media === undefined ? {} : { media }),
|
|
51
|
+
...(async === undefined ? {} : { async }),
|
|
52
|
+
...(defer === undefined ? {} : { defer }),
|
|
53
|
+
...(type === undefined ? {} : { type }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function uniqueAssets(assets, sourceName) {
|
|
58
|
+
const result = [];
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
for (const asset of assets ?? []) {
|
|
61
|
+
const source = assetSource(asset, sourceName);
|
|
62
|
+
if (seen.has(source)) continue;
|
|
63
|
+
seen.add(source);
|
|
64
|
+
result.push(asset);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderAssets(manifest, assets, nonce) {
|
|
70
|
+
const stylesheets = uniqueAssets(assets?.stylesheets, 'href').map((asset) => {
|
|
71
|
+
const attributes = assetAttributes(asset, 'href', manifest.baseUrl, {
|
|
72
|
+
rel: 'stylesheet',
|
|
73
|
+
});
|
|
74
|
+
return `<link${attributeString(attributes)}>`;
|
|
75
|
+
});
|
|
76
|
+
const modulePreloads = uniqueAssets(assets?.modulePreloads, 'href').map((asset) => {
|
|
77
|
+
const attributes = assetAttributes(asset, 'href', manifest.baseUrl, {
|
|
78
|
+
rel: 'modulepreload',
|
|
79
|
+
});
|
|
80
|
+
return `<link${attributeString(attributes)}>`;
|
|
81
|
+
});
|
|
82
|
+
const scripts = uniqueAssets(assets?.scripts, 'src').map((asset) => {
|
|
83
|
+
const attributes = assetAttributes(asset, 'src', manifest.baseUrl, {
|
|
84
|
+
type: 'module',
|
|
85
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
86
|
+
});
|
|
87
|
+
return `<script${attributeString(attributes)}></script>`;
|
|
88
|
+
});
|
|
89
|
+
return [...stylesheets, ...modulePreloads, ...scripts];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderHydrationEntry(manifest, hydrate, nonce) {
|
|
93
|
+
if (hydrate === undefined) return '';
|
|
94
|
+
const attributes = assetAttributes(hydrate, 'src', manifest.baseUrl, {
|
|
95
|
+
type: 'module',
|
|
96
|
+
'data-octane-hydrate': true,
|
|
97
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
98
|
+
});
|
|
99
|
+
return `<script${attributeString(attributes)}></script>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function renderFavicon(manifest) {
|
|
103
|
+
if (manifest.site.favicon === undefined) return '';
|
|
104
|
+
return `<link${attributeString({
|
|
105
|
+
rel: 'icon',
|
|
106
|
+
href: assetUrl(manifest.baseUrl, manifest.site.favicon),
|
|
107
|
+
})}>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Compose one route render into the real Docusaurus HTML document.
|
|
112
|
+
*
|
|
113
|
+
* Plugin-provided head/body tag strings were produced by Docusaurus's
|
|
114
|
+
* `injectHtmlTags` lifecycle and are intentionally treated as trusted site
|
|
115
|
+
* configuration. Attributes and build asset descriptors supplied here are
|
|
116
|
+
* escaped before insertion.
|
|
117
|
+
*/
|
|
118
|
+
export function renderDocusaurusDocument(rendered, manifest, options = {}) {
|
|
119
|
+
const htmlAttributes = {
|
|
120
|
+
...manifest.document.htmlAttributes,
|
|
121
|
+
...options.htmlAttributes,
|
|
122
|
+
};
|
|
123
|
+
const head = [
|
|
124
|
+
'<meta charset="UTF-8">',
|
|
125
|
+
`<meta name="generator" content="Docusaurus v${escapeDocumentAttribute(
|
|
126
|
+
manifest.docusaurusVersion,
|
|
127
|
+
)}">`,
|
|
128
|
+
renderFavicon(manifest),
|
|
129
|
+
manifest.document.headTags,
|
|
130
|
+
rendered.head,
|
|
131
|
+
rendered.css,
|
|
132
|
+
...renderAssets(manifest, options.assets, options.nonce),
|
|
133
|
+
].filter((value) => value !== '');
|
|
134
|
+
const hydration = renderHydrationEntry(manifest, options.hydrate, options.nonce);
|
|
135
|
+
const body = [
|
|
136
|
+
manifest.document.preBodyTags,
|
|
137
|
+
`<div id="${escapeDocumentAttribute(options.rootId ?? '__docusaurus')}">${rendered.html}</div>`,
|
|
138
|
+
manifest.document.postBodyTags,
|
|
139
|
+
hydration,
|
|
140
|
+
].filter((value) => value !== '');
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
'<!DOCTYPE html>',
|
|
144
|
+
`<html${attributeString(htmlAttributes)}>`,
|
|
145
|
+
'<head>',
|
|
146
|
+
...head,
|
|
147
|
+
'</head>',
|
|
148
|
+
`<body${attributeString(options.bodyAttributes)}>`,
|
|
149
|
+
...body,
|
|
150
|
+
'</body>',
|
|
151
|
+
'</html>',
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
package/src/hydrate.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { hydrateRoot, initializeHydrationEventCapture } from 'octane';
|
|
2
|
+
import { createDocusaurusBrowserRouter, DocusaurusRouterProvider } from './client.js';
|
|
3
|
+
|
|
4
|
+
function abortError(signal) {
|
|
5
|
+
return signal.reason ?? new DOMException('Docusaurus hydration was aborted.', 'AbortError');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function waitForRouter(router, signal) {
|
|
9
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
10
|
+
if (router.state.initialized) return Promise.resolve();
|
|
11
|
+
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
let unsubscribe = () => {};
|
|
15
|
+
|
|
16
|
+
const finish = (error) => {
|
|
17
|
+
if (settled) return;
|
|
18
|
+
settled = true;
|
|
19
|
+
unsubscribe();
|
|
20
|
+
signal?.removeEventListener('abort', onAbort);
|
|
21
|
+
if (error === undefined) resolve();
|
|
22
|
+
else reject(error);
|
|
23
|
+
};
|
|
24
|
+
const onAbort = () => finish(abortError(signal));
|
|
25
|
+
|
|
26
|
+
unsubscribe = router.subscribe((state) => {
|
|
27
|
+
if (state.initialized) finish();
|
|
28
|
+
});
|
|
29
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
30
|
+
if (router.state.initialized) finish();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function hydrateDocusaurusRoot(container, manifest, registry, options = {}) {
|
|
35
|
+
const { identifierPrefix, signal, ...routerOptions } = options;
|
|
36
|
+
initializeHydrationEventCapture(container.ownerDocument);
|
|
37
|
+
|
|
38
|
+
const router = createDocusaurusBrowserRouter(manifest, registry, routerOptions);
|
|
39
|
+
try {
|
|
40
|
+
await waitForRouter(router, signal);
|
|
41
|
+
const root = hydrateRoot(
|
|
42
|
+
container,
|
|
43
|
+
DocusaurusRouterProvider,
|
|
44
|
+
{ manifest, router },
|
|
45
|
+
{ identifierPrefix },
|
|
46
|
+
);
|
|
47
|
+
return { root, router };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
router.dispose();
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|