@ecopages/core 0.2.0-rc.0 → 0.2.0-rc.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/README.md CHANGED
@@ -80,7 +80,7 @@ Use this package README as the top-level map, then drill into the focused subsys
80
80
 
81
81
  - `src/config/README.md`: config finalization and app-owned runtime/build state
82
82
  - `src/plugins/README.md`: integration and processor authoring contracts
83
- - `src/build/README.md`: build adapter, executor, and development build coordination
83
+ - `src/build/README.md`: build adapter, executor, development build coordination, and standalone Node server packaging
84
84
  - `src/services/README.md`: cross-cutting runtime services and orchestration helpers
85
85
  - `src/adapters/README.md`: Bun, Node, and shared adapter boundaries
86
86
  - `src/dev/README.md`: dev transform server and on-demand client delivery
@@ -298,6 +298,7 @@ Use these entrypoints when implementing integrations, processors, or source tran
298
298
  - `@ecopages/core/plugins/processor`
299
299
  - `@ecopages/core/plugins/source-transform`
300
300
  - `@ecopages/core/route-renderer/orchestration/integration-renderer`
301
+ - `@ecopages/core/route-renderer/orchestration/document-shell/layout-shell-props.service`
301
302
  - `@ecopages/core/services/asset-processing-service`
302
303
  - `@ecopages/core/hmr/hmr-strategy`
303
304
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-rc.0",
3
+ "version": "0.2.0-rc.1",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-rc.0",
20
+ "@ecopages/file-system": "0.2.0-rc.1",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.5",
23
23
  "@oxc-project/runtime": "0.141.0",
@@ -31,7 +31,7 @@
31
31
  "@standard-schema/utils": "^0.3.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@ecopages/dev-toolbar": "0.2.0-rc.0"
34
+ "@ecopages/dev-toolbar": "0.2.0-rc.1"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@ecopages/dev-toolbar": {
@@ -75,6 +75,10 @@
75
75
  "types": "./src/route-renderer/orchestration/document-shell/document-shell-render.service.d.ts",
76
76
  "default": "./src/route-renderer/orchestration/document-shell/document-shell-render.service.js"
77
77
  },
78
+ "./route-renderer/orchestration/document-shell/layout-shell-props.service": {
79
+ "types": "./src/route-renderer/orchestration/document-shell/layout-shell-props.service.d.ts",
80
+ "default": "./src/route-renderer/orchestration/document-shell/layout-shell-props.service.js"
81
+ },
78
82
  "./route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils": {
79
83
  "types": "./src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.d.ts",
80
84
  "default": "./src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.js"
@@ -363,6 +367,10 @@
363
367
  "types": "./src/route-renderer/orchestration/document-shell/document-shell-render.service.d.ts",
364
368
  "default": "./src/route-renderer/orchestration/document-shell/document-shell-render.service.js"
365
369
  },
370
+ "./route-renderer/orchestration/document-shell/layout-shell-props.service.ts": {
371
+ "types": "./src/route-renderer/orchestration/document-shell/layout-shell-props.service.d.ts",
372
+ "default": "./src/route-renderer/orchestration/document-shell/layout-shell-props.service.js"
373
+ },
366
374
  "./route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.ts": {
367
375
  "types": "./src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.d.ts",
368
376
  "default": "./src/route-renderer/orchestration/page-browser-graph/page-browser-graph-merge.utils.js"
@@ -8,7 +8,7 @@
8
8
  * @module EcopagesApp
9
9
  */
10
10
  import type { Server } from 'bun';
11
- import type { ApiHandlerContext, RouteGroupBuilder } from '../../types/public-types.js';
11
+ import type { ApiHandlerContext, EcopagesRouteInfo, RouteGroupBuilder } from '../../types/public-types.js';
12
12
  import { SharedApplicationAdapter } from '../shared/runtime/application-adapter.js';
13
13
  import type { RuntimeHost } from '../shared/runtime/runtime-host.js';
14
14
  import type { EcopagesAppOptions } from '../create-app.js';
@@ -55,7 +55,7 @@ export declare class BunEcopagesApp<WebSocketData = undefined> extends SharedApp
55
55
  */
56
56
  protected bootServer(): Promise<Server<WebSocketData> | void>;
57
57
  stop(force?: boolean): Promise<void>;
58
- protected resolveAppRoutes(): Promise<import("../../index.browser.js").EcopagesRouteInfo[]>;
58
+ protected resolveAppRoutes(): Promise<EcopagesRouteInfo[]>;
59
59
  }
