@octanejs/docusaurus 0.0.3 → 0.0.5

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 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–3 are implemented here: headless loading, manifest/Vite integration,
98
- and MDX compilation. Client routing, static generation, hydration, and an
99
- Octane classic theme are deliberately left to the renderer/theme phases; the
100
- CLI therefore does not present `start` or `build` as working commands yet.
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",
3
+ "version": "0.0.5",
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.16"
74
+ "@octanejs/mdx": "0.1.18",
75
+ "@octanejs/remix-router": "0.1.17",
76
+ "@octanejs/seo": "0.0.6"
55
77
  },
56
78
  "peerDependencies": {
57
79
  "@docusaurus/core": "3.10.1",
58
80
  "vite": ">=7.0.0",
59
- "octane": "0.1.19"
81
+ "octane": "0.1.21"
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.19"
94
+ "octane": "0.1.21"
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, hydration, and theme commands arrive with the renderer phases.
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
+ }
@@ -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, '&lt;').replace(/>/g, '&gt;') : 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
+ }