@octanejs/app-core 0.0.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 +21 -0
- package/README.md +34 -0
- package/package.json +88 -0
- package/src/codegen.js +297 -0
- package/src/config-loader.js +307 -0
- package/src/config.js +14 -0
- package/src/constants.js +5 -0
- package/src/html.js +10 -0
- package/src/index.js +18 -0
- package/src/middleware.js +3 -0
- package/src/resolve-config.js +177 -0
- package/src/routes.js +135 -0
- package/src/server/component-wrappers.js +99 -0
- package/src/server/html-stream.js +74 -0
- package/src/server/html-template.js +136 -0
- package/src/server/middleware.js +127 -0
- package/src/server/node-http.js +262 -0
- package/src/server/production.js +335 -0
- package/src/server/router.js +123 -0
- package/src/server/server-entry.js +436 -0
- package/src/server/server-route.js +47 -0
- package/types/codegen.d.ts +67 -0
- package/types/config-loader.d.ts +26 -0
- package/types/config.d.ts +26 -0
- package/types/html.d.ts +14 -0
- package/types/index.d.ts +319 -0
- package/types/middleware.d.ts +3 -0
- package/types/node.d.ts +31 -0
- package/types/production.d.ts +98 -0
- package/types/routes.d.ts +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# `@octanejs/app-core`
|
|
2
|
+
|
|
3
|
+
Bundler-neutral app primitives shared by Octane's Vite and Rsbuild integrations. Most applications should install the integration for their build tool and import `defineConfig`, routes, and middleware from that package; integration and adapter authors can depend on app-core directly.
|
|
4
|
+
|
|
5
|
+
## Public surfaces
|
|
6
|
+
|
|
7
|
+
- `@octanejs/app-core/config` validates and resolves declarative `octane.config.ts` values.
|
|
8
|
+
- `@octanejs/app-core/config-loader` loads TypeScript config files with either an injected development module runner or a neutral esbuild evaluator. `loadOctaneConfigWithMetadata` reports existing and missing dependencies for watch and cache invalidation.
|
|
9
|
+
- `@octanejs/app-core/routes` and `/middleware` provide the router and Fetch API request pipeline.
|
|
10
|
+
- `@octanejs/app-core/html`, `/production`, and `/node` provide streaming HTML composition, the production Fetch handler, and the optional Node HTTP bridge.
|
|
11
|
+
- `@octanejs/app-core/codegen` generates client hydration entries, template-free server manifests, and production server entries. Runtime module IDs, integration module IDs, generated-file directories, and application import specifiers are explicit inputs so generators remain usable across bundlers and renderer targets.
|
|
12
|
+
|
|
13
|
+
Development integrations should inject their native module runner into `loadOctaneConfigWithMetadata`. The neutral evaluator is intended for configuration/build discovery: it bundles static JS, TS, and server-compiled TSRX helpers, while preserving dynamic application imports as lazy watched references so config loading never traverses renderer-only asset graphs.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import {
|
|
17
|
+
create_client_entry_source,
|
|
18
|
+
generateServerManifestEntry,
|
|
19
|
+
} from '@octanejs/app-core/codegen';
|
|
20
|
+
|
|
21
|
+
const clientEntry = create_client_entry_source({
|
|
22
|
+
staticEntries: [{ id: '/src/Page.tsrx', specifier: '/absolute/app/src/Page.tsrx' }],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const serverEntry = generateServerManifestEntry({
|
|
26
|
+
routes,
|
|
27
|
+
octaneConfigPath: '/absolute/app/octane.config.ts',
|
|
28
|
+
moduleImports: {
|
|
29
|
+
'/src/Page.tsrx': '/absolute/app/src/Page.tsrx',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Stable application IDs stay in route manifests and hydration data; only generated `import` specifiers are mapped to a bundler-resolvable path.
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/app-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"description": "Bundler-neutral app runtime, configuration, routing, and code generation for Octane integrations",
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "Dominic Gannaway",
|
|
12
|
+
"email": "dg@domgan.com"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
20
|
+
"directory": "packages/app-core"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"types",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"module": "src/index.js",
|
|
29
|
+
"main": "src/index.js",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./types/index.d.ts",
|
|
33
|
+
"import": "./src/index.js",
|
|
34
|
+
"default": "./src/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./config": {
|
|
37
|
+
"types": "./types/config.d.ts",
|
|
38
|
+
"import": "./src/config.js",
|
|
39
|
+
"default": "./src/config.js"
|
|
40
|
+
},
|
|
41
|
+
"./config-loader": {
|
|
42
|
+
"types": "./types/config-loader.d.ts",
|
|
43
|
+
"import": "./src/config-loader.js",
|
|
44
|
+
"default": "./src/config-loader.js"
|
|
45
|
+
},
|
|
46
|
+
"./routes": {
|
|
47
|
+
"types": "./types/routes.d.ts",
|
|
48
|
+
"import": "./src/routes.js",
|
|
49
|
+
"default": "./src/routes.js"
|
|
50
|
+
},
|
|
51
|
+
"./middleware": {
|
|
52
|
+
"types": "./types/middleware.d.ts",
|
|
53
|
+
"import": "./src/middleware.js",
|
|
54
|
+
"default": "./src/middleware.js"
|
|
55
|
+
},
|
|
56
|
+
"./html": {
|
|
57
|
+
"types": "./types/html.d.ts",
|
|
58
|
+
"import": "./src/html.js",
|
|
59
|
+
"default": "./src/html.js"
|
|
60
|
+
},
|
|
61
|
+
"./codegen": {
|
|
62
|
+
"types": "./types/codegen.d.ts",
|
|
63
|
+
"import": "./src/codegen.js",
|
|
64
|
+
"default": "./src/codegen.js"
|
|
65
|
+
},
|
|
66
|
+
"./production": {
|
|
67
|
+
"types": "./types/production.d.ts",
|
|
68
|
+
"import": "./src/server/production.js",
|
|
69
|
+
"default": "./src/server/production.js"
|
|
70
|
+
},
|
|
71
|
+
"./node": {
|
|
72
|
+
"types": "./types/node.d.ts",
|
|
73
|
+
"import": "./src/server/node-http.js",
|
|
74
|
+
"default": "./src/server/node-http.js"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@ripple-ts/adapter": "^0.3.86",
|
|
79
|
+
"esbuild": "^0.28.1"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"octane": "0.1.5"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@types/node": "^24.3.0",
|
|
86
|
+
"octane": "0.1.5"
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/codegen.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export { generateServerEntry, generateServerManifestEntry } from './server/server-entry.js';
|
|
6
|
+
|
|
7
|
+
export const RESOLVED_ADAPTER_BROWSER_STUB_ID = '\0octane:adapter-browser-stub';
|
|
8
|
+
// Server-only deploy/adapter packages: client-side imports of these specifiers
|
|
9
|
+
// resolve to the browser stub below instead of the real module (whose graph
|
|
10
|
+
// pulls node builtins). Every published adapter package MUST be listed here,
|
|
11
|
+
// and its public exports added to the stub.
|
|
12
|
+
export const SERVER_ONLY_ADAPTER_IDS = new Set([
|
|
13
|
+
'@ripple-ts/adapter-node',
|
|
14
|
+
'@ripple-ts/adapter-bun',
|
|
15
|
+
'@ripple-ts/adapter-vercel',
|
|
16
|
+
'@octanejs/adapter-vercel',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/** @type {Map<string, string>} */
|
|
20
|
+
const generated_file_cache = new Map();
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The browser stand-in shared by every SERVER_ONLY_ADAPTER_IDS package — it
|
|
24
|
+
* must export the UNION of their public names, each failing loudly on use
|
|
25
|
+
* (never at import, so merely reaching the module keeps the app alive).
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function create_adapter_browser_stub_source() {
|
|
29
|
+
return `export const runtime = undefined;
|
|
30
|
+
export function serve() {
|
|
31
|
+
throw new Error('[octane] Server adapters cannot run in the browser.');
|
|
32
|
+
}
|
|
33
|
+
export function nodeRequestToWebRequest() {
|
|
34
|
+
throw new Error('[octane] Node request helpers cannot run in the browser.');
|
|
35
|
+
}
|
|
36
|
+
export function webResponseToNodeResponse() {
|
|
37
|
+
throw new Error('[octane] Node response helpers cannot run in the browser.');
|
|
38
|
+
}
|
|
39
|
+
export function vercel() {
|
|
40
|
+
throw new Error('[octane] Deploy adapters cannot run in the browser.');
|
|
41
|
+
}
|
|
42
|
+
export function adapt() {
|
|
43
|
+
throw new Error('[octane] Deploy adapters cannot run in the browser.');
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the directory used for generated project entries. Integrations may
|
|
50
|
+
* supply an explicit `generatedDir`; otherwise their cache directory is used.
|
|
51
|
+
* The shape intentionally accepts Vite/Rsbuild resolved configs without
|
|
52
|
+
* importing either package's types.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ root: string, cacheDir?: string, generatedDir?: string }} options
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function get_project_generated_dir(options) {
|
|
58
|
+
if (options.generatedDir) return path.resolve(options.root, options.generatedDir);
|
|
59
|
+
const cacheDir = options.cacheDir ?? path.join(options.root, 'node_modules/.cache/octane');
|
|
60
|
+
return path.join(cacheDir, 'project');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {{ root: string, cacheDir?: string, generatedDir?: string }} options
|
|
65
|
+
* @param {string} name
|
|
66
|
+
* @param {string} source
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
export function write_project_generated_file(options, name, source) {
|
|
70
|
+
const dir = get_project_generated_dir(options);
|
|
71
|
+
const file = path.join(dir, name);
|
|
72
|
+
|
|
73
|
+
if (generated_file_cache.get(file) === source && fs.existsSync(file)) {
|
|
74
|
+
return file;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
78
|
+
if (!fs.existsSync(file) || fs.readFileSync(file, 'utf-8') !== source) {
|
|
79
|
+
fs.writeFileSync(file, source);
|
|
80
|
+
}
|
|
81
|
+
generated_file_cache.set(file, source);
|
|
82
|
+
return file;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Generate the client hydration entry (served at virtual:octane-hydrate).
|
|
87
|
+
*
|
|
88
|
+
* CONFIG-FREE: it does NOT import octane.config.ts. Importing the config into
|
|
89
|
+
* the browser would drag the plugin (and the server adapter) — with their
|
|
90
|
+
* `node:fs` imports — into the client graph and throw at module-eval. Instead
|
|
91
|
+
* the server serializes everything needed into #__octane_data ({ entry,
|
|
92
|
+
* exportName, layout, params, url, preHydrate }), and this entry
|
|
93
|
+
* dynamic-imports the page/layout from there.
|
|
94
|
+
*
|
|
95
|
+
* `staticEntries` (production builds) lists every module path the server can
|
|
96
|
+
* name in #__octane_data — page entries, layouts, and the preHydrate hook.
|
|
97
|
+
* Each becomes a STATIC `() => import('/src/…')` in a lookup map, so Rollup
|
|
98
|
+
* sees, chunks, and hashes them; the runtime falls back to the hidden dynamic
|
|
99
|
+
* import only for paths outside the map (the dev case, where the map is empty
|
|
100
|
+
* and the integration serves any module by URL).
|
|
101
|
+
*
|
|
102
|
+
* octane specifics:
|
|
103
|
+
* - `import { hydrateRoot } from 'octane'` (NO `mount`).
|
|
104
|
+
* - `hydrateRoot(container, body, props)` signature (container FIRST, React-18
|
|
105
|
+
* shape) — no `{ target, props }` wrapper.
|
|
106
|
+
* - The layout `children` is a props-first ComponentBody whose closure calls
|
|
107
|
+
* `Page({ params, url }, scope, extra)`, NOT a 0-arg thunk: octane's
|
|
108
|
+
* `childSlot` invokes a bare function child with `{}` props, so page data
|
|
109
|
+
* rides the closure — mirroring the server `createLayoutWrapper`.
|
|
110
|
+
* - hydrateRoot() itself locates/consumes the <script data-octane-suspense>
|
|
111
|
+
* seed inside #root, so the entry does nothing special for suspense.
|
|
112
|
+
* - `preHydrate` (config `router.preHydrate`, a project-root module ID) is
|
|
113
|
+
* imported and its default export awaited BEFORE hydrateRoot — the hook an
|
|
114
|
+
* app-level client router uses to commit its match tree so the first
|
|
115
|
+
* hydration pass adopts the same resolved tree the server rendered.
|
|
116
|
+
*
|
|
117
|
+
* `getComponentExport` mirrors routes.js `get_component_export` (route named
|
|
118
|
+
* export > default > first PascalCase) so server and client pick the SAME
|
|
119
|
+
* component.
|
|
120
|
+
*
|
|
121
|
+
* @param {{
|
|
122
|
+
* configPath?: string,
|
|
123
|
+
* staticEntries?: Array<string | { id: string, specifier: string }>,
|
|
124
|
+
* resolveImport?: (id: string) => string,
|
|
125
|
+
* runtimeModuleId?: string,
|
|
126
|
+
* generatedBy?: string,
|
|
127
|
+
* }} [options]
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
export function create_client_entry_source(options = {}) {
|
|
131
|
+
const staticEntries = new Map();
|
|
132
|
+
for (const entry of options.staticEntries ?? []) {
|
|
133
|
+
const id = typeof entry === 'string' ? entry : entry.id;
|
|
134
|
+
const specifier =
|
|
135
|
+
typeof entry === 'string' ? (options.resolveImport?.(id) ?? id) : entry.specifier;
|
|
136
|
+
staticEntries.set(id, specifier);
|
|
137
|
+
}
|
|
138
|
+
const runtimeModuleId = options.runtimeModuleId ?? 'octane';
|
|
139
|
+
const generatedBy = options.generatedBy ?? '@octanejs/app-core';
|
|
140
|
+
const static_map_lines = [...staticEntries]
|
|
141
|
+
.map(
|
|
142
|
+
([id, specifier]) => ` ${JSON.stringify(id)}: () => import(${JSON.stringify(specifier)}),`,
|
|
143
|
+
)
|
|
144
|
+
.join('\n');
|
|
145
|
+
|
|
146
|
+
return `// Auto-generated by ${generatedBy}.
|
|
147
|
+
// This file is written to the active integration's project cache.
|
|
148
|
+
|
|
149
|
+
import { hydrateRoot, Suspense, ErrorBoundary, createElement } from ${JSON.stringify(runtimeModuleId)};
|
|
150
|
+
|
|
151
|
+
// Static import map (production): every module the server may name in
|
|
152
|
+
// #__octane_data, as bundle-analyzable dynamic imports. Empty in dev.
|
|
153
|
+
const routeModules = {
|
|
154
|
+
${static_map_lines}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Keep the fallback hidden from static import analysis. Some integrations
|
|
158
|
+
// rewrite variable dynamic imports into queried URLs, which can evaluate as a
|
|
159
|
+
// SECOND browser module instance and stop pages/preHydrate hooks sharing
|
|
160
|
+
// singletons with statically imported copies of the same files.
|
|
161
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
162
|
+
|
|
163
|
+
function importModule(path) {
|
|
164
|
+
const loader = routeModules[path];
|
|
165
|
+
return loader ? loader() : dynamicImport(path);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getComponentExport(module, exportName) {
|
|
169
|
+
// Explicit export name requires an exact match; do NOT fall back, so a
|
|
170
|
+
// typo'd route renders nothing rather than the wrong component.
|
|
171
|
+
if (exportName) return typeof module[exportName] === 'function' ? module[exportName] : undefined;
|
|
172
|
+
if (typeof module.default === 'function') return module.default;
|
|
173
|
+
return Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function withRootBoundary(content, boundary) {
|
|
177
|
+
let body = content;
|
|
178
|
+
if (boundary.pending) {
|
|
179
|
+
const child = body;
|
|
180
|
+
const Pending = boundary.pending;
|
|
181
|
+
body = (props, scope) => Suspense({
|
|
182
|
+
fallback: createElement(Pending, {}),
|
|
183
|
+
children: (_props, childScope) => child(props, childScope),
|
|
184
|
+
}, scope);
|
|
185
|
+
}
|
|
186
|
+
if (boundary.catch) {
|
|
187
|
+
const child = body;
|
|
188
|
+
const Catch = boundary.catch;
|
|
189
|
+
body = (props, scope) => ErrorBoundary({
|
|
190
|
+
fallback: (error, reset) => createElement(Catch, { error, reset }),
|
|
191
|
+
children: (_props, childScope) => child(props, childScope),
|
|
192
|
+
}, scope);
|
|
193
|
+
}
|
|
194
|
+
return body;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
(async () => {
|
|
198
|
+
try {
|
|
199
|
+
const el = document.getElementById('__octane_data');
|
|
200
|
+
const target = document.getElementById('root');
|
|
201
|
+
if (!el || !target) {
|
|
202
|
+
console.error('[octane] Unable to hydrate: missing #__octane_data or #root.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const data = JSON.parse(el.textContent || '{}'); // { entry, exportName, layout, params, url, preHydrate }
|
|
206
|
+
if (!data.entry) {
|
|
207
|
+
console.error('[octane] Unable to hydrate: no route entry in #__octane_data.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const pageMod = await importModule(data.entry);
|
|
212
|
+
const Component = getComponentExport(pageMod, data.exportName ?? undefined);
|
|
213
|
+
if (!Component) {
|
|
214
|
+
console.error('[octane] Unable to hydrate: no component export for', data.entry);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const params = data.params;
|
|
219
|
+
const url = data.url;
|
|
220
|
+
|
|
221
|
+
// Run the app's pre-hydrate hook (config \`router.preHydrate\`) before the
|
|
222
|
+
// first hydration render — e.g. a client router committing its match tree
|
|
223
|
+
// so hydration adopts the same resolved tree the server rendered.
|
|
224
|
+
if (data.preHydrate) {
|
|
225
|
+
const preMod = await importModule(data.preHydrate);
|
|
226
|
+
const hook = preMod.default;
|
|
227
|
+
if (typeof hook === 'function') await hook({ url, params });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Build the same props-closing root wrapper as the server.
|
|
231
|
+
let Content;
|
|
232
|
+
if (data.layout) {
|
|
233
|
+
const layoutMod = await importModule(data.layout);
|
|
234
|
+
const Layout = getComponentExport(layoutMod);
|
|
235
|
+
if (Layout) {
|
|
236
|
+
// children is a ComponentBody closing over the page props; octane's
|
|
237
|
+
// childSlot invokes a function child PROPS-FIRST as \`({}, block, extra)\`,
|
|
238
|
+
// so we ignore the empty props and render the page with its real
|
|
239
|
+
// \`{ params, url }\`, threading the scope + extra — mirroring the server
|
|
240
|
+
// createLayoutWrapper so the markers line up.
|
|
241
|
+
const children = (_props, scope, extra) => Component({ params, url }, scope, extra);
|
|
242
|
+
Content = (_props, scope, extra) => Layout({ params, url, children }, scope, extra);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!Content) {
|
|
246
|
+
Content = (_props, scope, extra) => Component({ params, url }, scope, extra);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const rootBoundary = { pending: null, catch: null };
|
|
250
|
+
for (const kind of ['pending', 'catch']) {
|
|
251
|
+
const entry = data.rootBoundary?.[kind];
|
|
252
|
+
if (!entry) continue;
|
|
253
|
+
const module = await importModule(entry.path);
|
|
254
|
+
const Boundary = getComponentExport(module, entry.exportName ?? undefined);
|
|
255
|
+
if (!Boundary) {
|
|
256
|
+
console.error('[octane] Unable to hydrate: no rootBoundary component for', entry.path);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
rootBoundary[kind] = Boundary;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
hydrateRoot(target, withRootBoundary(Content, rootBoundary));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('[octane] Failed to bootstrap client hydration.', error);
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {string} filename
|
|
272
|
+
* @param {string} root
|
|
273
|
+
* @returns {string}
|
|
274
|
+
*/
|
|
275
|
+
export function normalize_module_reference(filename, root) {
|
|
276
|
+
const normalizedRoot = path.resolve(root);
|
|
277
|
+
const normalizedFile = path.resolve(filename);
|
|
278
|
+
const relative = path.relative(normalizedRoot, normalizedFile);
|
|
279
|
+
const withinRoot =
|
|
280
|
+
relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
281
|
+
return withinRoot
|
|
282
|
+
? `/${relative.split(path.sep).join('/')}`
|
|
283
|
+
: normalizedFile.split(path.sep).join('/');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Compatibility alias for the Vite integration. Module references themselves
|
|
288
|
+
* are bundler-neutral; a project-root absolute `/src/...` ID is understood by
|
|
289
|
+
* both Vite and Rsbuild/Rspack aliases.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} filename
|
|
292
|
+
* @param {string} root
|
|
293
|
+
* @returns {string}
|
|
294
|
+
*/
|
|
295
|
+
export function to_vite_root_import(filename, root) {
|
|
296
|
+
return normalize_module_reference(filename, root);
|
|
297
|
+
}
|