@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.
Files changed (111) hide show
  1. package/README.md +63 -7
  2. package/package.json +4 -47
  3. package/src/adapters/bun/create-app.ts +54 -2
  4. package/src/adapters/bun/hmr-manager.test.ts +0 -2
  5. package/src/adapters/bun/hmr-manager.ts +1 -24
  6. package/src/adapters/bun/server-adapter.ts +30 -4
  7. package/src/adapters/node/node-hmr-manager.test.ts +0 -2
  8. package/src/adapters/node/node-hmr-manager.ts +2 -25
  9. package/src/adapters/shared/explicit-static-render-preparation.ts +58 -0
  10. package/src/adapters/shared/explicit-static-route-matcher.test.ts +6 -6
  11. package/src/adapters/shared/explicit-static-route-matcher.ts +22 -31
  12. package/src/adapters/shared/file-route-middleware-pipeline.test.ts +5 -10
  13. package/src/adapters/shared/file-route-middleware-pipeline.ts +8 -17
  14. package/src/adapters/shared/fs-server-response-factory.test.ts +32 -43
  15. package/src/adapters/shared/fs-server-response-factory.ts +15 -37
  16. package/src/adapters/shared/fs-server-response-matcher.test.ts +65 -39
  17. package/src/adapters/shared/fs-server-response-matcher.ts +94 -43
  18. package/src/adapters/shared/hmr-manager.contract.test.ts +0 -4
  19. package/src/adapters/shared/render-context.ts +3 -3
  20. package/src/adapters/shared/server-adapter.test.ts +53 -0
  21. package/src/adapters/shared/server-adapter.ts +228 -159
  22. package/src/adapters/shared/server-route-handler.test.ts +6 -5
  23. package/src/adapters/shared/server-route-handler.ts +4 -4
  24. package/src/adapters/shared/server-static-builder.test.ts +4 -4
  25. package/src/adapters/shared/server-static-builder.ts +4 -4
  26. package/src/config/README.md +1 -1
  27. package/src/config/config-builder.test.ts +0 -1
  28. package/src/config/config-builder.ts +2 -7
  29. package/src/dev/host-runtime.ts +34 -0
  30. package/src/eco/eco.browser.test.ts +2 -2
  31. package/src/eco/eco.browser.ts +2 -2
  32. package/src/eco/eco.test.ts +6 -6
  33. package/src/eco/eco.ts +12 -12
  34. package/src/eco/eco.types.ts +3 -3
  35. package/src/errors/index.ts +1 -0
  36. package/src/hmr/client/hmr-runtime.ts +4 -2
  37. package/src/hmr/strategies/js-hmr-strategy.test.ts +0 -1
  38. package/src/hmr/strategies/js-hmr-strategy.ts +0 -6
  39. package/src/integrations/ghtml/ghtml-renderer.test.ts +7 -7
  40. package/src/integrations/ghtml/ghtml-renderer.ts +1 -11
  41. package/src/plugins/eco-component-meta-plugin.ts +0 -1
  42. package/src/plugins/integration-plugin.test.ts +9 -14
  43. package/src/plugins/integration-plugin.ts +34 -22
  44. package/src/plugins/processor.ts +17 -0
  45. package/src/route-renderer/GRAPH.md +81 -289
  46. package/src/route-renderer/README.md +67 -105
  47. package/src/route-renderer/orchestration/component-render-context.ts +45 -38
  48. package/src/route-renderer/orchestration/declared-ownership-graph.ts +62 -0
  49. package/src/route-renderer/orchestration/foreign-subtree-execution.service.ts +383 -0
  50. package/src/route-renderer/orchestration/integration-renderer.test.ts +118 -121
  51. package/src/route-renderer/orchestration/integration-renderer.ts +362 -403
  52. package/src/route-renderer/orchestration/ownership-planning.service.ts +97 -0
  53. package/src/route-renderer/orchestration/ownership-validation.service.ts +76 -0
  54. package/src/route-renderer/orchestration/processed-asset-dedupe.ts +1 -1
  55. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.test.ts → queued-foreign-subtree-resolution.service.test.ts} +76 -71
  56. package/src/route-renderer/orchestration/{queued-boundary-runtime.service.ts → queued-foreign-subtree-resolution.service.ts} +68 -63
  57. package/src/route-renderer/orchestration/render-output.utils.ts +21 -13
  58. package/src/route-renderer/orchestration/{render-preparation.service.test.ts → route-render-orchestrator.prepare-render-options.test.ts} +160 -85
  59. package/src/route-renderer/orchestration/route-render-orchestrator.test.ts +265 -0
  60. package/src/route-renderer/orchestration/{render-preparation.service.ts → route-render-orchestrator.ts} +244 -160
  61. package/src/route-renderer/page-loading/component-dependency-collection.ts +9 -3
  62. package/src/route-renderer/page-loading/declared-asset-collection.ts +2 -5
  63. package/src/route-renderer/page-loading/dependency-resolver.test.ts +107 -11
  64. package/src/route-renderer/page-loading/dependency-resolver.ts +6 -12
  65. package/src/route-renderer/page-loading/ecopages-virtual-imports.ts +1 -1
  66. package/src/route-renderer/page-loading/lazy-entry-collection.ts +1 -1
  67. package/src/route-renderer/page-loading/lazy-trigger-planning.ts +1 -1
  68. package/src/route-renderer/page-loading/module-declaration-aggregation.ts +1 -1
  69. package/src/route-renderer/page-loading/module-declaration-scripts.ts +1 -1
  70. package/src/route-renderer/page-loading/page-dependency-bundling.ts +105 -66
  71. package/src/route-renderer/route-renderer.ts +28 -31
  72. package/src/router/README.md +16 -19
  73. package/src/router/server/route-registry.test.ts +176 -0
  74. package/src/router/server/route-registry.ts +382 -0
  75. package/src/services/README.md +1 -2
  76. package/src/services/assets/asset-processing-service/asset-dependency-keys.ts +1 -1
  77. package/src/services/assets/asset-processing-service/asset-processing.service.test.ts +1 -4
  78. package/src/services/assets/asset-processing-service/asset-processing.service.ts +1 -2
  79. package/src/services/assets/asset-processing-service/assets.types.ts +3 -0
  80. package/src/services/assets/asset-processing-service/grouped-content-bundles.ts +1 -1
  81. package/src/services/assets/asset-processing-service/index.ts +1 -0
  82. package/src/{route-renderer/orchestration/page-packaging.service.test.ts → services/assets/asset-processing-service/page-package.test.ts} +38 -14
  83. package/src/services/assets/asset-processing-service/page-package.ts +93 -0
  84. package/src/services/assets/asset-processing-service/processors/base/base-script-processor.ts +4 -5
  85. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.test.ts +13 -10
  86. package/src/services/assets/asset-processing-service/processors/script/content-script.processor.ts +3 -0
  87. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.ts +6 -0
  88. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.ts +2 -0
  89. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.ts +1 -0
  90. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.ts +2 -0
  91. package/src/services/assets/asset-processing-service/ungrouped-dependency-processing.ts +1 -1
  92. package/src/services/html/html-transformer.service.test.ts +1 -4
  93. package/src/services/module-loading/app-server-module-transpiler.service.ts +1 -3
  94. package/src/services/module-loading/node-bootstrap-plugin.ts +17 -3
  95. package/src/services/module-loading/page-module-import.service.ts +0 -1
  96. package/src/services/module-loading/source-module-support.ts +1 -1
  97. package/src/static-site-generator/static-site-generator.test.ts +124 -32
  98. package/src/static-site-generator/static-site-generator.ts +168 -185
  99. package/src/types/internal-types.ts +13 -12
  100. package/src/types/public-types.ts +55 -39
  101. package/src/watchers/project-watcher.test-helpers.ts +4 -3
  102. package/src/route-renderer/orchestration/boundary-planning.service.ts +0 -146
  103. package/src/route-renderer/orchestration/page-packaging.service.ts +0 -85
  104. package/src/route-renderer/orchestration/render-execution.service.test.ts +0 -196
  105. package/src/route-renderer/orchestration/render-execution.service.ts +0 -182
  106. package/src/route-renderer/orchestration/route-shell-composer.service.ts +0 -162
  107. package/src/router/server/fs-router-scanner.test.ts +0 -83
  108. package/src/router/server/fs-router-scanner.ts +0 -224
  109. package/src/router/server/fs-router.test.ts +0 -214
  110. package/src/router/server/fs-router.ts +0 -122
  111. package/src/services/runtime-state/runtime-specifier-registry.service.ts +0 -96
