@octanejs/app-core 0.1.9 → 0.2.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/package.json +3 -3
- package/src/client-entry-runtime.js +7 -1
- package/src/codegen.js +77 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octanejs/app-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"esbuild": "^0.28.1"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
|
-
"octane": "^0.
|
|
82
|
+
"octane": "^0.16.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/node": "^24.13.3",
|
|
86
|
-
"octane": "0.
|
|
86
|
+
"octane": "0.16.0"
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
hydrateRoot,
|
|
4
|
+
__hydrateVoidRoot,
|
|
5
|
+
initializeHydrationEventCapture,
|
|
6
|
+
Suspense,
|
|
7
|
+
ErrorBoundary,
|
|
8
|
+
} from 'octane';
|
|
3
9
|
import { createElementFromConfig } from 'octane/internal/client';
|
|
4
10
|
|
|
5
11
|
/**
|
package/src/codegen.js
CHANGED
|
@@ -147,6 +147,18 @@ let lifecycleBootstrap;
|
|
|
147
147
|
let independentBootstrap;
|
|
148
148
|
let disposeIndependentHydration;
|
|
149
149
|
let incompatibleBuild = false;
|
|
150
|
+
${
|
|
151
|
+
options.streamedSignals === false
|
|
152
|
+
? ''
|
|
153
|
+
: `// Destructured so a bundler keeps only these two exports. The router never
|
|
154
|
+
// registers a streamed HTML region, so it joins results without placement.
|
|
155
|
+
const loadSignalHydration = () =>
|
|
156
|
+
import('octane/hydration/streamed-signals').then(
|
|
157
|
+
({ bootstrapStreamedSignalResults, installSignalDocumentLifecycle }) =>
|
|
158
|
+
({ bootstrapStreamedSignalResults, installSignalDocumentLifecycle }),
|
|
159
|
+
);
|
|
160
|
+
`
|
|
161
|
+
}
|
|
150
162
|
${
|
|
151
163
|
options.devClientBuild
|
|
152
164
|
? `
|
|
@@ -209,7 +221,12 @@ function importModule(path) {
|
|
|
209
221
|
return loader ? loader() : dynamicImport(path);
|
|
210
222
|
}
|
|
211
223
|
|
|
212
|
-
|
|
224
|
+
${
|
|
225
|
+
options.independentHydration === false
|
|
226
|
+
? `// This build compiled no independent islands.
|
|
227
|
+
async function bootstrapIndependentIslands() {}
|
|
228
|
+
`
|
|
229
|
+
: `const independentStyles = new Map();
|
|
213
230
|
|
|
214
231
|
function clientAssetPath(path) {
|
|
215
232
|
return path.startsWith('/') ? path : '/' + path;
|
|
@@ -280,12 +297,18 @@ async function bootstrapIndependentIslands(target) {
|
|
|
280
297
|
});
|
|
281
298
|
return independentBootstrap;
|
|
282
299
|
}
|
|
283
|
-
|
|
284
|
-
|
|
300
|
+
`
|
|
301
|
+
}
|
|
302
|
+
${
|
|
303
|
+
options.streamedSignals === false
|
|
304
|
+
? `// This build compiled no streamed signals.
|
|
305
|
+
async function bootstrapDocumentLifecycle() {}
|
|
306
|
+
`
|
|
307
|
+
: `async function bootstrapDocumentLifecycle() {
|
|
285
308
|
if (clientBuildId === null || !documentData?.streamedSignals) return;
|
|
286
309
|
if (lifecycleBootstrap) return lifecycleBootstrap;
|
|
287
310
|
lifecycleBootstrap = (async () => {
|
|
288
|
-
signalHydrationModule ??= await
|
|
311
|
+
signalHydrationModule ??= await loadSignalHydration();
|
|
289
312
|
if (incompatibleBuild) return;
|
|
290
313
|
documentLifecycle = signalHydrationModule.installSignalDocumentLifecycle({
|
|
291
314
|
document, buildId: clientBuildId, documentId: documentData.streamedSignals.documentId,
|
|
@@ -298,7 +321,8 @@ async function bootstrapDocumentLifecycle() {
|
|
|
298
321
|
})();
|
|
299
322
|
return lifecycleBootstrap;
|
|
300
323
|
}
|
|
301
|
-
|
|
324
|
+
`
|
|
325
|
+
}
|
|
302
326
|
`;
|
|
303
327
|
}
|
|
304
328
|
|
|
@@ -345,6 +369,15 @@ async function bootstrapDocumentLifecycle() {
|
|
|
345
369
|
* app-level client router uses to commit its match tree so the first
|
|
346
370
|
* hydration pass adopts the same resolved tree the server rendered.
|
|
347
371
|
*
|
|
372
|
+
* A build that knows its whole client graph narrows the entry to it. Each flag
|
|
373
|
+
* defaults to the general entry, and `false` drops that feature's code:
|
|
374
|
+
* - `rootBoundary`: the config names a root pending or catch boundary.
|
|
375
|
+
* - `independentHydration` / `streamedSignals`: some compiled module has an
|
|
376
|
+
* independent island / streamed signal state, so its lazy runtime loads.
|
|
377
|
+
* - `voidRoot: true` hydrates without returned-value reconciliation. Only a
|
|
378
|
+
* build that proved every page and layout it can render is a void `@{}`
|
|
379
|
+
* body, with no root boundary, may set it (see compiler/void-roots.js).
|
|
380
|
+
*
|
|
348
381
|
* `getComponentExport` mirrors routes.js `get_component_export` (route named
|
|
349
382
|
* export > default > first PascalCase) so server and client pick the SAME
|
|
350
383
|
* component.
|
|
@@ -359,16 +392,26 @@ async function bootstrapDocumentLifecycle() {
|
|
|
359
392
|
* resolveImport?: (id: string) => string,
|
|
360
393
|
* runtimeModuleId?: string,
|
|
361
394
|
* generatedBy?: string,
|
|
395
|
+
* rootBoundary?: boolean,
|
|
396
|
+
* voidRoot?: boolean,
|
|
397
|
+
* independentHydration?: boolean,
|
|
398
|
+
* streamedSignals?: boolean,
|
|
362
399
|
* }} [options]
|
|
363
400
|
* @returns {string}
|
|
364
401
|
*/
|
|
365
402
|
export function create_client_entry_source(options = {}) {
|
|
366
403
|
const runtimeModuleId =
|
|
367
404
|
options.runtimeModuleId ?? fileURLToPath(new URL('./client-entry-runtime.js', import.meta.url));
|
|
405
|
+
const rootBoundary = options.rootBoundary !== false;
|
|
406
|
+
const imports = [
|
|
407
|
+
options.voidRoot ? '__hydrateVoidRoot as hydrateRoot' : 'hydrateRoot',
|
|
408
|
+
'initializeHydrationEventCapture',
|
|
409
|
+
...(rootBoundary ? ['Suspense', 'ErrorBoundary', 'createElement'] : []),
|
|
410
|
+
];
|
|
368
411
|
return (
|
|
369
412
|
client_entry_prelude(
|
|
370
413
|
options,
|
|
371
|
-
`import {
|
|
414
|
+
`import { ${imports.join(', ')} } from ${JSON.stringify(runtimeModuleId)};`,
|
|
372
415
|
) +
|
|
373
416
|
`function getComponentExport(module, exportName) {
|
|
374
417
|
// Explicit export name requires an exact match; do NOT fall back, so a
|
|
@@ -377,7 +420,9 @@ export function create_client_entry_source(options = {}) {
|
|
|
377
420
|
if (typeof module.default === 'function') return module.default;
|
|
378
421
|
return Object.entries(module).find(([key, value]) => typeof value === 'function' && /^[A-Z]/.test(key))?.[1];
|
|
379
422
|
}
|
|
380
|
-
|
|
423
|
+
${
|
|
424
|
+
rootBoundary
|
|
425
|
+
? `
|
|
381
426
|
function withRootBoundary(content, boundary) {
|
|
382
427
|
let body = content;
|
|
383
428
|
// Keep ErrorBoundary closest to the route. Suspense may retain its pending
|
|
@@ -401,7 +446,9 @@ function withRootBoundary(content, boundary) {
|
|
|
401
446
|
}
|
|
402
447
|
return body;
|
|
403
448
|
}
|
|
404
|
-
|
|
449
|
+
`
|
|
450
|
+
: ''
|
|
451
|
+
}
|
|
405
452
|
(async () => {
|
|
406
453
|
try {
|
|
407
454
|
const el = document.getElementById('__octane_data');
|
|
@@ -428,11 +475,15 @@ function withRootBoundary(content, boundary) {
|
|
|
428
475
|
)) throw new Error('[octane] Invalid streamed signal document identity.');
|
|
429
476
|
if (globalThis.__octaneStreamedSignalSelections !== undefined) {
|
|
430
477
|
if (!data.streamedSignals) throw new Error('[octane] Missing streamed signal document identity.');
|
|
431
|
-
|
|
478
|
+
${
|
|
479
|
+
options.streamedSignals === false
|
|
480
|
+
? ` throw new Error('[octane] Streamed signals reached a client build that compiled none.');`
|
|
481
|
+
: ` signalHydrationModule ??= await loadSignalHydration();
|
|
432
482
|
if (incompatibleBuild) return;
|
|
433
|
-
streamedHydration = signalHydrationModule.
|
|
483
|
+
streamedHydration = signalHydrationModule.bootstrapStreamedSignalResults(data.streamedSignals);
|
|
434
484
|
signalOwner = streamedHydration.signalOwner;
|
|
435
|
-
await bootstrapDocumentLifecycle()
|
|
485
|
+
await bootstrapDocumentLifecycle();`
|
|
486
|
+
}
|
|
436
487
|
}
|
|
437
488
|
if (!data.entry) {
|
|
438
489
|
console.error('[octane] Unable to hydrate: no route entry in #__octane_data.');
|
|
@@ -489,7 +540,9 @@ function withRootBoundary(content, boundary) {
|
|
|
489
540
|
Content = (_props, scope, extra) => Component({ params, url }, scope, extra);
|
|
490
541
|
}
|
|
491
542
|
|
|
492
|
-
|
|
543
|
+
${
|
|
544
|
+
rootBoundary
|
|
545
|
+
? ` const rootBoundary = { pending: null, catch: null };
|
|
493
546
|
for (const kind of ['pending', 'catch']) {
|
|
494
547
|
const entry = data.rootBoundary?.[kind];
|
|
495
548
|
if (!entry) continue;
|
|
@@ -503,10 +556,13 @@ function withRootBoundary(content, boundary) {
|
|
|
503
556
|
}
|
|
504
557
|
rootBoundary[kind] = Boundary;
|
|
505
558
|
}
|
|
506
|
-
|
|
559
|
+
Content = withRootBoundary(Content, rootBoundary);
|
|
560
|
+
`
|
|
561
|
+
: ''
|
|
562
|
+
}
|
|
507
563
|
if (incompatibleBuild) return;
|
|
508
564
|
if (documentLifecycle && !await documentLifecycle.whenActive()) return;
|
|
509
|
-
hydrateRoot(target,
|
|
565
|
+
hydrateRoot(target, Content, undefined,
|
|
510
566
|
signalOwner === undefined ? undefined : { signalOwner });
|
|
511
567
|
} catch (error) {
|
|
512
568
|
console.error('[octane] Failed to bootstrap client hydration.', error);
|
|
@@ -564,11 +620,15 @@ export function create_islands_entry_source(options = {}) {
|
|
|
564
620
|
)) throw new Error('[octane] Invalid streamed signal document identity.');
|
|
565
621
|
if (globalThis.__octaneStreamedSignalSelections !== undefined) {
|
|
566
622
|
if (!data.streamedSignals) throw new Error('[octane] Missing streamed signal document identity.');
|
|
567
|
-
|
|
623
|
+
${
|
|
624
|
+
options.streamedSignals === false
|
|
625
|
+
? ` throw new Error('[octane] Streamed signals reached a client build that compiled none.');`
|
|
626
|
+
: ` signalHydrationModule ??= await loadSignalHydration();
|
|
568
627
|
if (incompatibleBuild) return;
|
|
569
|
-
streamedHydration = signalHydrationModule.
|
|
628
|
+
streamedHydration = signalHydrationModule.bootstrapStreamedSignalResults(data.streamedSignals);
|
|
570
629
|
signalOwner = streamedHydration.signalOwner;
|
|
571
|
-
await bootstrapDocumentLifecycle()
|
|
630
|
+
await bootstrapDocumentLifecycle();`
|
|
631
|
+
}
|
|
572
632
|
}
|
|
573
633
|
|
|
574
634
|
// The shell stays server-owned: only its independent islands activate.
|