@ecopages/core 0.2.0-alpha.25 → 0.2.0-alpha.27
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 +63 -7
- package/package.json +4 -47
- package/src/adapters/bun/create-app.ts +54 -2
- package/src/adapters/bun/hmr-manager.test.ts +0 -2
- package/src/adapters/bun/hmr-manager.ts +1 -24
- package/src/adapters/bun/server-adapter.ts +30 -4
- package/src/adapters/node/node-hmr-manager.test.ts +0 -2
- package/src/adapters/node/node-hmr-manager.ts +2 -25
- package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
- package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
- package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
- package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
- package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
- package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
- package/src/adapters/shared/fs-server-response-factory.ts +15 -37
- package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
- package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
- package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
- package/src/adapters/shared/render-context.ts +3 -3
- package/src/adapters/shared/server-adapter.test.ts +53 -0
- package/src/adapters/shared/server-adapter.ts +228 -159
- package/src/adapters/shared/server-route-handler.test.ts +6 -5
- package/src/adapters/shared/server-route-handler.ts +4 -4
- package/src/adapters/shared/server-static-builder.test.ts +4 -4
- package/src/adapters/shared/server-static-builder.ts +4 -4
- package/src/config/README.md +1 -1
- package/src/config/config-builder.test.ts +0 -1
- package/src/config/config-builder.ts +2 -7
- package/src/dev/host-runtime.ts +34 -0
- package/src/eco/eco.browser.test.ts +2 -2
- package/src/eco/eco.browser.ts +2 -2
- package/src/eco/eco.test.ts +6 -6
- package/src/eco/eco.ts +12 -12
- package/src/eco/eco.types.ts +3 -3
- package/src/errors/index.ts +1 -0
- package/src/hmr/client/hmr-runtime.ts +4 -2
- package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
- package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
- package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
- package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
- package/src/plugins/eco-component-meta-plugin.ts +0 -1
- package/src/plugins/integration-plugin.test.ts +9 -14
- package/src/plugins/integration-plugin.ts +34 -22
- package/src/plugins/processor.ts +17 -0
- package/src/route-renderer/GRAPH.md +81 -289
- package/src/route-renderer/README.md +67 -105
- package/src/route-renderer/orchestration/component-render-context.ts +45 -38
- package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
- package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
- package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
- package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
- package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
- package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
- package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
- package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
- package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
- package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
- package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
- package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
- package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
- package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
- package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
- package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
- package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
- package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
- package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
- package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
- package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
- package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
- package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
- package/src/route-renderer/route-renderer.ts +28 -31
- package/src/router/README.md +16 -19
- package/src/router/server/route-registry.test.ts +176 -0
- package/src/router/server/route-registry.ts +382 -0
- package/src/services/README.md +1 -2
- package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
- package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
- package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
- package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
- package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
- package/src/services/assets/asset-processing-service/index.ts +1 -0
- package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
- package/src/services/assets/asset-processing-service/page-package.ts +93 -0
- package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
- package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
- package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
- package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
- package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
- package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
- package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
- package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
- package/src/services/html/html-transformer.service.test.ts +1 -4
- package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
- package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
- package/src/services/module-loading/page-module-import.service.ts +0 -1
- package/src/services/module-loading/source-module-support.ts +1 -1
- package/src/static-site-generator/static-site-generator.test.ts +124 -32
- package/src/static-site-generator/static-site-generator.ts +168 -185
- package/src/types/internal-types.ts +13 -12
- package/src/types/public-types.ts +55 -39
- package/src/watchers/project-watcher.test-helpers.ts +4 -3
- package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
- package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
- package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
- package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
- package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
- package/src/router/server/fs-router-scanner.test.ts +0 -83
- package/src/router/server/fs-router-scanner.ts +0 -224
- package/src/router/server/fs-router.test.ts +0 -214
- package/src/router/server/fs-router.ts +0 -122
- package/src/services/runtime-state/runtime-specifier-registry.service.ts +0 -96
|
@@ -6,151 +6,113 @@ This folder contains the core rendering orchestration for Ecopages.
|
|
|
6
6
|
|
|
7
7
|
The route renderer layer is responsible for:
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
9
|
+
- selecting the correct integration renderer for a route
|
|
10
|
+
- loading page modules and resolving static data or metadata
|
|
11
|
+
- resolving component dependencies and page browser assets
|
|
12
|
+
- coordinating mixed-integration rendering
|
|
13
|
+
- emitting final HTML plus cache strategy
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Core Concepts
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The architecture is organized around three distinct concepts:
|
|
18
18
|
|
|
19
|
-
- `
|
|
20
|
-
- `
|
|
19
|
+
- `ownership`: preparation-time metadata that describes which integration owns each declared component edge
|
|
20
|
+
- `foreign child`: a component encountered during render whose owning integration differs from the current integration
|
|
21
|
+
- `foreign subtree`: the resolved HTML, assets, and root-attachment metadata returned by the owning renderer for that foreign child
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
These concepts intentionally live in different places:
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
- `ownership-planning.service.ts` builds `ownershipPlan`
|
|
26
|
+
- `ownership-validation.service.ts` validates declared foreign ownership up front
|
|
27
|
+
- `component-render-context.ts` intercepts foreign children during active component render
|
|
28
|
+
- `queued-foreign-subtree-resolution.service.ts` resolves queued foreign subtrees for renderers that cannot hand them off inline
|
|
29
|
+
- `integration-renderer.ts` owns renderer-to-renderer delegation and shared shell composition
|
|
30
|
+
- `route-render-flow.ts` owns route preparation, final response capture, and unresolved artifact enforcement
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
- `render-preparation.service.ts`: page module/data/dependency preparation before render.
|
|
28
|
-
- `render-execution.service.ts`: render capture, unresolved boundary artifact enforcement, and finalization.
|
|
29
|
-
- `route-shell-composer.service.ts`: shared page/view/layout/html-template shell composition used by multiple integrations.
|
|
30
|
-
- `queued-boundary-runtime.service.ts`: shared queued foreign-boundary runtime used directly by renderer-owned helpers, including string-first renderers.
|
|
32
|
+
## Main Files
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
### `route-renderer.ts`
|
|
33
35
|
|
|
34
|
-
- `
|
|
35
|
-
- `
|
|
36
|
-
- deferred boundary resolution for nested cross-integration component boundaries.
|
|
36
|
+
- `RouteRendererFactory` chooses integration renderers from route files
|
|
37
|
+
- `RouteRenderer` delegates execution to the selected renderer
|
|
37
38
|
|
|
38
|
-
###
|
|
39
|
+
### `orchestration/`
|
|
39
40
|
|
|
40
|
-
|
|
41
|
+
- `route-render-flow.ts`: one route render from page-module loading through final HTML output
|
|
42
|
+
- `integration-renderer.ts`: abstract base class for route rendering, explicit view rendering, and foreign-child delegation
|
|
43
|
+
- `ownership-planning.service.ts`: declared ownership graph construction
|
|
44
|
+
- `ownership-validation.service.ts`: up-front ownership validation for route roots and declared descendants
|
|
45
|
+
- `component-render-context.ts`: active render context used by `eco.component()` to intercept foreign children
|
|
46
|
+
- `queued-foreign-subtree-resolution.service.ts`: queue resolution for renderers that need token-based foreign-subtree handoff
|
|
47
|
+
- `render-output.utils.ts`: unresolved artifact normalization and marker inspection
|
|
41
48
|
|
|
42
49
|
### `page-loading/`
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
- `importPageFile()` runtime-aware loading.
|
|
47
|
-
- `resolvePageModule()` normalizes exports and statics.
|
|
48
|
-
- `resolvePageData()` resolves static props then metadata.
|
|
49
|
-
|
|
50
|
-
Builds processed assets from component dependency declarations:
|
|
51
|
+
- `page-module-loader.ts`: imports page modules and normalizes page exports
|
|
52
|
+
- `dependency-resolver.ts`: resolves component dependencies and browser-facing assets
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
- Lazy dependency grouping and trigger derivation.
|
|
54
|
-
- Default global injector behavior with optional legacy scripts-injector compatibility.
|
|
54
|
+
## Render Flow
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
The route-render contract is:
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
The current mixed-renderer contract has four phases:
|
|
66
|
-
|
|
67
|
-
1. `render-preparation.service.ts` builds the route inputs and a conservative `boundaryPlan` from declared component dependencies.
|
|
68
|
-
2. The selected integration renderer owns page, layout, document-shell, and explicit-view composition for that route.
|
|
69
|
-
3. `route-shell-composer.service.ts` applies the shared page/view/layout/html-template composition flow while calling back into the owning renderer for each boundary render.
|
|
70
|
-
4. Renderer-owned boundary runtimes resolve foreign nested components through the owning renderer and exchange a compatibility `renderBoundary()` payload with explicit attachment-policy semantics.
|
|
71
|
-
5. `render-execution.service.ts` finalizes the response and fails if unresolved boundary artifact HTML survives the renderer-owned resolution pass.
|
|
58
|
+
1. `RouteRendererFactory` selects the owning integration renderer.
|
|
59
|
+
2. `IntegrationRenderer.execute()` delegates preparation and finalization to `RouteRenderFlow`.
|
|
60
|
+
3. `RouteRenderFlow.prepareRenderOptions()` loads the page module, validates ownership, builds `ownershipPlan`, resolves page data, resolves dependencies, and builds the page browser graph.
|
|
61
|
+
4. The integration renderer performs page, layout, and document-shell rendering. When it encounters a foreign child, it delegates that child back to the owning renderer.
|
|
62
|
+
5. If a renderer needs queued handoff, it emits internal foreign-subtree tokens and resolves them before returning final HTML.
|
|
63
|
+
6. `RouteRenderFlow.execute()` captures the final body, rejects unresolved `<eco-marker>` artifacts, stamps root or document attributes when needed, and runs the HTML transformer.
|
|
72
64
|
|
|
73
65
|
Important:
|
|
74
66
|
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
67
|
+
- route-level fallback resolution is gone; unresolved artifacts are now a hard failure
|
|
68
|
+
- ownership is declared from component metadata, not inferred from final HTML
|
|
69
|
+
- same-integration children stay renderer-local and do not need to pass through a universal transport
|
|
78
70
|
|
|
79
71
|
## Declared Foreign Child Contract
|
|
80
72
|
|
|
81
|
-
Mixed-integration component configs must declare every possible foreign child in `config.dependencies.components`.
|
|
82
|
-
|
|
83
|
-
Current behavior:
|
|
73
|
+
Mixed-integration component configs must declare every possible foreign child in `config.dependencies.components`.
|
|
84
74
|
|
|
85
|
-
|
|
86
|
-
- Renderer-owned runtime discovery still resolves actual foreign descendants during render.
|
|
87
|
-
- If unresolved boundary artifact HTML reaches route finalization, Ecopages throws instead of attempting a route-level recovery pass.
|
|
75
|
+
That declaration is used for two things:
|
|
88
76
|
|
|
89
|
-
|
|
77
|
+
- `OwnershipValidationService` surfaces missing metadata or unknown integrations before render execution
|
|
78
|
+
- `OwnershipPlanningService` records the expected ownership shape in `ownershipPlan`
|
|
90
79
|
|
|
91
|
-
|
|
92
|
-
- On `eco:after-swap`, it prunes stale `ecopages/global-injector-map` scripts and calls `refresh()` so newly swapped `data-eco-trigger` elements can bind their lazy rules.
|
|
93
|
-
- It must not call injector `cleanup()` on every swap, because that permanently disables future refresh work for the current runtime instance.
|
|
80
|
+
At runtime, renderers still discover actual foreign children through the active component render context.
|
|
94
81
|
|
|
95
|
-
##
|
|
82
|
+
## Foreign Subtree Contract
|
|
96
83
|
|
|
97
|
-
|
|
84
|
+
`renderForeignSubtree()` is the compatibility contract for renderer-to-renderer handoff. It returns:
|
|
98
85
|
|
|
99
86
|
- `html`
|
|
100
87
|
- `assets`
|
|
101
88
|
- `rootTag`
|
|
102
|
-
- `integrationName`
|
|
103
89
|
- optional `rootAttributes`
|
|
104
90
|
- `attachmentPolicy`
|
|
105
|
-
|
|
106
|
-
`renderComponent()` still returns `ComponentRenderResult` internally, including `canAttachAttributes`, because renderer-local implementations have not been collapsed into one universal boundary primitive.
|
|
107
|
-
|
|
108
|
-
Base orchestration uses the compatibility payload to:
|
|
109
|
-
|
|
110
|
-
- keep queued foreign-boundary resolution renderer-owned
|
|
111
|
-
- apply root attributes only when `attachmentPolicy.kind === 'first-element'`
|
|
112
|
-
- preserve asset bubbling through the normal dependency pipeline
|
|
113
|
-
|
|
114
|
-
The lower-level `ComponentRenderResult` currently includes:
|
|
115
|
-
|
|
116
|
-
- `html`
|
|
117
|
-
- `canAttachAttributes`
|
|
118
|
-
- `rootTag`
|
|
119
91
|
- `integrationName`
|
|
120
|
-
- optional `rootAttributes`
|
|
121
|
-
- optional `assets`
|
|
122
|
-
|
|
123
|
-
When rendered output still contains unresolved boundary artifact HTML:
|
|
124
92
|
|
|
125
|
-
|
|
126
|
-
- renderer-owned boundary runtimes are responsible for resolving foreign nested components before final route HTML is returned.
|
|
93
|
+
`renderComponentWithForeignChildren()` is the higher-level renderer entrypoint. It is responsible for:
|
|
127
94
|
|
|
128
|
-
|
|
95
|
+
- reusing the execution-scoped owning-renderer cache
|
|
96
|
+
- deciding whether the current component can stay local
|
|
97
|
+
- creating a foreign-child runtime when nested foreign ownership must be resolved
|
|
98
|
+
- normalizing unresolved artifact HTML before the render leaves the renderer
|
|
129
99
|
|
|
130
|
-
##
|
|
100
|
+
## Queue Model
|
|
131
101
|
|
|
132
|
-
|
|
133
|
-
- The React integration attaches `data-eco-component-id` on the component's own SSR root element when a single root is available.
|
|
134
|
-
- Island client bootstrap mounts with `createRoot()` into that root boundary.
|
|
135
|
-
- The emitted hydration bootstrap also listens for `eco:after-swap` so islands hydrate correctly when their SSR markup appears after client-side navigation.
|
|
136
|
-
- React hydration bootstraps are emitted with `data-eco-rerun` and stable `data-eco-script-id` metadata so head-script reconciliation can safely re-execute them when needed.
|
|
137
|
-
- This keeps authored DOM shape stable for global layout/style selectors while preserving per-island runtime isolation.
|
|
102
|
+
Not every integration needs queue-based handoff.
|
|
138
103
|
|
|
139
|
-
|
|
104
|
+
- If a renderer can resolve a foreign child inline, it returns resolved output immediately.
|
|
105
|
+
- If it cannot, it may emit internal foreign-subtree tokens and resolve them before returning final HTML.
|
|
106
|
+
- The queue service is only for renderer-owned transport inside one render pass. It is not a general route-level fallback mechanism.
|
|
140
107
|
|
|
141
|
-
|
|
142
|
-
2. Integration renderer prepares render options.
|
|
143
|
-
3. Dependencies are resolved and processed.
|
|
144
|
-
4. Orchestration assets/artifacts are merged.
|
|
145
|
-
5. Integration renderer generates HTML body.
|
|
146
|
-
6. HTML transformer injects head/body dependencies.
|
|
147
|
-
7. Route result returns body + metadata + cache strategy.
|
|
108
|
+
## React Island Notes
|
|
148
109
|
|
|
149
|
-
|
|
110
|
+
- React islands are emitted without synthetic wrapper elements.
|
|
111
|
+
- The React integration attaches `data-eco-component-id` to the SSR root when a single root exists.
|
|
112
|
+
- The island bootstrap mounts with `createRoot()` into that SSR root.
|
|
113
|
+
- Hydration bootstraps listen for `eco:after-swap` so islands hydrate after client-side navigation.
|
|
150
114
|
|
|
151
|
-
|
|
115
|
+
## Current Limits
|
|
152
116
|
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
- `boundaryPlan` is currently preparation-time metadata and diagnostics. It does not yet drive a full route-composer execution model.
|
|
156
|
-
- A narrow `RouteShellComposer` now owns shared shell composition, but a broader route composer that absorbs boundary ownership or execution flow is still deferred.
|
|
117
|
+
- `ownershipPlan` is still diagnostic and preparatory metadata; it does not yet drive a full route-composer execution model.
|
|
118
|
+
- Different integrations still own different foreign-child runtime strategies, which is intentional where child transport or hydration behavior differs.
|
|
@@ -2,18 +2,18 @@ import type { EcoComponent } from '../../types/public-types.ts';
|
|
|
2
2
|
import { addTriggerAttribute, isThenable, wrapWithScriptsInjector } from './render-output.utils.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Result returned by a renderer-owned
|
|
5
|
+
* Result returned by a renderer-owned foreign-child runtime.
|
|
6
6
|
*
|
|
7
7
|
* `inline` keeps rendering inside the current integration. `resolved` returns a
|
|
8
8
|
* renderer-owned value immediately, which can be final HTML or a renderer-local
|
|
9
9
|
* transport token for later queue resolution.
|
|
10
10
|
*/
|
|
11
|
-
export type
|
|
11
|
+
export type ForeignChildInterceptionResult = { kind: 'inline' } | { kind: 'resolved'; value: unknown };
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Foreign-child metadata passed into the active renderer-owned runtime.
|
|
15
15
|
*/
|
|
16
|
-
export type
|
|
16
|
+
export type ForeignChildInterceptionInput = {
|
|
17
17
|
currentIntegration: string;
|
|
18
18
|
targetIntegration?: string;
|
|
19
19
|
component: EcoComponent;
|
|
@@ -21,20 +21,26 @@ export type ComponentBoundaryInterceptionInput = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Narrow renderer-owned
|
|
24
|
+
* Narrow renderer-owned foreign-child runtime injected into one active render
|
|
25
25
|
* context.
|
|
26
26
|
*
|
|
27
|
-
* Integrations implement this contract when foreign
|
|
27
|
+
* Integrations implement this contract when foreign children must be handed
|
|
28
28
|
* off inside the renderer instead of being left for route-level reconciliation.
|
|
29
29
|
*/
|
|
30
|
-
export interface
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
export interface ForeignChildRuntime {
|
|
31
|
+
/**
|
|
32
|
+
* Intercepts one foreign child during an active render.
|
|
33
|
+
*
|
|
34
|
+
* This runtime is typically installed by the Foreign Subtree execution module,
|
|
35
|
+
* which decides whether the child stays inline or leaves the current renderer.
|
|
36
|
+
*/
|
|
37
|
+
interceptForeignChild?(
|
|
38
|
+
input: ForeignChildInterceptionInput,
|
|
39
|
+
): ForeignChildInterceptionResult | Promise<ForeignChildInterceptionResult>;
|
|
40
|
+
interceptForeignChildSync?(input: ForeignChildInterceptionInput): ForeignChildInterceptionResult;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
type
|
|
43
|
+
type ForeignChildRenderInput = {
|
|
38
44
|
component: EcoComponent;
|
|
39
45
|
props: Record<string, unknown>;
|
|
40
46
|
targetIntegration?: string;
|
|
@@ -43,7 +49,7 @@ type ComponentBoundaryRenderInput = {
|
|
|
43
49
|
/**
|
|
44
50
|
* Shared output finalization behavior used both inside and outside active render contexts.
|
|
45
51
|
*
|
|
46
|
-
* This stage is responsible only for lazy trigger or injector wrapping.
|
|
52
|
+
* This stage is responsible only for lazy trigger or injector wrapping. Foreign-child
|
|
47
53
|
* interception stays in `ContextualComponentRenderRuntime`.
|
|
48
54
|
*/
|
|
49
55
|
class ComponentRenderOutputRuntime {
|
|
@@ -82,7 +88,7 @@ class ComponentRenderOutputRuntime {
|
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
/**
|
|
85
|
-
* Render-context-aware runtime that delegates
|
|
91
|
+
* Render-context-aware runtime that delegates foreign-child interception.
|
|
86
92
|
*/
|
|
87
93
|
class ContextualComponentRenderRuntime extends ComponentRenderOutputRuntime {
|
|
88
94
|
private readonly context: ComponentRenderContext;
|
|
@@ -92,7 +98,7 @@ class ContextualComponentRenderRuntime extends ComponentRenderOutputRuntime {
|
|
|
92
98
|
this.context = context;
|
|
93
99
|
}
|
|
94
100
|
|
|
95
|
-
private
|
|
101
|
+
private applyForeignChildInterceptionResult(result: ForeignChildInterceptionResult): unknown | undefined {
|
|
96
102
|
if (result.kind === 'resolved') {
|
|
97
103
|
return result.value;
|
|
98
104
|
}
|
|
@@ -101,44 +107,45 @@ class ContextualComponentRenderRuntime extends ComponentRenderOutputRuntime {
|
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
/**
|
|
104
|
-
* Resolves one
|
|
110
|
+
* Resolves one foreign-child interception through the active runtime.
|
|
105
111
|
*
|
|
106
112
|
* The runtime may choose inline rendering or immediate resolved output.
|
|
107
113
|
*/
|
|
108
|
-
|
|
109
|
-
const
|
|
114
|
+
interceptForeignChild(input: ForeignChildRenderInput): Promise<unknown | undefined> | unknown | undefined {
|
|
115
|
+
const foreignChildRuntimeInput = {
|
|
110
116
|
currentIntegration: this.context.currentIntegration,
|
|
111
117
|
targetIntegration: input.targetIntegration,
|
|
112
118
|
component: input.component,
|
|
113
119
|
props: input.props,
|
|
114
|
-
} satisfies
|
|
120
|
+
} satisfies ForeignChildInterceptionInput;
|
|
115
121
|
|
|
116
|
-
const asyncInterception = this.context.
|
|
122
|
+
const asyncInterception = this.context.foreignChildRuntime?.interceptForeignChild?.(foreignChildRuntimeInput);
|
|
117
123
|
if (asyncInterception !== undefined) {
|
|
118
|
-
if (isThenable<
|
|
119
|
-
return asyncInterception.then((result) => this.
|
|
124
|
+
if (isThenable<ForeignChildInterceptionResult>(asyncInterception)) {
|
|
125
|
+
return asyncInterception.then((result) => this.applyForeignChildInterceptionResult(result));
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
return this.
|
|
128
|
+
return this.applyForeignChildInterceptionResult(asyncInterception);
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
const syncInterception =
|
|
131
|
+
const syncInterception =
|
|
132
|
+
this.context.foreignChildRuntime?.interceptForeignChildSync?.(foreignChildRuntimeInput);
|
|
126
133
|
if (syncInterception === undefined) {
|
|
127
134
|
return undefined;
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
return this.
|
|
137
|
+
return this.applyForeignChildInterceptionResult(syncInterception);
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
/**
|
|
135
|
-
* Per-render mutable state used while applying
|
|
142
|
+
* Per-render mutable state used while applying foreign-child interception and lazy
|
|
136
143
|
* output wrapping.
|
|
137
144
|
*/
|
|
138
145
|
export type ComponentRenderContext = {
|
|
139
146
|
currentIntegration: string;
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
foreignChildRuntime?: ForeignChildRuntime;
|
|
148
|
+
interceptForeignChild(input: ForeignChildRenderInput): Promise<unknown | undefined> | unknown | undefined;
|
|
142
149
|
finalizeComponentRender<T>(component: EcoComponent, content: T): T;
|
|
143
150
|
};
|
|
144
151
|
|
|
@@ -250,14 +257,14 @@ export function getComponentRenderContext(): ComponentRenderContext | undefined
|
|
|
250
257
|
const componentRenderOutputRuntime = new ComponentRenderOutputRuntime();
|
|
251
258
|
|
|
252
259
|
/**
|
|
253
|
-
* Runs
|
|
260
|
+
* Runs foreign-child interception for one component render step.
|
|
254
261
|
*
|
|
255
|
-
* The active runtime may resolve the
|
|
262
|
+
* The active runtime may resolve the foreign child immediately or keep it inline.
|
|
256
263
|
*/
|
|
257
|
-
export function
|
|
258
|
-
input:
|
|
264
|
+
export function interceptForeignChild(
|
|
265
|
+
input: ForeignChildRenderInput,
|
|
259
266
|
): Promise<unknown | undefined> | unknown | undefined {
|
|
260
|
-
return getComponentRenderContext()?.
|
|
267
|
+
return getComponentRenderContext()?.interceptForeignChild(input);
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
/**
|
|
@@ -276,24 +283,24 @@ export function finalizeComponentRender<T>(component: EcoComponent, content: T):
|
|
|
276
283
|
* Runs render work under a fresh component render context and returns the
|
|
277
284
|
* resulting value.
|
|
278
285
|
*
|
|
279
|
-
* @param input Execution metadata for current integration and
|
|
286
|
+
* @param input Execution metadata for current integration and foreign-child policy.
|
|
280
287
|
* @param render Async render function to execute inside the context.
|
|
281
288
|
* @returns Render result value.
|
|
282
289
|
*/
|
|
283
290
|
export async function runWithComponentRenderContext<T>(
|
|
284
291
|
input: {
|
|
285
292
|
currentIntegration: string;
|
|
286
|
-
|
|
293
|
+
foreignChildRuntime?: ForeignChildRuntime;
|
|
287
294
|
},
|
|
288
295
|
render: () => Promise<T>,
|
|
289
296
|
): Promise<{ value: T }> {
|
|
290
297
|
const context = {
|
|
291
298
|
currentIntegration: input.currentIntegration,
|
|
292
|
-
|
|
299
|
+
foreignChildRuntime: input.foreignChildRuntime,
|
|
293
300
|
} as ComponentRenderContext;
|
|
294
301
|
const runtime = new ContextualComponentRenderRuntime(context);
|
|
295
|
-
context.
|
|
296
|
-
runtime.
|
|
302
|
+
context.interceptForeignChild = (deferredInput: ForeignChildRenderInput) =>
|
|
303
|
+
runtime.interceptForeignChild(deferredInput);
|
|
297
304
|
context.finalizeComponentRender = <TContent>(component: EcoComponent, content: TContent) =>
|
|
298
305
|
runtime.finalizeComponentRender(component, content);
|
|
299
306
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EcoComponent, OwnershipPlanNodeSource } from '../../types/public-types.ts';
|
|
2
|
+
|
|
3
|
+
export type DeclaredOwnershipRoot = {
|
|
4
|
+
component: EcoComponent;
|
|
5
|
+
source: Extract<OwnershipPlanNodeSource, 'page' | 'layout' | 'html-template'>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type DeclaredOwnershipNodeInput = {
|
|
9
|
+
component: EcoComponent;
|
|
10
|
+
source: Exclude<OwnershipPlanNodeSource, 'route'>;
|
|
11
|
+
parentIntegrationName: string;
|
|
12
|
+
integrationName: string;
|
|
13
|
+
componentId: string;
|
|
14
|
+
isForeignToParent: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function mapDeclaredOwnershipGraph<T>(input: {
|
|
18
|
+
roots: DeclaredOwnershipRoot[];
|
|
19
|
+
currentIntegrationName: string;
|
|
20
|
+
mapNode: (node: DeclaredOwnershipNodeInput, children: T[]) => T;
|
|
21
|
+
}): T[] {
|
|
22
|
+
let nextSyntheticId = 0;
|
|
23
|
+
|
|
24
|
+
const mapComponent = (
|
|
25
|
+
component: EcoComponent,
|
|
26
|
+
source: Exclude<OwnershipPlanNodeSource, 'route'>,
|
|
27
|
+
parentIntegrationName: string,
|
|
28
|
+
lineage: Set<object>,
|
|
29
|
+
): T => {
|
|
30
|
+
const integrationName =
|
|
31
|
+
component.config?.integration ?? component.config?.__eco?.integration ?? parentIntegrationName;
|
|
32
|
+
const componentMeta = component.config?.__eco;
|
|
33
|
+
const isForeignToParent = integrationName !== parentIntegrationName;
|
|
34
|
+
const componentId = componentMeta?.id ?? componentMeta?.file ?? `${source}:${(nextSyntheticId += 1)}`;
|
|
35
|
+
|
|
36
|
+
const nextLineage = new Set(lineage);
|
|
37
|
+
nextLineage.add(component);
|
|
38
|
+
const children = (component.config?.dependencies?.components ?? []).flatMap((child) => {
|
|
39
|
+
if (!child || nextLineage.has(child)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [mapComponent(child, 'dependency', integrationName, nextLineage)];
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return input.mapNode(
|
|
47
|
+
{
|
|
48
|
+
component,
|
|
49
|
+
source,
|
|
50
|
+
parentIntegrationName,
|
|
51
|
+
integrationName,
|
|
52
|
+
componentId,
|
|
53
|
+
isForeignToParent,
|
|
54
|
+
},
|
|
55
|
+
children,
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return input.roots.map(({ component, source }) =>
|
|
60
|
+
mapComponent(component, source, input.currentIntegrationName, new Set()),
|
|
61
|
+
);
|
|
62
|
+
}
|