@octanejs/app-core 0.1.8 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/app-core",
3
- "version": "0.1.8",
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.14.0"
82
+ "octane": "^0.16.0"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/node": "^24.13.3",
86
- "octane": "0.14.0"
86
+ "octane": "0.16.0"
87
87
  }
88
88
  }
@@ -1,5 +1,11 @@
1
1
  // @ts-check
2
- export { hydrateRoot, initializeHydrationEventCapture, Suspense, ErrorBoundary } from 'octane';
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
- const independentStyles = new Map();
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
- async function bootstrapDocumentLifecycle() {
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 import('octane/hydration/streamed-signals');
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 { hydrateRoot, initializeHydrationEventCapture, Suspense, ErrorBoundary, createElement } from ${JSON.stringify(runtimeModuleId)};`,
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
- signalHydrationModule = await import('octane/hydration/streamed-signals');
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.bootstrapStreamedSignalHydration(data.streamedSignals);
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
- const rootBoundary = { pending: null, catch: null };
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, withRootBoundary(Content, rootBoundary), undefined,
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
- signalHydrationModule = await import('octane/hydration/streamed-signals');
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.bootstrapStreamedSignalHydration(data.streamedSignals);
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.