@ecopages/core 0.2.0-beta.12 → 0.2.0-beta.14
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 +1 -1
- package/package.json +26 -2
- package/src/adapters/abstract/application-adapter.d.ts +27 -3
- package/src/adapters/abstract/application-adapter.js +28 -0
- package/src/adapters/abstract/server-adapter.d.ts +3 -2
- package/src/adapters/bun/create-app.d.ts +1 -1
- package/src/adapters/bun/create-app.js +23 -6
- package/src/adapters/bun/server-adapter.d.ts +9 -2
- package/src/adapters/bun/server-adapter.js +28 -4
- package/src/adapters/create-app.d.ts +1 -12
- package/src/adapters/create-app.js +0 -58
- package/src/adapters/index.d.ts +1 -1
- package/src/adapters/index.js +1 -2
- package/src/adapters/node/create-app.d.ts +1 -1
- package/src/adapters/node/create-app.js +14 -4
- package/src/adapters/node/server-adapter.d.ts +7 -1
- package/src/adapters/node/server-adapter.js +25 -3
- package/src/adapters/shared/fs-server-response-matcher.d.ts +2 -0
- package/src/adapters/shared/fs-server-response-matcher.js +39 -17
- package/src/dev/server-ready-message.d.ts +6 -0
- package/src/dev/server-ready-message.js +8 -0
- package/src/eco/README.md +11 -0
- package/src/eco/eco-declared-component.d.ts +14 -0
- package/src/eco/eco-declared-component.js +16 -0
- package/src/eco/eco.browser.js +5 -6
- package/src/eco/eco.js +5 -6
- package/src/eco/eco.types.d.ts +2 -2
- package/src/eco/page-layout-normalization.d.ts +21 -0
- package/src/eco/page-layout-normalization.js +59 -0
- package/src/errors/index.d.ts +1 -0
- package/src/errors/index.js +7 -1
- package/src/errors/undeclared-component-dependency-error.d.ts +11 -0
- package/src/errors/undeclared-component-dependency-error.js +17 -0
- package/src/route-renderer/orchestration/component-graph.js +12 -4
- package/src/route-renderer/orchestration/document-shell-render.service.d.ts +37 -1
- package/src/route-renderer/orchestration/document-shell-render.service.js +79 -13
- package/src/route-renderer/orchestration/integration-renderer.js +5 -1
- package/src/route-renderer/orchestration/layout-shell-props.service.d.ts +20 -0
- package/src/route-renderer/orchestration/layout-shell-props.service.js +31 -0
- package/src/route-renderer/orchestration/ownership-validation.service.d.ts +1 -0
- package/src/route-renderer/orchestration/ownership-validation.service.js +18 -0
- package/src/route-renderer/orchestration/route-prepared-options.builder.d.ts +3 -1
- package/src/route-renderer/orchestration/route-prepared-options.builder.js +3 -1
- package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +3 -1
- package/src/route-renderer/orchestration/route-render-orchestrator.js +3 -3
- package/src/route-renderer/orchestration/string-markup-renderer.js +2 -1
- package/src/route-renderer/page-loading/component-dependency-collection.js +5 -2
- package/src/types/public-types.d.ts +51 -4
- package/src/utils/parse-cli-args.d.ts +1 -0
- package/src/utils/parse-cli-args.js +3 -0
- package/src/utils/server-utils.module.js +1 -0
- package/src/watchers/project-watcher-ignore.d.ts +11 -0
- package/src/watchers/project-watcher-ignore.js +16 -0
- package/src/watchers/project-watcher.js +2 -6
|
@@ -39,13 +39,13 @@ class FileSystemResponseMatcher {
|
|
|
39
39
|
async handleNoMatch(requestUrl) {
|
|
40
40
|
const isStaticFileRequest = ServerUtils.hasKnownExtension(requestUrl);
|
|
41
41
|
if (!isStaticFileRequest) {
|
|
42
|
-
return this.
|
|
42
|
+
return this.renderCustomNotFoundResponseOrServerError(requestUrl);
|
|
43
43
|
}
|
|
44
44
|
const relativeUrl = requestUrl.startsWith("/") ? requestUrl.slice(1) : requestUrl;
|
|
45
45
|
const filePath = path.join(this.assetPrefix, relativeUrl);
|
|
46
46
|
const contentType = ServerUtils.getContentType(filePath);
|
|
47
47
|
const response = await this.fileSystemResponseFactory.createFileResponse(filePath, contentType);
|
|
48
|
-
return response ?? this.
|
|
48
|
+
return response ?? this.renderCustomNotFoundResponseOrServerError(requestUrl);
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* Handles a matched file-system page route.
|
|
@@ -100,17 +100,13 @@ class FileSystemResponseMatcher {
|
|
|
100
100
|
return error;
|
|
101
101
|
}
|
|
102
102
|
if (error instanceof LocalsAccessError) {
|
|
103
|
-
return
|
|
104
|
-
status: 500,
|
|
105
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
if (error instanceof Error) {
|
|
109
|
-
if (isDevelopmentRuntime() || appLogger.isDebugEnabled()) {
|
|
110
|
-
appLogger.error(`[FileSystemResponseMatcher] ${error.message} at ${match.requestedPathname}`);
|
|
111
|
-
}
|
|
103
|
+
return this.createInternalServerErrorResponse(error.message, match.requestedPathname, error);
|
|
112
104
|
}
|
|
113
|
-
return this.
|
|
105
|
+
return this.createInternalServerErrorResponse(
|
|
106
|
+
error instanceof Error ? error.message : "Internal Server Error",
|
|
107
|
+
match.requestedPathname,
|
|
108
|
+
error
|
|
109
|
+
);
|
|
114
110
|
}
|
|
115
111
|
}
|
|
116
112
|
/**
|
|
@@ -119,12 +115,9 @@ class FileSystemResponseMatcher {
|
|
|
119
115
|
*/
|
|
120
116
|
async renderCustomNotFoundResponse() {
|
|
121
117
|
const error404TemplatePath = this.appConfig.absolutePaths.error404TemplatePath;
|
|
118
|
+
let routeRenderer;
|
|
122
119
|
try {
|
|
123
|
-
|
|
124
|
-
const result = await routeRenderer.execute({
|
|
125
|
-
file: error404TemplatePath
|
|
126
|
-
});
|
|
127
|
-
return this.fileSystemResponseFactory.createHtmlNotFoundResponse(result.body);
|
|
120
|
+
routeRenderer = this.routeRendererFactory.getPageRenderer(error404TemplatePath);
|
|
128
121
|
} catch {
|
|
129
122
|
appLogger.debug(
|
|
130
123
|
"Custom 404 template not found, falling back to default 404 response",
|
|
@@ -132,6 +125,35 @@ class FileSystemResponseMatcher {
|
|
|
132
125
|
);
|
|
133
126
|
return this.fileSystemResponseFactory.createDefaultNotFoundResponse();
|
|
134
127
|
}
|
|
128
|
+
const result = await routeRenderer.execute({
|
|
129
|
+
file: error404TemplatePath
|
|
130
|
+
});
|
|
131
|
+
return this.fileSystemResponseFactory.createHtmlNotFoundResponse(result.body);
|
|
132
|
+
}
|
|
133
|
+
async renderCustomNotFoundResponseOrServerError(pathname) {
|
|
134
|
+
try {
|
|
135
|
+
return await this.renderCustomNotFoundResponse();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof Response) {
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
return this.createInternalServerErrorResponse(
|
|
141
|
+
error instanceof Error ? error.message : "Internal Server Error",
|
|
142
|
+
pathname,
|
|
143
|
+
error
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
createInternalServerErrorResponse(message, pathname, error) {
|
|
148
|
+
if (isDevelopmentRuntime() || appLogger.isDebugEnabled()) {
|
|
149
|
+
appLogger.error(`[FileSystemResponseMatcher] ${message} at ${pathname}`, error);
|
|
150
|
+
} else {
|
|
151
|
+
appLogger.error(`[FileSystemResponseMatcher] Render error at ${pathname}`, error);
|
|
152
|
+
}
|
|
153
|
+
return new Response("Internal Server Error", {
|
|
154
|
+
status: 500,
|
|
155
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
156
|
+
});
|
|
135
157
|
}
|
|
136
158
|
async createExecutionPlan(match, request) {
|
|
137
159
|
const cacheKey = this.pageRequestCacheCoordinator.buildCacheKey({
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional helpers for apps that want a machine-readable "ready" log line
|
|
3
|
+
* (for example Playwright `webServer` stdout matching). Pass to `app.start(onAppStart)`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const ECOPAGES_SERVER_READY_MARKER = "[@ecopages/ready]";
|
|
6
|
+
export declare function formatServerReadyMessage(origin: string): string;
|
package/src/eco/README.md
CHANGED
|
@@ -279,6 +279,17 @@ export default eco.page({
|
|
|
279
279
|
});
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
+
**Nested layouts (outer → inner):**
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
export default eco.page({
|
|
286
|
+
layout: [MarketingShell, DocsShell],
|
|
287
|
+
render: () => <h1>Hello</h1>,
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
A single layout remains equivalent to a one-element array. Normalization stores the stack on `config.layouts` and `config.layoutEntries` at factory time.
|
|
292
|
+
|
|
282
293
|
### `eco.component()`
|
|
283
294
|
|
|
284
295
|
Define a reusable component with dependencies.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EcoDeclaredComponent } from '../types/public-types.js';
|
|
2
|
+
export { getUndeclaredComponentDependencyMessage } from '../errors/undeclared-component-dependency-error.js';
|
|
3
|
+
/**
|
|
4
|
+
* @remarks
|
|
5
|
+
* Declared components are produced by `eco.component()`, `eco.layout()`, and
|
|
6
|
+
* `eco.html()` after the component-meta plugin injects `config.__eco`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isEcoDeclaredComponent(component: unknown): component is EcoDeclaredComponent;
|
|
9
|
+
/**
|
|
10
|
+
* @throws {UndeclaredComponentDependencyError} When the value lacks `config.__eco`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assertEcoDeclaredComponent(component: unknown, context?: {
|
|
13
|
+
parentComponentFile?: string;
|
|
14
|
+
}): asserts component is EcoDeclaredComponent;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { UndeclaredComponentDependencyError } from "../errors/undeclared-component-dependency-error.js";
|
|
2
|
+
import { getUndeclaredComponentDependencyMessage } from "../errors/undeclared-component-dependency-error.js";
|
|
3
|
+
function isEcoDeclaredComponent(component) {
|
|
4
|
+
return typeof component === "function" && typeof component.config?.__eco?.file === "string" && typeof component.config?.__eco?.integration === "string";
|
|
5
|
+
}
|
|
6
|
+
function assertEcoDeclaredComponent(component, context = {}) {
|
|
7
|
+
if (isEcoDeclaredComponent(component)) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
throw new UndeclaredComponentDependencyError(context.parentComponentFile);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
assertEcoDeclaredComponent,
|
|
14
|
+
getUndeclaredComponentDependencyMessage,
|
|
15
|
+
isEcoDeclaredComponent
|
|
16
|
+
};
|
package/src/eco/eco.browser.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyPageLayoutConfig, mergeLayoutDependencies, normalizePageLayouts } from "./page-layout-normalization.js";
|
|
1
2
|
function createComponentFactory(options) {
|
|
2
3
|
const component2 = ((props) => options.render(props));
|
|
3
4
|
component2.config = {
|
|
@@ -32,17 +33,15 @@ function page(options) {
|
|
|
32
33
|
requires,
|
|
33
34
|
middleware
|
|
34
35
|
} = options;
|
|
36
|
+
const layoutEntries = normalizePageLayouts(pageLayout);
|
|
35
37
|
const pageComponent = createComponentFactory({
|
|
36
38
|
__eco: options.__eco,
|
|
37
39
|
integration: options.integration,
|
|
38
|
-
dependencies:
|
|
39
|
-
...dependencies,
|
|
40
|
-
components: [...dependencies?.components ?? [], pageLayout]
|
|
41
|
-
} : dependencies,
|
|
40
|
+
dependencies: mergeLayoutDependencies(dependencies, layoutEntries),
|
|
42
41
|
render
|
|
43
42
|
});
|
|
44
|
-
if (
|
|
45
|
-
pageComponent.config
|
|
43
|
+
if (pageComponent.config) {
|
|
44
|
+
applyPageLayoutConfig(pageComponent.config, layoutEntries);
|
|
46
45
|
}
|
|
47
46
|
if (staticPaths2) {
|
|
48
47
|
pageComponent.staticPaths = staticPaths2;
|
package/src/eco/eco.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
interceptForeignChild
|
|
5
5
|
} from "../route-renderer/orchestration/component-render-context.js";
|
|
6
6
|
import { isThenable } from "../route-renderer/orchestration/render-output.utils.js";
|
|
7
|
+
import { applyPageLayoutConfig, mergeLayoutDependencies, normalizePageLayouts } from "./page-layout-normalization.js";
|
|
7
8
|
function createComponentFactory(options) {
|
|
8
9
|
const integrationName = options.integration ?? options.__eco?.integration;
|
|
9
10
|
const comp = ((props) => {
|
|
@@ -74,18 +75,16 @@ function layout(options) {
|
|
|
74
75
|
}
|
|
75
76
|
function page(options) {
|
|
76
77
|
const { layout: layout2, dependencies, render, staticPaths: staticPaths2, staticProps: staticProps2, metadata: metadata2, cache, requires, middleware } = options;
|
|
78
|
+
const layoutEntries = normalizePageLayouts(layout2);
|
|
77
79
|
const componentOptions = {
|
|
78
80
|
__eco: options.__eco,
|
|
79
81
|
integration: options.integration,
|
|
80
|
-
dependencies:
|
|
81
|
-
...dependencies,
|
|
82
|
-
components: [...dependencies?.components || [], layout2]
|
|
83
|
-
} : dependencies,
|
|
82
|
+
dependencies: mergeLayoutDependencies(dependencies, layoutEntries),
|
|
84
83
|
render
|
|
85
84
|
};
|
|
86
85
|
const pageComponent = createComponentFactory(componentOptions);
|
|
87
|
-
if (
|
|
88
|
-
pageComponent.config
|
|
86
|
+
if (pageComponent.config) {
|
|
87
|
+
applyPageLayoutConfig(pageComponent.config, layoutEntries);
|
|
89
88
|
}
|
|
90
89
|
if (staticPaths2) pageComponent.staticPaths = staticPaths2;
|
|
91
90
|
if (staticProps2) pageComponent.staticProps = staticProps2;
|
package/src/eco/eco.types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Type definitions for the eco namespace API
|
|
3
3
|
* @module
|
|
4
4
|
*/
|
|
5
|
-
import type { DependencyLazyTrigger, EcoComponent, EcoComponentDependencies, EcoHtmlComponent, EcoInjectedMeta, EcoLayoutComponent,
|
|
5
|
+
import type { DependencyLazyTrigger, EcoComponent, EcoComponentDependencies, EcoHtmlComponent, EcoInjectedMeta, EcoLayoutComponent, EcoPageLayouts, EcoPagesElement, FileRouteMiddleware, GetMetadata, GetStaticPaths, GetStaticProps, HtmlTemplateProps, LayoutProps, RequestLocals, RequestPageContext } from '../types/public-types.js';
|
|
6
6
|
import type { CacheStrategy } from '../services/cache/cache.types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Extracts the props type from one eco component.
|
|
@@ -81,7 +81,7 @@ export interface PageOptionsBase<T, E = EcoPagesElement> {
|
|
|
81
81
|
__eco?: EcoInjectedMeta;
|
|
82
82
|
integration?: string;
|
|
83
83
|
dependencies?: EcoComponentDependencies;
|
|
84
|
-
layout?:
|
|
84
|
+
layout?: EcoPageLayouts<E>;
|
|
85
85
|
/**
|
|
86
86
|
* Define static paths for dynamic routes (e.g., [slug].tsx).
|
|
87
87
|
* Returns all possible paths that should be pre-rendered at build time.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EcoComponentConfig, EcoComponentDependencies, EcoPageLayoutEntry, EcoPageLayouts } from '../types/public-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes `layout` page options to an outer→inner stack of layout entries.
|
|
4
|
+
*/
|
|
5
|
+
export declare function normalizePageLayouts<E>(layout?: EcoPageLayouts<E>): EcoPageLayoutEntry<E>[];
|
|
6
|
+
/**
|
|
7
|
+
* Merges layout components into `dependencies.components` without duplicates.
|
|
8
|
+
*/
|
|
9
|
+
export declare function mergeLayoutDependencies(dependencies: EcoComponentDependencies | undefined, layoutEntries: EcoPageLayoutEntry[]): EcoComponentDependencies | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Writes normalized layout metadata onto a page `config`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyPageLayoutConfig(pageConfig: EcoComponentConfig, layoutEntries: EcoPageLayoutEntry[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* Migrates MDX-exported `config.layout` values onto normalized layout metadata.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* `eco.page({ layout })` already normalizes at factory time. MDX modules that
|
|
19
|
+
* export `config.layout` directly still need this import-time migration.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ensurePageConfigLayouts(config: EcoComponentConfig | undefined): EcoComponentConfig | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function isLayoutSpecObject(spec) {
|
|
2
|
+
return typeof spec === "object" && spec !== null && "component" in spec;
|
|
3
|
+
}
|
|
4
|
+
function normalizePageLayouts(layout) {
|
|
5
|
+
if (!layout) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
const specs = Array.isArray(layout) ? layout : [layout];
|
|
9
|
+
return specs.map((spec) => {
|
|
10
|
+
if (isLayoutSpecObject(spec)) {
|
|
11
|
+
return {
|
|
12
|
+
component: spec.component,
|
|
13
|
+
props: spec.props
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
component: spec
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function mergeLayoutDependencies(dependencies, layoutEntries) {
|
|
22
|
+
if (layoutEntries.length === 0) {
|
|
23
|
+
return dependencies;
|
|
24
|
+
}
|
|
25
|
+
const mergedComponents = [...dependencies?.components ?? []];
|
|
26
|
+
for (const entry of layoutEntries) {
|
|
27
|
+
if (!mergedComponents.includes(entry.component)) {
|
|
28
|
+
mergedComponents.push(entry.component);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...dependencies,
|
|
33
|
+
components: mergedComponents
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function applyPageLayoutConfig(pageConfig, layoutEntries) {
|
|
37
|
+
if (layoutEntries.length === 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
pageConfig.layouts = layoutEntries.map((entry) => entry.component);
|
|
41
|
+
pageConfig.layoutEntries = layoutEntries;
|
|
42
|
+
}
|
|
43
|
+
function ensurePageConfigLayouts(config) {
|
|
44
|
+
if (!config || config.layouts && config.layouts.length > 0) {
|
|
45
|
+
return config;
|
|
46
|
+
}
|
|
47
|
+
const legacyLayout = config.layout;
|
|
48
|
+
if (!legacyLayout) {
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
applyPageLayoutConfig(config, normalizePageLayouts(legacyLayout));
|
|
52
|
+
return config;
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
applyPageLayoutConfig,
|
|
56
|
+
ensurePageConfigLayouts,
|
|
57
|
+
mergeLayoutDependencies,
|
|
58
|
+
normalizePageLayouts
|
|
59
|
+
};
|
package/src/errors/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { HttpError } from './http-error.js';
|
|
2
2
|
export type { HttpErrorDetails, HttpErrorJson } from './http-error.js';
|
|
3
3
|
export { LocalsAccessError } from './locals-access-error.js';
|
|
4
|
+
export { UndeclaredComponentDependencyError, getUndeclaredComponentDependencyMessage, } from './undeclared-component-dependency-error.js';
|
package/src/errors/index.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { HttpError } from "./http-error.js";
|
|
2
2
|
import { LocalsAccessError } from "./locals-access-error.js";
|
|
3
|
+
import {
|
|
4
|
+
UndeclaredComponentDependencyError,
|
|
5
|
+
getUndeclaredComponentDependencyMessage
|
|
6
|
+
} from "./undeclared-component-dependency-error.js";
|
|
3
7
|
export {
|
|
4
8
|
HttpError,
|
|
5
|
-
LocalsAccessError
|
|
9
|
+
LocalsAccessError,
|
|
10
|
+
UndeclaredComponentDependencyError,
|
|
11
|
+
getUndeclaredComponentDependencyMessage
|
|
6
12
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function getUndeclaredComponentDependencyMessage(parentComponentFile?: string): string;
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when `dependencies.components` contains a plain function or component
|
|
4
|
+
* without plugin-injected `config.__eco` metadata.
|
|
5
|
+
*/
|
|
6
|
+
export declare class UndeclaredComponentDependencyError extends Error {
|
|
7
|
+
name: string;
|
|
8
|
+
/** Parent component file path when the invalid entry was declared. */
|
|
9
|
+
readonly parentComponentFile?: string;
|
|
10
|
+
constructor(parentComponentFile?: string);
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function getUndeclaredComponentDependencyMessage(parentComponentFile) {
|
|
2
|
+
const parentHint = parentComponentFile ? ` (declared by ${parentComponentFile})` : "";
|
|
3
|
+
return `[ecopages] dependencies.components entries must be eco.component(), eco.layout(), or eco.html() components with __eco metadata${parentHint}. Plain functions and untyped components are not valid dependency entries.`;
|
|
4
|
+
}
|
|
5
|
+
class UndeclaredComponentDependencyError extends Error {
|
|
6
|
+
name = "UndeclaredComponentDependencyError";
|
|
7
|
+
/** Parent component file path when the invalid entry was declared. */
|
|
8
|
+
parentComponentFile;
|
|
9
|
+
constructor(parentComponentFile) {
|
|
10
|
+
super(getUndeclaredComponentDependencyMessage(parentComponentFile));
|
|
11
|
+
this.parentComponentFile = parentComponentFile;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
UndeclaredComponentDependencyError,
|
|
16
|
+
getUndeclaredComponentDependencyMessage
|
|
17
|
+
};
|
|
@@ -25,8 +25,12 @@ function walkComponentGraph(input) {
|
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
if (input.visitLayout
|
|
29
|
-
|
|
28
|
+
if (input.visitLayout) {
|
|
29
|
+
for (const layout of ecoComponent.config?.layouts ?? []) {
|
|
30
|
+
if (layout?.config) {
|
|
31
|
+
visitConfig(layout.config, integrationName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
for (const child of ecoComponent.config?.dependencies?.components ?? []) {
|
|
32
36
|
visitComponent(child, integrationName, "dependency");
|
|
@@ -40,8 +44,12 @@ function walkComponentGraph(input) {
|
|
|
40
44
|
if (input.onConfig) {
|
|
41
45
|
input.onConfig(config);
|
|
42
46
|
}
|
|
43
|
-
if (input.visitLayout
|
|
44
|
-
|
|
47
|
+
if (input.visitLayout) {
|
|
48
|
+
for (const layout of config.layouts ?? []) {
|
|
49
|
+
if (layout?.config) {
|
|
50
|
+
visitConfig(layout.config, parentIntegrationName);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
for (const child of config.dependencies?.components ?? []) {
|
|
47
55
|
visitComponent(child, parentIntegrationName, "dependency");
|
|
@@ -1,13 +1,43 @@
|
|
|
1
|
-
import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, PageMetadataProps } from '../../types/public-types.js';
|
|
1
|
+
import type { BaseIntegrationContext, ComponentRenderInput, ComponentRenderResult, EcoComponent, PageMetadataProps } from '../../types/public-types.js';
|
|
2
2
|
import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.js';
|
|
3
3
|
export type DocumentShellLayoutInput = {
|
|
4
4
|
component: EcoComponent;
|
|
5
5
|
props?: Record<string, unknown>;
|
|
6
6
|
};
|
|
7
|
+
export type DocumentShellComposeChildrenContext = {
|
|
8
|
+
primaryComponent: EcoComponent;
|
|
9
|
+
primaryProps: Record<string, unknown>;
|
|
10
|
+
primaryRender?: ComponentRenderResult;
|
|
11
|
+
layouts: DocumentShellLayoutInput[];
|
|
12
|
+
rendererCache: BaseIntegrationContext['rendererCache'];
|
|
13
|
+
renderComponentWithForeignChildren: DocumentShellRenderDependencies['renderComponentWithForeignChildren'];
|
|
14
|
+
};
|
|
15
|
+
export type DocumentShellComposeChildrenResult = {
|
|
16
|
+
/** Serialized or element children passed to the HTML template shell. */
|
|
17
|
+
children: unknown;
|
|
18
|
+
layoutRenders: ComponentRenderResult[];
|
|
19
|
+
/**
|
|
20
|
+
* Primary/page render assets when the hook performs unified composition.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* Omit only when the default sequential string path runs (core renders primary first).
|
|
24
|
+
*/
|
|
25
|
+
primaryRender?: ComponentRenderResult;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Optional hook that replaces default sequential string layout wrapping.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* When provided, core skips the initial primary render and expects the hook to return
|
|
32
|
+
* `children` for the HTML template. Supply `primaryRender` so dependency assets still merge.
|
|
33
|
+
*/
|
|
34
|
+
export type DocumentShellComposeChildrenHook = (context: DocumentShellComposeChildrenContext) => Promise<DocumentShellComposeChildrenResult>;
|
|
7
35
|
export type DocumentShellComposeInput = {
|
|
8
36
|
primaryComponent: EcoComponent;
|
|
9
37
|
primaryProps: Record<string, unknown>;
|
|
10
38
|
layout?: DocumentShellLayoutInput;
|
|
39
|
+
layouts?: DocumentShellLayoutInput[];
|
|
40
|
+
composeChildren?: DocumentShellComposeChildrenHook;
|
|
11
41
|
htmlTemplate: EcoComponent;
|
|
12
42
|
documentProps: Record<string, unknown>;
|
|
13
43
|
};
|
|
@@ -17,6 +47,8 @@ export type DocumentShellPageRenderInput = {
|
|
|
17
47
|
props: Record<string, unknown>;
|
|
18
48
|
};
|
|
19
49
|
layout?: DocumentShellLayoutInput;
|
|
50
|
+
layouts?: DocumentShellLayoutInput[];
|
|
51
|
+
composeChildren?: DocumentShellComposeChildrenHook;
|
|
20
52
|
htmlTemplate: EcoComponent;
|
|
21
53
|
metadata: PageMetadataProps;
|
|
22
54
|
pageProps: Record<string, unknown>;
|
|
@@ -31,6 +63,10 @@ export type DocumentShellAttributeStamping = {
|
|
|
31
63
|
applyAttributesToFirstBodyElement(html: string, attributes: Record<string, string>): string;
|
|
32
64
|
applyAttributesToHtmlElement(html: string, attributes: Record<string, string>): string;
|
|
33
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Default sequential string-child composition for document shell layers.
|
|
68
|
+
*/
|
|
69
|
+
export declare function composeSequentialLayoutChildren(context: DocumentShellComposeChildrenContext): Promise<DocumentShellComposeChildrenResult>;
|
|
34
70
|
/**
|
|
35
71
|
* Composes page or view content through optional layout and document shells.
|
|
36
72
|
*
|
|
@@ -1,23 +1,86 @@
|
|
|
1
|
+
function resolveDocumentShellLayouts(input) {
|
|
2
|
+
if (input.layouts && input.layouts.length > 0) {
|
|
3
|
+
return input.layouts;
|
|
4
|
+
}
|
|
5
|
+
return input.layout ? [input.layout] : [];
|
|
6
|
+
}
|
|
7
|
+
function resolveComponentIntegrationName(component) {
|
|
8
|
+
return component.config?.integration ?? component.config?.__eco?.integration ?? "ecopages";
|
|
9
|
+
}
|
|
10
|
+
async function composeSequentialLayoutChildren(context) {
|
|
11
|
+
if (!context.primaryRender) {
|
|
12
|
+
throw new Error("[ecopages] composeSequentialLayoutChildren requires primaryRender.");
|
|
13
|
+
}
|
|
14
|
+
let children = context.primaryRender.html;
|
|
15
|
+
const layoutRenders = [];
|
|
16
|
+
for (const layout of [...context.layouts].reverse()) {
|
|
17
|
+
const layoutRender = await context.renderComponentWithForeignChildren({
|
|
18
|
+
component: layout.component,
|
|
19
|
+
props: layout.props ?? {},
|
|
20
|
+
children,
|
|
21
|
+
integrationContext: { rendererCache: context.rendererCache }
|
|
22
|
+
});
|
|
23
|
+
layoutRenders.push(layoutRender);
|
|
24
|
+
children = layoutRender.html;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
children,
|
|
28
|
+
layoutRenders
|
|
29
|
+
};
|
|
30
|
+
}
|
|
1
31
|
async function composeDocumentShell(dependencies, input) {
|
|
2
32
|
const rendererCache = /* @__PURE__ */ new Map();
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
const layouts = resolveDocumentShellLayouts(input);
|
|
34
|
+
const composeChildren = input.composeChildren ?? composeSequentialLayoutChildren;
|
|
35
|
+
const usesCustomComposeChildren = input.composeChildren !== void 0;
|
|
36
|
+
let primaryRender;
|
|
37
|
+
let children;
|
|
38
|
+
let layoutRenders;
|
|
39
|
+
if (usesCustomComposeChildren) {
|
|
40
|
+
const composed = await composeChildren({
|
|
41
|
+
primaryComponent: input.primaryComponent,
|
|
42
|
+
primaryProps: input.primaryProps,
|
|
43
|
+
layouts,
|
|
44
|
+
rendererCache,
|
|
45
|
+
renderComponentWithForeignChildren: dependencies.renderComponentWithForeignChildren
|
|
46
|
+
});
|
|
47
|
+
children = composed.children;
|
|
48
|
+
layoutRenders = composed.layoutRenders;
|
|
49
|
+
primaryRender = composed.primaryRender ?? {
|
|
50
|
+
html: typeof composed.children === "string" ? composed.children : "",
|
|
51
|
+
assets: [],
|
|
52
|
+
canAttachAttributes: true,
|
|
53
|
+
rootTag: "main",
|
|
54
|
+
integrationName: resolveComponentIntegrationName(input.primaryComponent)
|
|
55
|
+
};
|
|
56
|
+
} else {
|
|
57
|
+
primaryRender = await dependencies.renderComponentWithForeignChildren({
|
|
58
|
+
component: input.primaryComponent,
|
|
59
|
+
props: input.primaryProps,
|
|
60
|
+
integrationContext: { rendererCache }
|
|
61
|
+
});
|
|
62
|
+
const composed = await composeChildren({
|
|
63
|
+
primaryComponent: input.primaryComponent,
|
|
64
|
+
primaryProps: input.primaryProps,
|
|
65
|
+
primaryRender,
|
|
66
|
+
layouts,
|
|
67
|
+
rendererCache,
|
|
68
|
+
renderComponentWithForeignChildren: dependencies.renderComponentWithForeignChildren
|
|
69
|
+
});
|
|
70
|
+
children = composed.children;
|
|
71
|
+
layoutRenders = composed.layoutRenders;
|
|
72
|
+
}
|
|
14
73
|
const documentRender = await dependencies.renderComponentWithForeignChildren({
|
|
15
74
|
component: input.htmlTemplate,
|
|
16
75
|
props: input.documentProps,
|
|
17
|
-
children
|
|
76
|
+
children,
|
|
18
77
|
integrationContext: { rendererCache }
|
|
19
78
|
});
|
|
20
|
-
dependencies.appendProcessedDependencies(
|
|
79
|
+
dependencies.appendProcessedDependencies(
|
|
80
|
+
primaryRender.assets,
|
|
81
|
+
...layoutRenders.map((layoutRender) => layoutRender.assets),
|
|
82
|
+
documentRender.assets
|
|
83
|
+
);
|
|
21
84
|
return {
|
|
22
85
|
documentHtml: documentRender.html
|
|
23
86
|
};
|
|
@@ -37,6 +100,8 @@ async function renderPageDocumentShell(dependencies, input, docType) {
|
|
|
37
100
|
primaryComponent: input.page.component,
|
|
38
101
|
primaryProps: input.page.props,
|
|
39
102
|
layout: input.layout,
|
|
103
|
+
layouts: input.layouts,
|
|
104
|
+
composeChildren: input.composeChildren,
|
|
40
105
|
htmlTemplate: input.htmlTemplate,
|
|
41
106
|
documentProps: {
|
|
42
107
|
metadata: input.metadata,
|
|
@@ -50,5 +115,6 @@ async function renderPageDocumentShell(dependencies, input, docType) {
|
|
|
50
115
|
export {
|
|
51
116
|
applyDocumentShellAttributeStamping,
|
|
52
117
|
composeDocumentShell,
|
|
118
|
+
composeSequentialLayoutChildren,
|
|
53
119
|
renderPageDocumentShell
|
|
54
120
|
};
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
composeDocumentShell,
|
|
24
24
|
renderPageDocumentShell
|
|
25
25
|
} from "./document-shell-render.service.js";
|
|
26
|
+
import { resolveInnermostPageLayout, resolvePageLayoutComponents } from "./layout-shell-props.service.js";
|
|
26
27
|
class IntegrationRenderer {
|
|
27
28
|
appConfig;
|
|
28
29
|
assetProcessingService;
|
|
@@ -594,7 +595,8 @@ class IntegrationRenderer {
|
|
|
594
595
|
});
|
|
595
596
|
const { Page, integrationSpecificProps } = pageModule;
|
|
596
597
|
const HtmlTemplate = await this.getHtmlTemplate();
|
|
597
|
-
const
|
|
598
|
+
const Layouts = resolvePageLayoutComponents(Page.config?.layouts);
|
|
599
|
+
const Layout = resolveInnermostPageLayout(Layouts);
|
|
598
600
|
const { props, metadata } = await this.pageModuleLoaderService.resolvePageData({
|
|
599
601
|
pageModule,
|
|
600
602
|
routeOptions
|
|
@@ -602,7 +604,9 @@ class IntegrationRenderer {
|
|
|
602
604
|
return {
|
|
603
605
|
Page,
|
|
604
606
|
HtmlTemplate,
|
|
607
|
+
Layouts,
|
|
605
608
|
Layout,
|
|
609
|
+
layoutEntries: Page.config?.layoutEntries,
|
|
606
610
|
props,
|
|
607
611
|
metadata,
|
|
608
612
|
integrationSpecificProps
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EcoPageLayoutEntry, LayoutPropsContext } from '../../types/public-types.js';
|
|
2
|
+
export type LayoutShellPropsContext = LayoutPropsContext & {
|
|
3
|
+
pageProps?: Record<string, unknown>;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the default shell props passed to a layout tier during route render.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveLayoutShellProps(context: LayoutShellPropsContext): Record<string, unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Resolves props for one layout entry, merging shell props with an optional factory.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveLayoutEntryProps(entry: EcoPageLayoutEntry, context: LayoutShellPropsContext): Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Reads the normalized layout stack from a page component config.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolvePageLayoutComponents(layouts?: EcoPageLayoutEntry['component'][]): EcoPageLayoutEntry['component'][];
|
|
17
|
+
/**
|
|
18
|
+
* Returns the innermost layout component from a normalized page layout stack.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveInnermostPageLayout(layouts?: EcoPageLayoutEntry['component'][]): EcoPageLayoutEntry['component'] | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function resolveLayoutShellProps(context) {
|
|
2
|
+
return context.locals ? { locals: context.locals } : {};
|
|
3
|
+
}
|
|
4
|
+
function resolveLayoutEntryProps(entry, context) {
|
|
5
|
+
const shellProps = resolveLayoutShellProps(context);
|
|
6
|
+
if (!entry.props) {
|
|
7
|
+
return shellProps;
|
|
8
|
+
}
|
|
9
|
+
const layoutPropsContext = {
|
|
10
|
+
params: context.params,
|
|
11
|
+
query: context.query,
|
|
12
|
+
locals: context.locals
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
...shellProps,
|
|
16
|
+
...entry.props(layoutPropsContext)
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function resolvePageLayoutComponents(layouts) {
|
|
20
|
+
return layouts && layouts.length > 0 ? layouts : [];
|
|
21
|
+
}
|
|
22
|
+
function resolveInnermostPageLayout(layouts) {
|
|
23
|
+
const stack = resolvePageLayoutComponents(layouts);
|
|
24
|
+
return stack[stack.length - 1];
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
resolveInnermostPageLayout,
|
|
28
|
+
resolveLayoutEntryProps,
|
|
29
|
+
resolveLayoutShellProps,
|
|
30
|
+
resolvePageLayoutComponents
|
|
31
|
+
};
|