60
60
  /**
61
61
  * Factory function to create a Bun application
@@ -12,55 +12,13 @@ export declare function defineApiHandler<TPath extends string, TSchema extends R
12
12
  declare const defineApiMethod: <M extends ApiHandler["method"]>(method: M) => <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: DefineApiHandlerInput<TPath, TSchema, TContext> & {
13
13
  path: TPath;
14
14
  }) => ApiHandler<TPath, Request, unknown>;
15
- export declare const defineGet: <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: Omit<ApiHandler<TPath, Request, unknown>, "middleware" | "method" | "schema" | "handler"> & {
16
- handler: (context: SchemaHandlerContext<TSchema, TContext>) => Promise<Response> | Response;
17
- middleware?: Middleware<Request, unknown, TContext>[] | undefined;
18
- schema?: TSchema | undefined;
19
- } & {
20
- path: TPath;
21
- }) => ApiHandler<TPath, Request, unknown>;
22
- export declare const definePost: <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: Omit<ApiHandler<TPath, Request, unknown>, "middleware" | "method" | "schema" | "handler"> & {
23
- handler: (context: SchemaHandlerContext<TSchema, TContext>) => Promise<Response> | Response;
24
- middleware?: Middleware<Request, unknown, TContext>[] | undefined;
25
- schema?: TSchema | undefined;
26
- } & {
27
- path: TPath;
28
- }) => ApiHandler<TPath, Request, unknown>;
29
- export declare const definePut: <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: Omit<ApiHandler<TPath, Request, unknown>, "middleware" | "method" | "schema" | "handler"> & {
30
- handler: (context: SchemaHandlerContext<TSchema, TContext>) => Promise<Response> | Response;
31
- middleware?: Middleware<Request, unknown, TContext>[] | undefined;
32
- schema?: TSchema | undefined;
33
- } & {
34
- path: TPath;
35
- }) => ApiHandler<TPath, Request, unknown>;
36
- export declare const defineDelete: <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: Omit<ApiHandler<TPath, Request, unknown>, "middleware" | "method" | "schema" | "handler"> & {
37
- handler: (context: SchemaHandlerContext<TSchema, TContext>) => Promise<Response> | Response;
38
- middleware?: Middleware<Request, unknown, TContext>[] | undefined;
39
- schema?: TSchema | undefined;
40
- } & {
41
- path: TPath;
42
- }) => ApiHandler<TPath, Request, unknown>;
43
- export declare const definePatch: <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: Omit<ApiHandler<TPath, Request, unknown>, "middleware" | "method" | "schema" | "handler"> & {
44
- handler: (context: SchemaHandlerContext<TSchema, TContext>) => Promise<Response> | Response;
45
- middleware?: Middleware<Request, unknown, TContext>[] | undefined;
46
- schema?: TSchema | undefined;
47
- } & {
48
- path: TPath;
49
- }) => ApiHandler<TPath, Request, unknown>;
50
- export declare const defineOptions: <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: Omit<ApiHandler<TPath, Request, unknown>, "middleware" | "method" | "schema" | "handler"> & {
51
- handler: (context: SchemaHandlerContext<TSchema, TContext>) => Promise<Response> | Response;
52
- middleware?: Middleware<Request, unknown, TContext>[] | undefined;
53
- schema?: TSchema | undefined;
54
- } & {
55
- path: TPath;
56
- }) => ApiHandler<TPath, Request, unknown>;
57
- export declare const defineHead: <const TPath extends string, TSchema extends RouteSchema | undefined = undefined, TContext extends UniversalContext = UniversalContext>(handler: Omit<ApiHandler<TPath, Request, unknown>, "middleware" | "method" | "schema" | "handler"> & {
58
- handler: (context: SchemaHandlerContext<TSchema, TContext>) => Promise<Response> | Response;
59
- middleware?: Middleware<Request, unknown, TContext>[] | undefined;
60
- schema?: TSchema | undefined;
61
- } & {
62
- path: TPath;
63
- }) => ApiHandler<TPath, Request, unknown>;
15
+ export declare const defineGet: ReturnType<typeof defineApiMethod<'GET'>>;
16
+ export declare const definePost: ReturnType<typeof defineApiMethod<'POST'>>;
17
+ export declare const definePut: ReturnType<typeof defineApiMethod<'PUT'>>;
18
+ export declare const defineDelete: ReturnType<typeof defineApiMethod<'DELETE'>>;
19
+ export declare const definePatch: ReturnType<typeof defineApiMethod<'PATCH'>>;
20
+ export declare const defineOptions: ReturnType<typeof defineApiMethod<'OPTIONS'>>;
21
+ export declare const defineHead: ReturnType<typeof defineApiMethod<'HEAD'>>;
64
22
  export declare function json(data: unknown, options?: ResponseOptions): Response;
65
23
  export declare function html(content: string, options?: ResponseOptions): Response;
66
24
  export declare function redirect(url: string, status?: number): Response;
@@ -56,7 +56,7 @@ build/
56
56
  - `runtime/build-request-policy.ts`: server/browser request constructors and plugin collision rules.
57
57
  - `runtime/build-request-identity.ts` / `cache/cache-keys.ts`: canonical request identity and shared cache fingerprints.
58
58
  - `contracts/build-types.ts`: the `EcoBuildPlugin` contract used by integrations and processors.
59
- - `rolldown/rolldown-build-adapter.ts`: the production `BuildAdapter`. Wraps the bundler, normalizes Node output imports, and exposes a normalized `BuildResult`.
59
+ - `rolldown/rolldown-build-adapter.ts`: the production `BuildAdapter`. Wraps the bundler, normalizes Node output imports, and exposes a normalized `BuildResult`. App dependencies remain bare imports; Core-owned runtime packages that need CommonJS named-export interop are bundled, while other Core dependencies resolve to `file:` URLs. Generated server bundles therefore do not require app-level framework dependencies.
60
60
  - `rolldown/rolldown-plugin-bridge.ts`: `EcoBuildPlugin[]` → bundler-plugin translation.
61
61
  - `runtime/serialized-build-executor.ts`: FIFO queue primitive.
62
62
  - `cache/server-entry-build-cache.ts`: production server-entry bundle cache (`.eco/.server-entry/.build-cache.json` + `dist/.server/manifest.json`).
@@ -20,6 +20,7 @@ import { isDeclaredAppPackageImport, isWorkspacePackageImport, normalizeNodeRunt
20
20
  const corePackageRequire = createRequire(new URL('../../../package.json', import.meta.url));
21
21
  const nodeBuiltinSpecifiers = new Set(builtinModules);
22
22
  let corePackageNames;
23
+ const CORE_RUNTIME_BUNDLED_PACKAGES = new Set(['ws']);
23
24
  /**
24
25
  * Returns the set of packages declared as direct dependencies in core's own
25
26
  * `package.json` (all dependency fields combined).
@@ -102,12 +103,12 @@ function getAppRootRequire(cache, contextRoot) {
102
103
  * TypeScript or JSX file (source packages); pre-compiled packages are left
103
104
  * external so the app's own resolver handles them at runtime.
104
105
  * 3. **Everything else** (undeclared transitives) — split into two sub-cases:
105
- * - A **direct dependency of core** that resolves to a compiled JS file
106
- * (e.g. `oxc-parser`, `rolldown`, `ws`): externalized so the
107
- * `runtime-build-output-normalizer` can rewrite the import to an
108
- * absolute `file://` URL after the build. "Direct" means declared in
109
- * core's own `package.json`; transitives that merely *resolve through*
110
- * core's `require` are excluded.
106
+ * - A **direct dependency of core** that resolves to a compiled JS file:
107
+ * externalized so the `runtime-build-output-normalizer` can rewrite the
108
+ * import to an absolute `file://` URL after the build. Runtime packages
109
+ * that require CommonJS named-export interop (currently `ws`) are
110
+ * bundled instead. "Direct" means declared in core's own `package.json`;
111
+ * transitives that merely *resolve through* core's `require` are excluded.
111
112
  * - **Everything else** (true transitives like `lexical`, `@lexical/react`):
112
113
  * bundled unconditionally. pnpm strict hoisting means these packages
113
114
  * are unreachable as bare specifiers from output directories such as
@@ -128,6 +129,9 @@ function shouldBundlePackageImport(id, contextRoot, appRootRequireCache) {
128
129
  return Boolean(appResolvedPath && /\.(?:[cm]?ts|tsx|jsx)$/u.test(appResolvedPath));
129
130
  }
130
131
  if (isCoreDeclaredPackageImport(id)) {
132
+ if (CORE_RUNTIME_BUNDLED_PACKAGES.has(getPackageName(id))) {
133
+ return true;
134
+ }
131
135
  const coreResolvedPath = tryResolveModule(id, corePackageRequire);
132
136
  if (coreResolvedPath && !/\.(?:[cm]?ts|tsx|jsx)$/u.test(coreResolvedPath)) {
133
137
  return false;
@@ -135,6 +139,11 @@ function shouldBundlePackageImport(id, contextRoot, appRootRequireCache) {
135
139
  }
136
140
  return true;
137
141
  }
142
+ function getPackageName(specifier) {
143
+ return specifier.startsWith('@')
144
+ ? specifier.split('/').slice(0, 2).join('/')
145
+ : (specifier.split('/')[0] ?? specifier);
146
+ }
138
147
  function createExternalMatcher(options, appRootRequireCache) {
139
148
  const explicitExternals = new Set(options.external ?? []);
140
149
  const externalPackages = options.externalPackages === true;
@@ -6,19 +6,6 @@ import { isBarePackageImportSpecifier } from '../../plugins/tsconfig-import-reso
6
6
  const corePackageRequire = createRequire(new URL('../../../package.json', import.meta.url));
7
7
  const appDeclaredPackageCache = new Map();
8
8
  const appWorkspacePackageCache = new Map();
9
- /**
10
- * Core-owned packages that are part of the Node adapter's runtime surface and
11
- * should therefore be preserved as bare specifiers in bundled output.
12
- *
13
- * Only list packages whose bare-specifier form will be resolvable from the
14
- * app's Node.js runtime (i.e. they ship alongside the framework and do not
15
- * need to be in the app's own package.json).
16
- *
17
- * Build-time tools (e.g. oxc-parser, rolldown) deliberately omitted — their
18
- * imports in server-side output must be rewritten to absolute file: URLs so
19
- * the bundler does not embed them in app code.
20
- */
21
- const CORE_RUNTIME_BARE_SPECIFIER_PACKAGES = new Set(['ws']);
22
9
  function tryResolveRuntimeImport(specifier, resolver) {
23
10
  try {
24
11
  return resolver.resolve(specifier);
@@ -143,24 +130,11 @@ export function isDeclaredAppPackageImport(specifier, rootDir) {
143
130
  * Whether the given bare specifier should be left as-is (not rewritten to a
144
131
  * file URL) in bundled output.
145
132
  *
146
- * Two categories qualify:
147
- * 1. **App-declared packages** present in the app's own package.json; the
148
- * app's resolver will find them at runtime.
149
- * 2. **Core runtime surface packages** — a curated subset of the framework's
150
- * own dependencies that are part of its public Node adapter surface (e.g.
151
- * `ws`). These ship alongside the framework and are resolvable from the
152
- * bundled output's location even when the app has not declared them.
153
- *
154
- * Build-time packages declared by core (e.g. `oxc-parser`, `rolldown`) are
155
- * intentionally *not* included here — their imports must still be rewritten
156
- * to absolute `file:` URLs so they are not left as bare specifiers in app
157
- * output.
133
+ * Only app-declared packages qualify: their resolver can find them from a
134
+ * generated server bundle. Core dependencies remain file URLs because a
135
+ * bundle in `dist/.server` is not inside Core's Node.js resolution chain.
158
136
  */
159
137
  function isDeclaredInResolutionChain(specifier, rootDir) {
160
- const packageName = getPackageNameFromSpecifier(specifier);
161
- if (CORE_RUNTIME_BARE_SPECIFIER_PACKAGES.has(packageName)) {
162
- return true;
163
- }
164
138
  return isDeclaredAppPackageImport(specifier, rootDir);
165
139
  }
166
140
  function rewriteRuntimeImportSpecifier(specifier, quote, rootDir) {
@@ -16,7 +16,8 @@ These contracts are responsible for:
16
16
  ## Main Files
17
17
 
18
18
  - `integration-plugin.ts`: framework-semantics contract for render integrations
19
- - `processor.ts`: asset-transformation contract for processors
19
+ - `define-integration.ts`: typed factory for integrations that only need declarative config and a renderer class
20
+ - `processor.ts`: asset-transformation contract for processors, including generated `@types` package helpers for virtual modules
20
21
  - `runtime-capability.ts`: runtime compatibility declaration types
21
22
  - `source-transform.ts`: bundler-neutral source-transform primitive with Ecopages adapters plus app-level Vite plugin composition helpers
22
23
  - `eco-component-meta-plugin.ts`: component-identity attribution transform that uses a lexical `eco.` gate, then an Oxc `CallExpression` rewrite
@@ -64,13 +64,14 @@ export interface IntegrationPluginConfig {
64
64
  */
65
65
  jsxImportSource?: string;
66
66
  }
67
- type RendererClass<C> = new (options: {
67
+ type IntegrationRendererConstructorOptions = {
68
68
  appConfig: EcoPagesAppConfig;
69
69
  assetProcessingService: AssetProcessingService;
70
70
  resolvedIntegrationDependencies: ProcessedAsset[];
71
71
  rendererModules?: unknown;
72
72
  runtimeOrigin: string;
73
- }) => IntegrationRenderer<C>;
73
+ };
74
+ type RendererClass<C> = new (options: IntegrationRendererConstructorOptions) => IntegrationRenderer<C>;
74
75
  /**
75
76
  * Base class for framework integrations.
76
77
  *
@@ -186,13 +187,7 @@ export declare abstract class IntegrationPlugin<C = EcoPagesElement> {
186
187
  */
187
188
  protected createRendererOptions(options?: {
188
189
  rendererModules?: unknown;
189
- }): {
190
- appConfig: EcoPagesAppConfig;
191
- assetProcessingService: AssetProcessingService;
192
- resolvedIntegrationDependencies: ProcessedAsset[];
193
- rendererModules: unknown;
194
- runtimeOrigin: string;
195
- };
190
+ }): IntegrationRendererConstructorOptions;
196
191
  /**
197
192
  * Attaches runtime-only services after a renderer instance has been created.
198
193
  */
@@ -13,6 +13,19 @@ export declare function resolveGeneratedPath(type: keyof typeof GENERATED_BASE_P
13
13
  module: string;
14
14
  subPath?: string;
15
15
  }): string;
16
+ /**
17
+ * Serializes the TypeScript `@types` package manifest for generated virtual-module declarations.
18
+ */
19
+ export declare function serializeGeneratedTypesPackage(packageName: string): string;
20
+ /**
21
+ * Writes the generated `@types` package manifest that TypeScript auto-loads from `node_modules`.
22
+ */
23
+ export declare function writeGeneratedTypesPackage(options: {
24
+ root: string;
25
+ module: string;
26
+ packageName: string;
27
+ writeFile: (filePath: string, content: string) => void;
28
+ }): void;
16
29
  export interface ProcessorWatchContext {
17
30
  path: string;
18
31
  bridge: IClientBridge;
@@ -14,6 +14,27 @@ export function resolveGeneratedPath(type, options) {
14
14
  const parts = [root, GENERATED_BASE_PATHS[type], module, subPath].filter(Boolean);
15
15
  return path.join(...parts);
16
16
  }
17
+ /**
18
+ * Serializes the TypeScript `@types` package manifest for generated virtual-module declarations.
19
+ */
20
+ export function serializeGeneratedTypesPackage(packageName) {
21
+ return `${JSON.stringify({
22
+ name: packageName,
23
+ version: '0.0.0',
24
+ types: './index.d.js',
25
+ }, null, 2)}\n`;
26
+ }
27
+ /**
28
+ * Writes the generated `@types` package manifest that TypeScript auto-loads from `node_modules`.
29
+ */
30
+ export function writeGeneratedTypesPackage(options) {
31
+ const packageManifestPath = resolveGeneratedPath('types', {
32
+ root: options.root,
33
+ module: options.module,
34
+ subPath: 'package.json',
35
+ });
36
+ options.writeFile(packageManifestPath, serializeGeneratedTypesPackage(options.packageName));
37
+ }
17
38
  /**
18
39
  * Base class for content and asset processors that contribute build plugins.
19
40
  *
@@ -63,7 +63,7 @@ Domain folders:
63
63
 
64
64
  - `ownership-graph/`: component graph walk/collect and up-front ownership validation
65
65
  - `foreign-child/`: active render context, foreign-subtree execution, deferred template serialization
66
- - `document-shell/`: shared page/layout/html shell composition and attribute stamping helpers
66
+ - `document-shell/`: shared page/layout/html shell composition, layout prop resolution, and attribute stamping helpers
67
67
  - `route-pipeline/`: route preparation, final HTML capture, marker-artifact enforcement
68
68
  - `page-browser-graph/`: browser asset graph build, session cache, and injector assets
69
69
 
@@ -1,7 +1,12 @@
1
- import type { EcoPageLayoutEntry, LayoutPropsContext } from '../../../types/public-types.js';
1
+ import type { EcoComponent, EcoPageLayoutEntry, LayoutPropsContext } from '../../../types/public-types.js';
2
+ import type { DocumentShellLayoutInput } from './document-shell-render.service.js';
2
3
  export type LayoutShellPropsContext = LayoutPropsContext & {
3
4
  pageProps?: Record<string, unknown>;
4
5
  };
6
+ export type DocumentShellLayoutResolutionInput = LayoutShellPropsContext & {
7
+ layout?: EcoComponent;
8
+ layoutEntries?: EcoPageLayoutEntry[];
9
+ };
5
10
  /**
6
11
  * Resolves the default shell props passed to a layout tier during route render.
7
12
  */
@@ -10,6 +15,13 @@ export declare function resolveLayoutShellProps(context: LayoutShellPropsContext
10
15
  * Resolves props for one layout entry, merging shell props with an optional factory.
11
16
  */
12
17
  export declare function resolveLayoutEntryProps(entry: EcoPageLayoutEntry, context: LayoutShellPropsContext): Record<string, unknown>;
18
+ /**
19
+ * Resolves declarative layout entries into the document-shell inputs used during route render.
20
+ *
21
+ * @remarks Layout prop factories must be evaluated here so integrations do not
22
+ * silently fall back to the innermost layout and discard route-specific props.
23
+ */
24
+ export declare function resolveDocumentShellLayouts(input: DocumentShellLayoutResolutionInput): DocumentShellLayoutInput[];
13
25
  /**
14
26
  * Reads the normalized layout stack from a page component config.
15
27
  */
@@ -22,6 +22,21 @@ export function resolveLayoutEntryProps(entry, context) {
22
22
  ...entry.props(layoutPropsContext),
23
23
  };
24
24
  }
25
+ /**
26
+ * Resolves declarative layout entries into the document-shell inputs used during route render.
27
+ *
28
+ * @remarks Layout prop factories must be evaluated here so integrations do not
29
+ * silently fall back to the innermost layout and discard route-specific props.
30
+ */
31
+ export function resolveDocumentShellLayouts(input) {
32
+ if (input.layoutEntries && input.layoutEntries.length > 0) {
33
+ return input.layoutEntries.map((entry) => ({
34
+ component: entry.component,
35
+ props: resolveLayoutEntryProps(entry, input),
36
+ }));
37
+ }
38
+ return input.layout ? [{ component: input.layout, props: resolveLayoutShellProps(input) }] : [];
39
+ }
25
40
  /**
26
41
  * Reads the normalized layout stack from a page component config.
27
42
  */
@@ -16,6 +16,7 @@ import { type ResolvedPageDependencies } from '../page-loading/resolved-page-dep
16
16
  import { type GroupedGraphBuildPlan } from './page-browser-graph/grouped-graph-build-plan.js';
17
17
  import type { ForeignChildRuntime } from './foreign-child/component-render-context.js';
18
18
  import { ForeignSubtreeExecutionService } from './foreign-child/foreign-subtree-execution.service.js';
19
+ import { type DocumentShellLayoutInput } from './document-shell/document-shell-render.service.js';
19
20
  /**
20
21
  * Controls how one route module is loaded outside the normal render path.
21
22
  */
@@ -210,6 +211,7 @@ export declare abstract class IntegrationRenderer<C = EcoPagesElement> {
210
211
  component: EcoComponent;
211
212
  props?: Record<string, unknown>;
212
213
  };
214
+ layouts?: DocumentShellLayoutInput[];
213
215
  htmlTemplate: EcoComponent;
214
216
  metadata: PageMetadataProps;
215
217
  pageProps: Record<string, unknown>;
@@ -9,6 +9,6 @@ import { IntegrationRenderer, type RenderToResponseContext } from './integration
9
9
  */
10
10
  export declare abstract class StringMarkupRenderer extends IntegrationRenderer<EcoPagesElement> {
11
11
  renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
12
- render({ params, query, props, locals, pageLocals, metadata, Page, Layout, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions): Promise<RouteRendererBody>;
12
+ render({ params, query, props, locals, pageLocals, metadata, Page, Layout, layoutEntries, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions): Promise<RouteRendererBody>;
13
13
  renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
14
14
  }
@@ -1,5 +1,5 @@
1
1
  import { IntegrationRenderer } from './integration-renderer.js';
2
- import { resolveInnermostPageLayout } from './document-shell/layout-shell-props.service.js';
2
+ import { resolveDocumentShellLayouts, resolveInnermostPageLayout, } from './document-shell/layout-shell-props.service.js';
3
3
  /**
4
4
  * Base renderer for integrations whose page output is string HTML markup.
5
5
  *
@@ -14,19 +14,20 @@ export class StringMarkupRenderer extends IntegrationRenderer {
14
14
  }
15
15
  return this.renderStringComponentWithQueuedForeignSubtrees(input, input.component);
16
16
  }
17
- async render({ params, query, props, locals, pageLocals, metadata, Page, Layout, HtmlTemplate, pageProps, }) {
17
+ async render({ params, query, props, locals, pageLocals, metadata, Page, Layout, layoutEntries, HtmlTemplate, pageProps, }) {
18
18
  try {
19
19
  return await this.renderPageWithDocumentShell({
20
20
  page: {
21
21
  component: Page,
22
22
  props: { params, query, ...props, locals: pageLocals },
23
23
  },
24
- layout: Layout
25
- ? {
26
- component: Layout,
27
- props: locals ? { locals } : {},
28
- }
29
- : undefined,
24
+ layouts: resolveDocumentShellLayouts({
25
+ layout: Layout,
26
+ layoutEntries,
27
+ params,
28
+ query,
29
+ locals,
30
+ }),
30
31
  htmlTemplate: HtmlTemplate,
31
32
  metadata,
32
33
  pageProps: pageProps ?? props ?? {},
@@ -603,8 +603,8 @@ export interface HtmlTemplateProps<T = EcoPagesElement> extends PageHeadProps<T>
603
603
  * Request-scoped context available to layout prop factories on `eco.page`.
604
604
  */
605
605
  export type LayoutPropsContext = {
606
- params?: Record<string, string>;
607
- query?: Record<string, string>;
606
+ params?: PageParams;
607
+ query?: PageQuery;
608
608
  locals?: RequestLocals;
609
609
  };
610
610
  /**