@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
|
@@ -2,7 +2,7 @@ import type { Readable } from 'node:stream';
|
|
|
2
2
|
import type { ApiResponseBuilder } from '../adapters/shared/api-response.ts';
|
|
3
3
|
import type { BuildExecutor } from '../build/build-adapter.ts';
|
|
4
4
|
import type { EcoBuildPlugin } from '../build/build-types.ts';
|
|
5
|
-
import type {
|
|
5
|
+
import type { ForeignChildRuntime } from '../route-renderer/orchestration/component-render-context.ts';
|
|
6
6
|
import type { EcoPageComponent } from '../eco/eco.types.ts';
|
|
7
7
|
import type { EcoPagesAppConfig } from './internal-types.ts';
|
|
8
8
|
import type { HmrStrategy } from '../hmr/hmr-strategy.ts';
|
|
@@ -31,7 +31,7 @@ export type {
|
|
|
31
31
|
StandardSchemaFailureResult,
|
|
32
32
|
StandardSchemaIssue,
|
|
33
33
|
InferOutput,
|
|
34
|
-
|
|
34
|
+
ForeignChildRuntime,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export type InteractionEventsString = ScriptsInjectorInteractionEventsString;
|
|
@@ -112,11 +112,6 @@ export interface DefaultHmrContext {
|
|
|
112
112
|
*/
|
|
113
113
|
getWatchedFiles(): Map<string, string>;
|
|
114
114
|
|
|
115
|
-
/**
|
|
116
|
-
* Map of bare specifiers to runtime URLs for browser import resolution.
|
|
117
|
-
*/
|
|
118
|
-
getSpecifierMap(): Map<string, string>;
|
|
119
|
-
|
|
120
115
|
/**
|
|
121
116
|
* Directory where HMR bundles are written.
|
|
122
117
|
*/
|
|
@@ -218,16 +213,6 @@ export interface IHmrManager {
|
|
|
218
213
|
*/
|
|
219
214
|
registerScriptEntrypoint(entrypointPath: string): Promise<string>;
|
|
220
215
|
|
|
221
|
-
/**
|
|
222
|
-
* Registers mappings from bare specifiers to runtime URLs.
|
|
223
|
-
*
|
|
224
|
-
* @remarks
|
|
225
|
-
* This is the shared registration seam for integration-owned runtime alias
|
|
226
|
-
* maps. The registry may later back a broader import-map-style facility, but
|
|
227
|
-
* the mappings themselves remain integration-owned.
|
|
228
|
-
*/
|
|
229
|
-
registerSpecifierMap(map: Record<string, string>): void;
|
|
230
|
-
|
|
231
216
|
/**
|
|
232
217
|
* Registers a custom HMR strategy.
|
|
233
218
|
*/
|
|
@@ -263,11 +248,6 @@ export interface IHmrManager {
|
|
|
263
248
|
*/
|
|
264
249
|
getWatchedFiles(): Map<string, string>;
|
|
265
250
|
|
|
266
|
-
/**
|
|
267
|
-
* Gets the registered bare-specifier map.
|
|
268
|
-
*/
|
|
269
|
-
getSpecifierMap(): Map<string, string>;
|
|
270
|
-
|
|
271
251
|
/**
|
|
272
252
|
* Gets the HMR dist directory.
|
|
273
253
|
*/
|
|
@@ -783,7 +763,7 @@ export type IntegrationRendererRenderOptions<C = EcoPagesElement> = RouteRendere
|
|
|
783
763
|
pageProps?: Record<string, unknown>;
|
|
784
764
|
cacheStrategy?: CacheStrategy;
|
|
785
765
|
pageLocals?: RequestLocals;
|
|
786
|
-
|
|
766
|
+
ownershipPlan?: OwnershipPlan;
|
|
787
767
|
};
|
|
788
768
|
|
|
789
769
|
/**
|
|
@@ -824,19 +804,34 @@ export interface PagePackageResult {
|
|
|
824
804
|
dynamicChunks: ProcessedAsset[];
|
|
825
805
|
}
|
|
826
806
|
|
|
827
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Page-scoped browser output planned before final HTML packaging.
|
|
809
|
+
*
|
|
810
|
+
* The initial seam keeps the graph payload intentionally small: integrations
|
|
811
|
+
* return the processed assets that belong to the Page browser graph, while the
|
|
812
|
+
* surrounding route pipeline remains free to evolve toward richer entry/lazy/
|
|
813
|
+
* shared chunk structure later.
|
|
814
|
+
*/
|
|
815
|
+
export interface PageBrowserGraphResult {
|
|
816
|
+
/**
|
|
817
|
+
* Processed assets owned by the current Page browser graph.
|
|
818
|
+
*/
|
|
819
|
+
assets: ProcessedAsset[];
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export type OwnershipValidationErrorCode = 'UNKNOWN_INTEGRATION_OWNER' | 'MISSING_COMPONENT_METADATA';
|
|
828
823
|
|
|
829
|
-
export interface
|
|
830
|
-
code:
|
|
824
|
+
export interface OwnershipValidationError {
|
|
825
|
+
code: OwnershipValidationErrorCode;
|
|
831
826
|
message: string;
|
|
832
827
|
componentId?: string;
|
|
833
828
|
componentFile?: string;
|
|
834
829
|
integrationName?: string;
|
|
835
830
|
}
|
|
836
831
|
|
|
837
|
-
export type
|
|
832
|
+
export type OwnershipPlanNodeSource = 'route' | 'page' | 'layout' | 'html-template' | 'dependency';
|
|
838
833
|
|
|
839
|
-
export interface
|
|
834
|
+
export interface IntegrationOwnership {
|
|
840
835
|
integrationName: string;
|
|
841
836
|
componentId: string;
|
|
842
837
|
componentFile?: string;
|
|
@@ -844,35 +839,35 @@ export interface BoundaryOwnership {
|
|
|
844
839
|
isForeignToParent: boolean;
|
|
845
840
|
}
|
|
846
841
|
|
|
847
|
-
export interface
|
|
842
|
+
export interface OwnershipPlanNode {
|
|
848
843
|
id: string;
|
|
849
|
-
source:
|
|
850
|
-
ownership:
|
|
851
|
-
children:
|
|
844
|
+
source: OwnershipPlanNodeSource;
|
|
845
|
+
ownership: IntegrationOwnership;
|
|
846
|
+
children: OwnershipPlanNode[];
|
|
852
847
|
declaredDependenciesValid: boolean;
|
|
853
848
|
}
|
|
854
849
|
|
|
855
|
-
export interface
|
|
856
|
-
root:
|
|
850
|
+
export interface OwnershipPlan {
|
|
851
|
+
root: OwnershipPlanNode;
|
|
857
852
|
rendererNames: string[];
|
|
858
853
|
foreignEdgeCount: number;
|
|
859
854
|
hasValidationErrors: boolean;
|
|
860
|
-
validationErrors:
|
|
855
|
+
validationErrors: OwnershipValidationError[];
|
|
861
856
|
}
|
|
862
857
|
|
|
863
|
-
export type
|
|
858
|
+
export type ForeignSubtreeAttachmentPolicy = { kind: 'none' } | { kind: 'first-element' };
|
|
864
859
|
|
|
865
|
-
export interface
|
|
860
|
+
export interface ForeignSubtreeRenderPayload {
|
|
866
861
|
html: string;
|
|
867
862
|
assets: ProcessedAsset[];
|
|
868
863
|
rootTag?: string;
|
|
869
864
|
rootAttributes?: Record<string, string>;
|
|
870
|
-
attachmentPolicy:
|
|
865
|
+
attachmentPolicy: ForeignSubtreeAttachmentPolicy;
|
|
871
866
|
integrationName: string;
|
|
872
867
|
}
|
|
873
868
|
|
|
874
869
|
/**
|
|
875
|
-
* Shared execution-scoped context threaded through
|
|
870
|
+
* Shared execution-scoped context threaded through foreign-child renders.
|
|
876
871
|
*
|
|
877
872
|
* Integrations can extend this with renderer-local runtime keys, but the cache
|
|
878
873
|
* and optional component instance identity are shared across all renderers.
|
|
@@ -1144,6 +1139,18 @@ export interface ApiHandlerContext<TRequest extends Request = Request, TServer =
|
|
|
1144
1139
|
headers?: unknown;
|
|
1145
1140
|
}
|
|
1146
1141
|
|
|
1142
|
+
/**
|
|
1143
|
+
* Context available to file-route page middleware.
|
|
1144
|
+
*
|
|
1145
|
+
* Page middleware can mutate locals, short-circuit the request, and use the
|
|
1146
|
+
* response helpers, but final document rendering stays owned by the page route
|
|
1147
|
+
* execution path.
|
|
1148
|
+
*/
|
|
1149
|
+
export interface FileRouteMiddlewareContext<TRequest extends Request = Request, TServer = any> extends Omit<
|
|
1150
|
+
ApiHandlerContext<TRequest, TServer>,
|
|
1151
|
+
'render' | 'renderPartial'
|
|
1152
|
+
> {}
|
|
1153
|
+
|
|
1147
1154
|
/**
|
|
1148
1155
|
* Next function for middleware chain.
|
|
1149
1156
|
* Call to continue to the next middleware or final handler.
|
|
@@ -1185,6 +1192,15 @@ export type Middleware<
|
|
|
1185
1192
|
TContext extends ApiHandlerContext<TRequest, TServer> = ApiHandlerContext<TRequest, TServer>,
|
|
1186
1193
|
> = (context: TContext, next: MiddlewareNext) => Promise<Response> | Response;
|
|
1187
1194
|
|
|
1195
|
+
/**
|
|
1196
|
+
* Middleware contract for file-based page routes.
|
|
1197
|
+
*/
|
|
1198
|
+
export type FileRouteMiddleware<
|
|
1199
|
+
TRequest extends Request = Request,
|
|
1200
|
+
TServer = any,
|
|
1201
|
+
TContext extends FileRouteMiddlewareContext<TRequest, TServer> = FileRouteMiddlewareContext<TRequest, TServer>,
|
|
1202
|
+
> = (context: TContext, next: MiddlewareNext) => Promise<Response> | Response;
|
|
1203
|
+
|
|
1188
1204
|
/**
|
|
1189
1205
|
* Helper type for defining middleware with extended context.
|
|
1190
1206
|
* Automatically infers TRequest and TServer from the provided context type.
|
|
@@ -10,21 +10,22 @@ export const createMockHmrManager = (): IHmrManager =>
|
|
|
10
10
|
setPlugins: vi.fn(() => {}),
|
|
11
11
|
registerEntrypoint: vi.fn(async () => ''),
|
|
12
12
|
registerScriptEntrypoint: vi.fn(async () => ''),
|
|
13
|
-
registerSpecifierMap: vi.fn(() => {}),
|
|
14
13
|
registerStrategy: vi.fn(() => {}),
|
|
15
14
|
isEnabled: vi.fn(() => true),
|
|
16
15
|
getOutputUrl: vi.fn(() => undefined),
|
|
17
16
|
getWatchedFiles: vi.fn(() => new Map()),
|
|
18
|
-
getSpecifierMap: vi.fn(() => new Map()),
|
|
19
17
|
getDistDir: vi.fn(() => ''),
|
|
20
18
|
getPlugins: vi.fn(() => []),
|
|
21
19
|
getDefaultContext: vi.fn(() => ({
|
|
22
20
|
getWatchedFiles: () => new Map(),
|
|
23
|
-
getSpecifierMap: () => new Map(),
|
|
24
21
|
getDistDir: () => '',
|
|
25
22
|
getPlugins: () => [],
|
|
26
23
|
getSrcDir: () => '',
|
|
27
24
|
getLayoutsDir: () => '',
|
|
25
|
+
getPagesDir: () => '',
|
|
26
|
+
getBuildExecutor: () => ({ build: vi.fn(async () => ({ success: true, logs: [], outputs: [] })) }),
|
|
27
|
+
getBrowserBundleService: () => ({ bundle: vi.fn(async () => ({ success: true, logs: [], outputs: [] })) }),
|
|
28
|
+
importServerModule: vi.fn(async () => ({})),
|
|
28
29
|
})),
|
|
29
30
|
}) as unknown as IHmrManager;
|
|
30
31
|
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
|
|
2
|
-
import type {
|
|
3
|
-
BoundaryPlan,
|
|
4
|
-
BoundaryPlanNode,
|
|
5
|
-
BoundaryPlanNodeSource,
|
|
6
|
-
BoundaryValidationError,
|
|
7
|
-
EcoComponent,
|
|
8
|
-
} from '../../types/public-types.ts';
|
|
9
|
-
|
|
10
|
-
type BoundaryPlanBuildInput = {
|
|
11
|
-
routeFile: string;
|
|
12
|
-
currentIntegrationName: string;
|
|
13
|
-
HtmlTemplate: EcoComponent;
|
|
14
|
-
Layout?: EcoComponent;
|
|
15
|
-
Page: EcoComponent;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type BoundaryPlanBuildEntry = {
|
|
19
|
-
component: EcoComponent;
|
|
20
|
-
source: Extract<BoundaryPlanNodeSource, 'page' | 'layout' | 'html-template'>;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Builds a declared ownership plan from the component dependency graph.
|
|
25
|
-
*
|
|
26
|
-
* The plan is intentionally conservative: it reflects declared component
|
|
27
|
-
* dependencies available during render preparation and records diagnostics for
|
|
28
|
-
* foreign ownership edges that cannot be validated against registered
|
|
29
|
-
* integrations or stable component metadata.
|
|
30
|
-
*/
|
|
31
|
-
export class BoundaryPlanningService {
|
|
32
|
-
private readonly appConfig: EcoPagesAppConfig;
|
|
33
|
-
private nextSyntheticId = 0;
|
|
34
|
-
|
|
35
|
-
constructor(appConfig: EcoPagesAppConfig) {
|
|
36
|
-
this.appConfig = appConfig;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
buildPlan(input: BoundaryPlanBuildInput): BoundaryPlan {
|
|
40
|
-
this.nextSyntheticId = 0;
|
|
41
|
-
|
|
42
|
-
const validationErrors: BoundaryValidationError[] = [];
|
|
43
|
-
const rendererNames = new Set<string>([input.currentIntegrationName]);
|
|
44
|
-
let foreignEdgeCount = 0;
|
|
45
|
-
|
|
46
|
-
const buildNode = (
|
|
47
|
-
component: EcoComponent,
|
|
48
|
-
source: Exclude<BoundaryPlanNodeSource, 'route'>,
|
|
49
|
-
parentIntegrationName: string,
|
|
50
|
-
lineage: Set<object>,
|
|
51
|
-
): BoundaryPlanNode => {
|
|
52
|
-
const integrationName =
|
|
53
|
-
component.config?.integration ?? component.config?.__eco?.integration ?? parentIntegrationName;
|
|
54
|
-
const componentMeta = component.config?.__eco;
|
|
55
|
-
const isForeignToParent = integrationName !== parentIntegrationName;
|
|
56
|
-
const componentId = componentMeta?.id ?? componentMeta?.file ?? `${source}:${(this.nextSyntheticId += 1)}`;
|
|
57
|
-
|
|
58
|
-
rendererNames.add(integrationName);
|
|
59
|
-
|
|
60
|
-
if (isForeignToParent) {
|
|
61
|
-
foreignEdgeCount += 1;
|
|
62
|
-
|
|
63
|
-
if (!componentMeta) {
|
|
64
|
-
validationErrors.push({
|
|
65
|
-
code: 'MISSING_COMPONENT_METADATA',
|
|
66
|
-
message: `[ecopages] Foreign boundary "${componentId}" must provide stable __eco metadata so ownership diagnostics stay actionable. Declared dependencies must include all possible foreign children.`,
|
|
67
|
-
componentId,
|
|
68
|
-
integrationName,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!this.isRegisteredIntegration(integrationName, input.currentIntegrationName)) {
|
|
73
|
-
validationErrors.push({
|
|
74
|
-
code: 'UNKNOWN_INTEGRATION_OWNER',
|
|
75
|
-
message: `[ecopages] Foreign boundary "${componentId}" references unknown integration owner "${integrationName}". Declared dependencies must include all possible foreign children and those integrations must be registered.`,
|
|
76
|
-
componentId,
|
|
77
|
-
componentFile: componentMeta?.file,
|
|
78
|
-
integrationName,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const nextLineage = new Set(lineage);
|
|
84
|
-
nextLineage.add(component);
|
|
85
|
-
const children = (component.config?.dependencies?.components ?? []).flatMap((child) => {
|
|
86
|
-
if (!child || nextLineage.has(child)) {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return [buildNode(child, 'dependency', integrationName, nextLineage)];
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
id: componentId,
|
|
95
|
-
source,
|
|
96
|
-
ownership: {
|
|
97
|
-
integrationName,
|
|
98
|
-
componentId,
|
|
99
|
-
componentFile: componentMeta?.file,
|
|
100
|
-
isPageEntry: source === 'page',
|
|
101
|
-
isForeignToParent,
|
|
102
|
-
},
|
|
103
|
-
children,
|
|
104
|
-
declaredDependenciesValid: true,
|
|
105
|
-
};
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const roots: BoundaryPlanBuildEntry[] = [
|
|
109
|
-
{ component: input.HtmlTemplate, source: 'html-template' },
|
|
110
|
-
...(input.Layout ? [{ component: input.Layout, source: 'layout' as const }] : []),
|
|
111
|
-
{ component: input.Page, source: 'page' },
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
const root: BoundaryPlanNode = {
|
|
115
|
-
id: `route:${input.routeFile}`,
|
|
116
|
-
source: 'route',
|
|
117
|
-
ownership: {
|
|
118
|
-
integrationName: input.currentIntegrationName,
|
|
119
|
-
componentId: `route:${input.routeFile}`,
|
|
120
|
-
componentFile: input.routeFile,
|
|
121
|
-
isPageEntry: false,
|
|
122
|
-
isForeignToParent: false,
|
|
123
|
-
},
|
|
124
|
-
children: roots.map(({ component, source }) =>
|
|
125
|
-
buildNode(component, source, input.currentIntegrationName, new Set()),
|
|
126
|
-
),
|
|
127
|
-
declaredDependenciesValid: validationErrors.length === 0,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
root,
|
|
132
|
-
rendererNames: Array.from(rendererNames),
|
|
133
|
-
foreignEdgeCount,
|
|
134
|
-
hasValidationErrors: validationErrors.length > 0,
|
|
135
|
-
validationErrors,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private isRegisteredIntegration(integrationName: string, currentIntegrationName: string): boolean {
|
|
140
|
-
if (integrationName === currentIntegrationName) {
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return this.appConfig.integrations.some((integration) => integration.name === integrationName);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import type { ProcessedAsset } from '../../services/assets/asset-processing-service/index.ts';
|
|
2
|
-
import type { PagePackageResult } from '../../types/public-types.ts';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates the structured page package consumed by final HTML injection.
|
|
6
|
-
*
|
|
7
|
-
* This first pass is intentionally behavior-preserving. It establishes the
|
|
8
|
-
* packaging seam while forwarding the current flat processed asset list.
|
|
9
|
-
*/
|
|
10
|
-
export class PagePackagingService {
|
|
11
|
-
/**
|
|
12
|
-
* Partitions processed assets into the page-level groups used during final
|
|
13
|
-
* HTML injection and post-processing.
|
|
14
|
-
*/
|
|
15
|
-
createPagePackage(assets: ProcessedAsset[]): PagePackageResult {
|
|
16
|
-
const inlineAssets: ProcessedAsset[] = [];
|
|
17
|
-
const separateAssets: ProcessedAsset[] = [];
|
|
18
|
-
const dynamicChunks: ProcessedAsset[] = [];
|
|
19
|
-
let pageScript: ProcessedAsset | undefined;
|
|
20
|
-
let pageStylesheet: ProcessedAsset | undefined;
|
|
21
|
-
|
|
22
|
-
for (const asset of assets) {
|
|
23
|
-
if (asset.inline) {
|
|
24
|
-
inlineAssets.push(asset);
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (asset.packageRole === 'dynamic-chunk') {
|
|
29
|
-
dynamicChunks.push(asset);
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (!pageScript && asset.packageRole === 'page-script') {
|
|
34
|
-
pageScript = asset;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (!pageStylesheet && asset.packageRole === 'page-style') {
|
|
39
|
-
pageStylesheet = asset;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (asset.packageRole === 'keep-separate' || asset.packageRole === 'runtime') {
|
|
44
|
-
separateAssets.push(asset);
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!pageScript && asset.kind === 'script' && !asset.excludeFromHtml) {
|
|
49
|
-
pageScript = asset;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (!pageStylesheet && asset.kind === 'stylesheet') {
|
|
54
|
-
pageStylesheet = asset;
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
separateAssets.push(asset);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const htmlAssets = assets.filter((asset) => this.shouldIncludeInHtml(asset));
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
assets,
|
|
65
|
-
htmlAssets,
|
|
66
|
-
pageScript,
|
|
67
|
-
pageStylesheet,
|
|
68
|
-
inlineAssets,
|
|
69
|
-
separateAssets,
|
|
70
|
-
dynamicChunks,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
private shouldIncludeInHtml(asset: ProcessedAsset): boolean {
|
|
75
|
-
if (asset.excludeFromHtml) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (asset.packageRole === 'runtime') {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { getComponentRenderContext } from './component-render-context.ts';
|
|
3
|
-
import type {
|
|
4
|
-
EcoComponent,
|
|
5
|
-
IntegrationRendererRenderOptions,
|
|
6
|
-
RouteRendererBody,
|
|
7
|
-
RouteRendererOptions,
|
|
8
|
-
} from '../../types/public-types.ts';
|
|
9
|
-
import { RenderExecutionService } from './render-execution.service.ts';
|
|
10
|
-
|
|
11
|
-
describe('RenderExecutionService', () => {
|
|
12
|
-
it('captures streamed render bodies before final HTML handling', async () => {
|
|
13
|
-
const service = new RenderExecutionService();
|
|
14
|
-
const encoder = new TextEncoder();
|
|
15
|
-
|
|
16
|
-
const result = await service.captureHtmlRender(
|
|
17
|
-
async () =>
|
|
18
|
-
new ReadableStream({
|
|
19
|
-
start(controller) {
|
|
20
|
-
controller.enqueue(encoder.encode('<html><body><main>Streamed</main></body></html>'));
|
|
21
|
-
controller.close();
|
|
22
|
-
},
|
|
23
|
-
}) as unknown as BodyInit,
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
expect(result.body).toBeInstanceOf(ReadableStream);
|
|
27
|
-
expect(result.html).toContain('<main>Streamed</main>');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('preserves streamed bodies when no boundary resolution or attribute stamping is required', async () => {
|
|
31
|
-
const service = new RenderExecutionService();
|
|
32
|
-
const HtmlTemplate = (() => '<html></html>') as EcoComponent<Record<string, unknown>>;
|
|
33
|
-
const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
|
|
34
|
-
const encoder = new TextEncoder();
|
|
35
|
-
|
|
36
|
-
const result = await service.execute(
|
|
37
|
-
{
|
|
38
|
-
file: '/app/pages/index.tsx',
|
|
39
|
-
params: {},
|
|
40
|
-
query: {},
|
|
41
|
-
} as unknown as RouteRendererOptions,
|
|
42
|
-
{
|
|
43
|
-
prepareRenderOptions: async () =>
|
|
44
|
-
({
|
|
45
|
-
HtmlTemplate,
|
|
46
|
-
Page,
|
|
47
|
-
cacheStrategy: { revalidate: 60 },
|
|
48
|
-
}) as unknown as IntegrationRendererRenderOptions<unknown>,
|
|
49
|
-
render: async () =>
|
|
50
|
-
new ReadableStream({
|
|
51
|
-
start(controller) {
|
|
52
|
-
controller.enqueue(encoder.encode('<html><body><main>Streamed</main></body></html>'));
|
|
53
|
-
controller.close();
|
|
54
|
-
},
|
|
55
|
-
}) as unknown as BodyInit,
|
|
56
|
-
getDocumentAttributes: () => undefined,
|
|
57
|
-
applyAttributesToHtmlElement: (html) => html,
|
|
58
|
-
applyAttributesToFirstBodyElement: (html) => html,
|
|
59
|
-
transformResponse: async (response) => response.body as RouteRendererBody,
|
|
60
|
-
},
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
expect(result.cacheStrategy).toEqual({ revalidate: 60 });
|
|
64
|
-
expect(result.body).toBeInstanceOf(ReadableStream);
|
|
65
|
-
expect(await new Response(result.body as BodyInit).text()).toContain('<main>Streamed</main>');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('applies root and document attributes to fully resolved route HTML', async () => {
|
|
69
|
-
const service = new RenderExecutionService();
|
|
70
|
-
const HtmlTemplate = (() => '<html></html>') as EcoComponent<Record<string, unknown>>;
|
|
71
|
-
const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
|
|
72
|
-
|
|
73
|
-
const result = await service.execute(
|
|
74
|
-
{
|
|
75
|
-
file: '/app/pages/index.tsx',
|
|
76
|
-
params: {},
|
|
77
|
-
query: {},
|
|
78
|
-
} as unknown as RouteRendererOptions,
|
|
79
|
-
{
|
|
80
|
-
prepareRenderOptions: async () =>
|
|
81
|
-
({
|
|
82
|
-
HtmlTemplate,
|
|
83
|
-
Page,
|
|
84
|
-
cacheStrategy: { revalidate: 60 },
|
|
85
|
-
componentRender: {
|
|
86
|
-
canAttachAttributes: true,
|
|
87
|
-
rootAttributes: { 'data-eco-component-id': 'eco-page-root' },
|
|
88
|
-
},
|
|
89
|
-
}) as unknown as IntegrationRendererRenderOptions<unknown>,
|
|
90
|
-
render: async () => '<html><body><main>Resolved</main></body></html>',
|
|
91
|
-
getDocumentAttributes: () => ({ 'data-eco-document-owner': 'react-router' }),
|
|
92
|
-
applyAttributesToHtmlElement: (html, attributes) =>
|
|
93
|
-
html.replace('<html', `<html data-eco-document-owner="${attributes['data-eco-document-owner']}"`),
|
|
94
|
-
applyAttributesToFirstBodyElement: (html, attributes) =>
|
|
95
|
-
html.replace('<main', `<main data-eco-component-id="${attributes['data-eco-component-id']}"`),
|
|
96
|
-
transformResponse: async (response) => await response.text(),
|
|
97
|
-
},
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
expect(result.cacheStrategy).toEqual({ revalidate: 60 });
|
|
101
|
-
expect(result.body).toContain('<html data-eco-document-owner="react-router"><body>');
|
|
102
|
-
expect(result.body).toContain('<main data-eco-component-id="eco-page-root">Resolved</main>');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('throws when route HTML contains escaped unresolved boundary artifacts', async () => {
|
|
106
|
-
const service = new RenderExecutionService();
|
|
107
|
-
const HtmlTemplate = (() => '<html></html>') as EcoComponent<Record<string, unknown>>;
|
|
108
|
-
const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
|
|
109
|
-
|
|
110
|
-
await expect(
|
|
111
|
-
service.execute(
|
|
112
|
-
{
|
|
113
|
-
file: '/app/pages/index.tsx',
|
|
114
|
-
params: {},
|
|
115
|
-
query: {},
|
|
116
|
-
} as unknown as RouteRendererOptions,
|
|
117
|
-
{
|
|
118
|
-
prepareRenderOptions: async () =>
|
|
119
|
-
({
|
|
120
|
-
HtmlTemplate,
|
|
121
|
-
Page,
|
|
122
|
-
cacheStrategy: 'dynamic',
|
|
123
|
-
}) as unknown as IntegrationRendererRenderOptions<unknown>,
|
|
124
|
-
render: async () =>
|
|
125
|
-
'<html><body>&lt;eco-marker data-eco-node-id="n_2" data-eco-component-ref="page-component" data-eco-props-ref="p_2"&gt;&lt;/eco-marker&gt;</body></html>',
|
|
126
|
-
getDocumentAttributes: () => undefined,
|
|
127
|
-
applyAttributesToHtmlElement: (html) => html,
|
|
128
|
-
applyAttributesToFirstBodyElement: (html) => html,
|
|
129
|
-
transformResponse: async (response) => await response.text(),
|
|
130
|
-
},
|
|
131
|
-
),
|
|
132
|
-
).rejects.toThrow('Full-route unresolved-boundary fallback has been removed');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('renders routes with no active component render context', async () => {
|
|
136
|
-
const service = new RenderExecutionService();
|
|
137
|
-
const HtmlTemplate = (() => '<html></html>') as EcoComponent<Record<string, unknown>>;
|
|
138
|
-
const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
|
|
139
|
-
|
|
140
|
-
const result = await service.execute(
|
|
141
|
-
{
|
|
142
|
-
file: '/app/pages/index.tsx',
|
|
143
|
-
params: {},
|
|
144
|
-
query: {},
|
|
145
|
-
} as unknown as RouteRendererOptions,
|
|
146
|
-
{
|
|
147
|
-
prepareRenderOptions: async () =>
|
|
148
|
-
({
|
|
149
|
-
HtmlTemplate,
|
|
150
|
-
Page,
|
|
151
|
-
cacheStrategy: 'dynamic',
|
|
152
|
-
}) as unknown as IntegrationRendererRenderOptions<unknown>,
|
|
153
|
-
render: async () => {
|
|
154
|
-
expect(getComponentRenderContext()).toBeUndefined();
|
|
155
|
-
return '<html><body><main>Plain render</main></body></html>';
|
|
156
|
-
},
|
|
157
|
-
getDocumentAttributes: () => undefined,
|
|
158
|
-
applyAttributesToHtmlElement: (html) => html,
|
|
159
|
-
applyAttributesToFirstBodyElement: (html) => html,
|
|
160
|
-
transformResponse: async (response) => await response.text(),
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
expect(result.body).toContain('<main>Plain render</main>');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('throws when route HTML returns unresolved boundary artifact HTML', async () => {
|
|
168
|
-
const service = new RenderExecutionService();
|
|
169
|
-
const HtmlTemplate = (() => '<html></html>') as EcoComponent<Record<string, unknown>>;
|
|
170
|
-
const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
|
|
171
|
-
|
|
172
|
-
await expect(
|
|
173
|
-
service.execute(
|
|
174
|
-
{
|
|
175
|
-
file: '/app/pages/index.tsx',
|
|
176
|
-
params: {},
|
|
177
|
-
query: {},
|
|
178
|
-
} as unknown as RouteRendererOptions,
|
|
179
|
-
{
|
|
180
|
-
prepareRenderOptions: async () =>
|
|
181
|
-
({
|
|
182
|
-
HtmlTemplate,
|
|
183
|
-
Page,
|
|
184
|
-
cacheStrategy: 'dynamic',
|
|
185
|
-
}) as unknown as IntegrationRendererRenderOptions<unknown>,
|
|
186
|
-
render: async () =>
|
|
187
|
-
'<html><body><eco-marker data-eco-node-id="n_1" data-eco-component-ref="unexpected-marker" data-eco-props-ref="p_1"></eco-marker></body></html>',
|
|
188
|
-
getDocumentAttributes: () => undefined,
|
|
189
|
-
applyAttributesToHtmlElement: (html) => html,
|
|
190
|
-
applyAttributesToFirstBodyElement: (html) => html,
|
|
191
|
-
transformResponse: async (response) => await response.text(),
|
|
192
|
-
},
|
|
193
|
-
),
|
|
194
|
-
).rejects.toThrow('Full-route unresolved-boundary fallback has been removed');
|
|
195
|
-
});
|
|
196
|
-
});
|