@@ -0,0 +1,265 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type {
3
+ EcoComponent,
4
+ HtmlTemplateProps,
5
+ PageMetadataProps,
6
+ RouteRendererBody,
7
+ RouteRendererOptions,
8
+ } from '../../types/public-types.ts';
9
+ import {
10
+ type RouteHtmlFinalization,
11
+ type RouteRenderOrchestratorAdapter,
12
+ RouteRenderOrchestrator,
13
+ } from './route-render-orchestrator.ts';
14
+
15
+ function createFlowAdapter(input: {
16
+ resolvePageModule: (file: string) => Promise<{
17
+ Page: EcoComponent<Record<string, unknown>>;
18
+ integrationSpecificProps: Record<string, unknown>;
19
+ getStaticProps?: unknown;
20
+ getMetadata?: unknown;
21
+ }>;
22
+ getHtmlTemplate: () => Promise<EcoComponent<HtmlTemplateProps>>;
23
+ resolvePageData: (
24
+ pageModule: {
25
+ getStaticProps?: unknown;
26
+ getMetadata?: unknown;
27
+ },
28
+ routeOptions: RouteRendererOptions,
29
+ ) => Promise<{ props: Record<string, unknown>; metadata: PageMetadataProps }>;
30
+ resolveDependencies: (components: (EcoComponent | Partial<EcoComponent>)[]) => Promise<any[]>;
31
+ buildPageBrowserGraph: (file: string) => Promise<{ assets: any[] } | undefined>;
32
+ shouldRenderPageComponent: (input: {
33
+ Page: EcoComponent;
34
+ Layout?: EcoComponent;
35
+ options: RouteRendererOptions;
36
+ }) => boolean;
37
+ renderPageComponent: (input: {
38
+ Page: EcoComponent;
39
+ Layout?: EcoComponent;
40
+ props: Record<string, unknown>;
41
+ routeOptions: RouteRendererOptions;
42
+ }) => Promise<any>;
43
+ renderRouteBody: () => Promise<RouteRendererBody>;
44
+ getRouteHtmlFinalization?: () => RouteHtmlFinalization;
45
+ transformRouteResponse: (response: Response) => Promise<RouteRendererBody>;
46
+ }): RouteRenderOrchestratorAdapter<unknown> {
47
+ return {
48
+ name: 'test-renderer',
49
+ resolveRouteRenderInputs: async (routeOptions) => {
50
+ const pageModule = await input.resolvePageModule(routeOptions.file);
51
+ const HtmlTemplate = await input.getHtmlTemplate();
52
+ const Layout = pageModule.Page.config?.layout;
53
+ const { props, metadata } = await input.resolvePageData(pageModule, routeOptions);
54
+
55
+ return {
56
+ Page: pageModule.Page,
57
+ HtmlTemplate,
58
+ Layout,
59
+ props,
60
+ metadata,
61
+ integrationSpecificProps: pageModule.integrationSpecificProps,
62
+ shouldRenderPageComponent: input.shouldRenderPageComponent({
63
+ Page: pageModule.Page,
64
+ Layout,
65
+ options: routeOptions,
66
+ }),
67
+ };
68
+ },
69
+ resolveRouteAssets: async ({ routeOptions, components }) => ({
70
+ resolvedDependencies: await input.resolveDependencies(components),
71
+ pageBrowserGraph: await input.buildPageBrowserGraph(routeOptions.file),
72
+ }),
73
+ resolveRoutePageComponentRender: async (renderInput) => {
74
+ if (
75
+ !input.shouldRenderPageComponent({
76
+ Page: renderInput.Page,
77
+ Layout: renderInput.Layout,
78
+ options: renderInput.routeOptions,
79
+ })
80
+ ) {
81
+ return undefined;
82
+ }
83
+
84
+ return await input.renderPageComponent(renderInput);
85
+ },
86
+ renderRouteBody: input.renderRouteBody,
87
+ getRouteHtmlFinalization: input.getRouteHtmlFinalization ?? (() => ({})),
88
+ transformRouteResponse: input.transformRouteResponse,
89
+ };
90
+ }
91
+
92
+ describe('RouteRenderOrchestrator', () => {
93
+ const appConfig = {
94
+ cache: { defaultStrategy: 'static' },
95
+ defaultMetadata: { title: 'Default title', description: 'Default description' },
96
+ integrations: [],
97
+ } as any;
98
+ const assetProcessingService = {
99
+ processDependencies: async () => [],
100
+ } as any;
101
+
102
+ it('captures streamed render bodies before final HTML handling', async () => {
103
+ const flow = new RouteRenderOrchestrator(appConfig, assetProcessingService);
104
+ const encoder = new TextEncoder();
105
+
106
+ const result = await flow.captureHtmlRender(
107
+ async () =>
108
+ new ReadableStream({
109
+ start(controller) {
110
+ controller.enqueue(encoder.encode('<html><body><main>Streamed</main></body></html>'));
111
+ controller.close();
112
+ },
113
+ }) as unknown as BodyInit,
114
+ );
115
+
116
+ expect(result.body).toBeInstanceOf(ReadableStream);
117
+ expect(result.html).toContain('<main>Streamed</main>');
118
+ });
119
+
120
+ it('preserves streamed bodies when no foreign-subtree resolution or attribute stamping is required', async () => {
121
+ const flow = new RouteRenderOrchestrator(appConfig, assetProcessingService);
122
+ const encoder = new TextEncoder();
123
+ const HtmlTemplate = (() => '<html></html>') as EcoComponent<HtmlTemplateProps>;
124
+ const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
125
+ (Page as EcoComponent<Record<string, unknown>> & { cache?: unknown }).cache = { revalidate: 60 };
126
+
127
+ const result = await flow.execute(
128
+ {
129
+ file: '/app/pages/index.tsx',
130
+ params: {},
131
+ query: {},
132
+ } as unknown as RouteRendererOptions,
133
+ createFlowAdapter({
134
+ resolvePageModule: async () => ({ Page, integrationSpecificProps: {} }),
135
+ getHtmlTemplate: async () => HtmlTemplate,
136
+ resolvePageData: async () => ({ props: {}, metadata: appConfig.defaultMetadata }),
137
+ resolveDependencies: async () => [],
138
+ buildPageBrowserGraph: async () => undefined,
139
+ shouldRenderPageComponent: () => true,
140
+ renderPageComponent: async () => ({
141
+ html: '<main>Page</main>',
142
+ canAttachAttributes: true,
143
+ integrationName: 'ghtml',
144
+ }),
145
+ renderRouteBody: async () =>
146
+ new ReadableStream({
147
+ start(controller) {
148
+ controller.enqueue(encoder.encode('<html><body><main>Streamed</main></body></html>'));
149
+ controller.close();
150
+ },
151
+ }) as unknown as BodyInit,
152
+ transformRouteResponse: async (response) => response.body as RouteRendererBody,
153
+ }),
154
+ );
155
+
156
+ expect(result.cacheStrategy).toEqual({ revalidate: 60 });
157
+ expect(result.body).toBeInstanceOf(ReadableStream);
158
+ expect(await new Response(result.body as BodyInit).text()).toContain('<main>Streamed</main>');
159
+ });
160
+
161
+ it('applies root and document attributes to fully resolved route HTML', async () => {
162
+ const flow = new RouteRenderOrchestrator(appConfig, assetProcessingService);
163
+ const HtmlTemplate = (() => '<html></html>') as EcoComponent<HtmlTemplateProps>;
164
+ const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
165
+ (Page as EcoComponent<Record<string, unknown>> & { cache?: unknown }).cache = { revalidate: 60 };
166
+
167
+ const result = await flow.execute(
168
+ {
169
+ file: '/app/pages/index.tsx',
170
+ params: {},
171
+ query: {},
172
+ } as unknown as RouteRendererOptions,
173
+ createFlowAdapter({
174
+ resolvePageModule: async () => ({ Page, integrationSpecificProps: {} }),
175
+ getHtmlTemplate: async () => HtmlTemplate,
176
+ resolvePageData: async () => ({ props: {}, metadata: appConfig.defaultMetadata }),
177
+ resolveDependencies: async () => [],
178
+ buildPageBrowserGraph: async () => undefined,
179
+ shouldRenderPageComponent: () => true,
180
+ renderPageComponent: async () => ({
181
+ html: '<main>Page</main>',
182
+ canAttachAttributes: true,
183
+ integrationName: 'ghtml',
184
+ rootAttributes: { 'data-eco-component-id': 'eco-page-root' },
185
+ }),
186
+ renderRouteBody: async () => '<html><body><main>Resolved</main></body></html>',
187
+ getRouteHtmlFinalization: () => ({
188
+ finalizeHtml: (html) =>
189
+ html
190
+ .replace('<html', '<html data-eco-document-owner="react-router"')
191
+ .replace('<main', '<main data-eco-component-id="eco-page-root"'),
192
+ }),
193
+ transformRouteResponse: async (response) => await response.text(),
194
+ }),
195
+ );
196
+
197
+ expect(result.cacheStrategy).toEqual({ revalidate: 60 });
198
+ expect(result.body).toContain('<html data-eco-document-owner="react-router"><body>');
199
+ expect(result.body).toContain('<main data-eco-component-id="eco-page-root">Resolved</main>');
200
+ });
201
+
202
+ it('throws when route HTML contains escaped unresolved eco-marker artifacts', async () => {
203
+ const flow = new RouteRenderOrchestrator(appConfig, assetProcessingService);
204
+ const HtmlTemplate = (() => '<html></html>') as EcoComponent<HtmlTemplateProps>;
205
+ const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
206
+
207
+ await expect(
208
+ flow.execute(
209
+ {
210
+ file: '/app/pages/index.tsx',
211
+ params: {},
212
+ query: {},
213
+ } as unknown as RouteRendererOptions,
214
+ createFlowAdapter({
215
+ resolvePageModule: async () => ({ Page, integrationSpecificProps: {} }),
216
+ getHtmlTemplate: async () => HtmlTemplate,
217
+ resolvePageData: async () => ({ props: {}, metadata: appConfig.defaultMetadata }),
218
+ resolveDependencies: async () => [],
219
+ buildPageBrowserGraph: async () => undefined,
220
+ shouldRenderPageComponent: () => true,
221
+ renderPageComponent: async () => ({
222
+ html: '<main>Page</main>',
223
+ canAttachAttributes: true,
224
+ integrationName: 'ghtml',
225
+ }),
226
+ renderRouteBody: async () =>
227
+ '<html><body>&amp;lt;eco-marker data-eco-node-id=&quot;n_2&quot; data-eco-component-ref=&quot;page-component&quot; data-eco-props-ref=&quot;p_2&quot;&amp;gt;&amp;lt;/eco-marker&amp;gt;</body></html>',
228
+ transformRouteResponse: async (response) => await response.text(),
229
+ }),
230
+ ),
231
+ ).rejects.toThrow('Full-route unresolved-marker fallback has been removed');
232
+ });
233
+
234
+ it('throws when route HTML returns unresolved eco-marker artifact HTML', async () => {
235
+ const flow = new RouteRenderOrchestrator(appConfig, assetProcessingService);
236
+ const HtmlTemplate = (() => '<html></html>') as EcoComponent<HtmlTemplateProps>;
237
+ const Page = (() => '<main>Page</main>') as EcoComponent<Record<string, unknown>>;
238
+
239
+ await expect(
240
+ flow.execute(
241
+ {
242
+ file: '/app/pages/index.tsx',
243
+ params: {},
244
+ query: {},
245
+ } as unknown as RouteRendererOptions,
246
+ createFlowAdapter({
247
+ resolvePageModule: async () => ({ Page, integrationSpecificProps: {} }),
248
+ getHtmlTemplate: async () => HtmlTemplate,
249
+ resolvePageData: async () => ({ props: {}, metadata: appConfig.defaultMetadata }),
250
+ resolveDependencies: async () => [],
251
+ buildPageBrowserGraph: async () => undefined,
252
+ shouldRenderPageComponent: () => true,
253
+ renderPageComponent: async () => ({
254
+ html: '<main>Page</main>',
255
+ canAttachAttributes: true,
256
+ integrationName: 'ghtml',
257
+ }),
258
+ renderRouteBody: async () =>
259
+ '<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>',
260
+ transformRouteResponse: async (response) => await response.text(),
261
+ }),
262
+ ),
263
+ ).rejects.toThrow('Full-route unresolved-marker fallback has been removed');
264
+ });
265
+ });