@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,34 @@
1
+ import type { EcoPagesAppConfig } from '../types/public-types.ts';
2
+ import {
3
+ DevelopmentInvalidationService,
4
+ type DevelopmentInvalidationPlan,
5
+ } from '../services/invalidation/development-invalidation.service.ts';
6
+ import { setHostModuleLoader } from '../services/module-loading/host-module-loader-registry.ts';
7
+
8
+ export type HostRuntimeModuleLoader = (id: string) => Promise<unknown>;
9
+
10
+ export interface DevelopmentHostRuntime {
11
+ registerHostModuleLoader(loader: HostRuntimeModuleLoader): void;
12
+ planFileChange(filePath: string): DevelopmentInvalidationPlan;
13
+ invalidateServerModules(changedFiles?: string[]): void;
14
+ resetRuntimeState(changedFiles?: string[]): void;
15
+ }
16
+
17
+ export function createDevelopmentHostRuntime(appConfig: EcoPagesAppConfig): DevelopmentHostRuntime {
18
+ const invalidationService = new DevelopmentInvalidationService(appConfig);
19
+
20
+ return {
21
+ registerHostModuleLoader(loader) {
22
+ setHostModuleLoader(loader);
23
+ },
24
+ planFileChange(filePath) {
25
+ return invalidationService.planFileChange(filePath);
26
+ },
27
+ invalidateServerModules(changedFiles) {
28
+ invalidationService.invalidateServerModules(changedFiles);
29
+ },
30
+ resetRuntimeState(changedFiles) {
31
+ invalidationService.resetRuntimeState(changedFiles);
32
+ },
33
+ };
34
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest';
2
- import type { Middleware } from '../types/public-types.ts';
2
+ import type { FileRouteMiddleware } from '../types/public-types.ts';
3
3
  import { eco } from './eco.browser.ts';
4
4
 
5
5
  describe('browser eco facade', () => {
@@ -10,7 +10,7 @@ describe('browser eco facade', () => {
10
10
 
11
11
  const staticPaths = async () => ({ paths: [{ params: { slug: 'intro' } }] });
12
12
  const metadata = async () => ({ title: 'Docs', description: 'Docs page' });
13
- const middleware: Middleware[] = [async (_ctx, next) => next()];
13
+ const middleware: FileRouteMiddleware[] = [async (_ctx, next) => next()];
14
14
 
15
15
  const Page = eco.page({
16
16
  layout: Layout,
@@ -4,10 +4,10 @@ import type {
4
4
  EcoLayoutComponent,
5
5
  EcoPagesElement,
6
6
  EcoPageComponent,
7
+ FileRouteMiddleware,
7
8
  GetMetadata,
8
9
  GetStaticPaths,
9
10
  GetStaticProps,
10
- Middleware,
11
11
  RequestPageContext,
12
12
  } from '../types/public-types.ts';
13
13
  import type { CacheStrategy } from '../services/cache/cache.types.ts';
@@ -38,7 +38,7 @@ function layout<E = EcoPagesElement>(options: LayoutOptions<E>): EcoLayoutCompon
38
38
  }
39
39
 
40
40
  function page<T, E>(
41
- options: PageOptionsBase<T, E> & { cache?: CacheStrategy; middleware?: Middleware[] },
41
+ options: PageOptionsBase<T, E> & { cache?: CacheStrategy; middleware?: FileRouteMiddleware[] },
42
42
  ): EcoPageComponent<T> {
43
43
  const {
44
44
  layout: pageLayout,
@@ -13,16 +13,16 @@ import type {
13
13
  } from '../types/public-types.ts';
14
14
  import type { EcoPagesAppConfig } from '../types/internal-types.ts';
15
15
  import {
16
- type ComponentBoundaryRuntime,
16
+ type ForeignChildRuntime,
17
17
  getComponentRenderContext,
18
18
  runWithComponentRenderContext,
19
19
  } from '../route-renderer/orchestration/component-render-context.ts';
20
20
 
21
21
  const mockAppConfig = {} as EcoPagesAppConfig;
22
22
 
23
- function createResolvedBoundaryRuntime(targetIntegrations: string[], value: string): ComponentBoundaryRuntime {
23
+ function createResolvedForeignChildRuntime(targetIntegrations: string[], value: string): ForeignChildRuntime {
24
24
  return {
25
- interceptBoundary: async ({ targetIntegration }: { targetIntegration?: string }) =>
25
+ interceptForeignChild: async ({ targetIntegration }: { targetIntegration?: string }) =>
26
26
  targetIntegration !== undefined && targetIntegrations.includes(targetIntegration)
27
27
  ? ({ kind: 'resolved', value } as const)
28
28
  : ({ kind: 'inline' } as const),
@@ -204,7 +204,7 @@ describe('eco namespace', () => {
204
204
  expect(Component.config?.integration).toBe('lit');
205
205
  });
206
206
 
207
- test('should render inline when boundary context returns inline for a React component', async () => {
207
+ test('should render inline when the foreign-child runtime returns inline for a React component', async () => {
208
208
  const ReactButton = eco.component({
209
209
  integration: 'react',
210
210
  __eco: {
@@ -225,7 +225,7 @@ describe('eco namespace', () => {
225
225
  expect(execution.value).toBe('<button type="button">Click</button>');
226
226
  });
227
227
 
228
- test('should resolve foreign boundaries immediately when the runtime returns resolved output', async () => {
228
+ test('should resolve foreign children immediately when the runtime returns resolved output', async () => {
229
229
  const ReactButton = eco.component({
230
230
  integration: 'react',
231
231
  __eco: {
@@ -239,7 +239,7 @@ describe('eco namespace', () => {
239
239
  const execution = await runWithComponentRenderContext(
240
240
  {
241
241
  currentIntegration: 'lit',
242
- boundaryRuntime: createResolvedBoundaryRuntime(
242
+ foreignChildRuntime: createResolvedForeignChildRuntime(
243
243
  ['react'],
244
244
  '<aside>Resolved in owning renderer</aside>',
245
245
  ),
package/src/eco/eco.ts CHANGED
@@ -9,10 +9,10 @@ import type {
9
9
  EcoLayoutComponent,
10
10
  EcoPagesElement,
11
11
  EcoPageComponent,
12
+ FileRouteMiddleware,
12
13
  GetMetadata,
13
14
  GetStaticPaths,
14
15
  GetStaticProps,
15
- Middleware,
16
16
  RequestLocals,
17
17
  RequestPageContext,
18
18
  } from '../types/public-types.ts';
@@ -30,19 +30,19 @@ import type {
30
30
  } from './eco.types.ts';
31
31
  import {
32
32
  finalizeComponentRender,
33
- interceptComponentBoundary,
33
+ interceptForeignChild,
34
34
  } from '../route-renderer/orchestration/component-render-context.ts';
35
35
  import { isThenable } from '../route-renderer/orchestration/render-output.utils.ts';
36
36
 
37
37
  /**
38
- * Creates a component factory with lazy-trigger support and boundary-runtime
38
+ * Creates a component factory with lazy-trigger support and foreign-child-runtime
39
39
  * interception.
40
40
  *
41
41
  * Behavior:
42
42
  * - In normal render flow, returns `options.render(props)` with optional lazy
43
43
  * trigger/script wrapping.
44
- * - When rendering under an active component boundary runtime and the current
45
- * renderer-owned boundary runtime resolves the boundary immediately, returns
44
+ * - When rendering under an active foreign-child runtime and the current
45
+ * renderer-owned foreign-child runtime resolves the foreign child immediately, returns
46
46
  * that resolved output instead of rendering the component inline.
47
47
  *
48
48
  * @param options Component options for rendering and dependency declaration.
@@ -53,20 +53,20 @@ function createComponentFactory<P, E>(options: ComponentOptions<P, E>): EcoCompo
53
53
  const comp: EcoComponent<P, E> = ((props: P) => {
54
54
  const componentProps = (props ?? {}) as Record<string, unknown>;
55
55
  const renderInline = () => finalizeComponentRender(comp, options.render(props)) as E;
56
- const boundaryRender = interceptComponentBoundary({
56
+ const foreignChildRender = interceptForeignChild({
57
57
  component: comp,
58
58
  props: componentProps,
59
59
  targetIntegration: integrationName,
60
60
  });
61
61
 
62
- if (isThenable<unknown | undefined>(boundaryRender)) {
63
- return boundaryRender.then((resolvedBoundaryRender) =>
64
- resolvedBoundaryRender !== undefined ? (resolvedBoundaryRender as E) : renderInline(),
62
+ if (isThenable<unknown | undefined>(foreignChildRender)) {
63
+ return foreignChildRender.then((resolvedForeignChildRender) =>
64
+ resolvedForeignChildRender !== undefined ? (resolvedForeignChildRender as E) : renderInline(),
65
65
  ) as E;
66
66
  }
67
67
 
68
- if (boundaryRender !== undefined) {
69
- return boundaryRender as E;
68
+ if (foreignChildRender !== undefined) {
69
+ return foreignChildRender as E;
70
70
  }
71
71
 
72
72
  return renderInline();
@@ -129,7 +129,7 @@ function page<T = {}, E = EcoPagesElement, const K extends keyof RequestLocals =
129
129
  * @returns Eco page component.
130
130
  */
131
131
  function page<T, E>(
132
- options: PageOptionsBase<T, E> & { cache?: CacheStrategy; middleware?: Middleware[] },
132
+ options: PageOptionsBase<T, E> & { cache?: CacheStrategy; middleware?: FileRouteMiddleware[] },
133
133
  ): EcoPageComponent<T> {
134
134
  const { layout, dependencies, render, staticPaths, staticProps, metadata, cache, requires, middleware } = options;
135
135
 
@@ -12,12 +12,12 @@ import type {
12
12
  EcoLayoutComponent,
13
13
  EcoPageLayoutComponent,
14
14
  EcoPagesElement,
15
+ FileRouteMiddleware,
15
16
  GetMetadata,
16
17
  GetStaticPaths,
17
18
  GetStaticProps,
18
19
  HtmlTemplateProps,
19
20
  LayoutProps,
20
- Middleware,
21
21
  RequestLocals,
22
22
  RequestPageContext,
23
23
  } from '../types/public-types.ts';
@@ -119,7 +119,7 @@ interface PageOptionsWithMiddleware<T, E = EcoPagesElement> extends PageOptionsB
119
119
  * Request-time middleware for file-based routes.
120
120
  * Runs before rendering and can short-circuit by returning a Response.
121
121
  */
122
- middleware: Middleware[];
122
+ middleware: FileRouteMiddleware[];
123
123
  }
124
124
 
125
125
  /**
@@ -164,7 +164,7 @@ export type EcoPageComponent<T> = EcoComponent<PagePropsFor<T> & Partial<Request
164
164
  metadata?: GetMetadata<T>;
165
165
  cache?: CacheStrategy;
166
166
  requires?: PageRequires;
167
- middleware?: Middleware[];
167
+ middleware?: FileRouteMiddleware[];
168
168
  };
169
169
 
170
170
  /**
@@ -1,2 +1,3 @@
1
1
  export { HttpError } from './http-error.ts';
2
2
  export type { HttpErrorDetails, HttpErrorJson } from './http-error.ts';
3
+ export { LocalsAccessError } from './locals-access-error.ts';
@@ -52,7 +52,9 @@ interface HMRPayload {
52
52
  break;
53
53
  case 'layout-update': {
54
54
  await waitForNavigationToSettle(navigationRuntime);
55
- if (await navigationRuntime.reloadCurrentPage({ clearCache: true, moduleUrl: getActiveHmrModuleUrl() })) {
55
+ if (
56
+ await navigationRuntime.reloadCurrentPage({ clearCache: true, moduleUrl: getActiveHmrModuleUrl() })
57
+ ) {
56
58
  } else {
57
59
  location.reload();
58
60
  }
@@ -107,7 +109,7 @@ interface HMRPayload {
107
109
  }
108
110
 
109
111
  const handlerPaths = Object.keys(window.__ECO_PAGES__?.hmrHandlers ?? {});
110
- return handlerPaths.at(-1);
112
+ return handlerPaths[handlerPaths.length - 1];
111
113
  }
112
114
 
113
115
  async function waitForNavigationToSettle(navigationRuntime: ReturnType<typeof getEcoNavigationRuntime>) {
@@ -12,7 +12,6 @@ const SRC_DIR = path.join(TMP_DIR, 'src');
12
12
  function createMockContext(overrides: Partial<JsHmrContext> = {}): JsHmrContext {
13
13
  return {
14
14
  getWatchedFiles: () => new Map(),
15
- getSpecifierMap: () => new Map(),
16
15
  getEntrypointDependencyGraph: () => new NoopDevGraphService(),
17
16
  getDistDir: () => TMP_DIR,
18
17
  getPlugins: () => [],
@@ -26,11 +26,6 @@ export interface JsHmrContext {
26
26
  */
27
27
  getWatchedFiles(): Map<string, string>;
28
28
 
29
- /**
30
- * Map of bare specifiers to vendor URLs for import resolution.
31
- */
32
- getSpecifierMap(): Map<string, string>;
33
-
34
29
  getEntrypointDependencyGraph(): EntrypointDependencyGraph;
35
30
 
36
31
  /**
@@ -101,7 +96,6 @@ export interface JsHmrContext {
101
96
  * ```typescript
102
97
  * const context = {
103
98
  * getWatchedFiles: () => watchedFilesMap,
104
- * getSpecifierMap: () => specifierMap,
105
99
  * getDistDir: () => '/path/to/dist/_hmr',
106
100
  * getPlugins: () => [],
107
101
  * getSrcDir: () => '/path/to/src'
@@ -3,7 +3,7 @@ import HtmlTemplate from '../../../__fixtures__/app/src/includes/html.ghtml.js';
3
3
  import { FIXTURE_APP_PROJECT_DIR } from '../../../__fixtures__/constants.js';
4
4
  import {
5
5
  eco,
6
- type BoundaryRenderPayload,
6
+ type ForeignSubtreeRenderPayload,
7
7
  type EcoComponent,
8
8
  type EcoPagesElement,
9
9
  type HtmlTemplateProps,
@@ -121,7 +121,7 @@ describe('GhtmlRenderer', () => {
121
121
  ).rejects.toThrow('Error rendering page: Page failed to render');
122
122
  });
123
123
 
124
- it('should resolve deferred foreign layout content without unresolved boundary artifacts', async () => {
124
+ it('should resolve deferred foreign layout content without unresolved eco-marker artifacts', async () => {
125
125
  const deferredPlugin = new DeferredPlugin();
126
126
  const config = await new ConfigBuilder()
127
127
  .setRootDir(FIXTURE_APP_PROJECT_DIR)
@@ -177,17 +177,17 @@ describe('GhtmlRenderer', () => {
177
177
  expect(body).not.toContain('<eco-marker');
178
178
  });
179
179
 
180
- it('should expose the compatibility boundary payload contract', async () => {
180
+ it('should expose the compatibility foreign-subtree payload contract', async () => {
181
181
  const renderer = createRenderer();
182
- const Component = (async () => '<main>Boundary</main>') as EcoComponent<Record<string, unknown>>;
182
+ const Component = (async () => '<main>Foreign Subtree</main>') as EcoComponent<Record<string, unknown>>;
183
183
 
184
- const result = await renderer.renderBoundary({
184
+ const result = await renderer.renderForeignSubtree({
185
185
  component: Component,
186
186
  props: {},
187
187
  });
188
188
 
189
- expect(result).toEqual<BoundaryRenderPayload>({
190
- html: '<main>Boundary</main>',
189
+ expect(result).toEqual<ForeignSubtreeRenderPayload>({
190
+ html: '<main>Foreign Subtree</main>',
191
191
  assets: [],
192
192
  rootTag: 'main',
193
193
  rootAttributes: undefined,
@@ -30,22 +30,12 @@ export class GhtmlRenderer extends IntegrationRenderer<EcoPagesElement> {
30
30
  name = GHTML_PLUGIN_NAME;
31
31
 
32
32
  override async renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult> {
33
- return this.renderStringComponentBoundaryWithQueuedForeignBoundaries(
33
+ return this.renderStringComponentWithQueuedForeignSubtrees(
34
34
  input,
35
35
  input.component as GhtmlViewFn<Record<string, unknown>>,
36
36
  );
37
37
  }
38
38
 
39
- protected override createComponentBoundaryRuntime(options: {
40
- boundaryInput: ComponentRenderInput;
41
- rendererCache: Map<string, IntegrationRenderer<any>>;
42
- }) {
43
- return this.createQueuedBoundaryRuntime({
44
- boundaryInput: options.boundaryInput,
45
- rendererCache: options.rendererCache,
46
- });
47
- }
48
-
49
39
  async render({
50
40
  params,
51
41
  query,
@@ -26,7 +26,6 @@
26
26
  * @module eco-component-meta-plugin
27
27
  */
28
28
 
29
- import path from 'node:path';
30
29
  import { parseSync } from 'oxc-parser';
31
30
  import type { EcoBuildPlugin } from '../build/build-types.ts';
32
31
  import type { EcoPagesAppConfig } from '../types/internal-types.ts';
@@ -95,11 +95,10 @@ describe('IntegrationPlugin', () => {
95
95
  await expect(plugin.teardown()).resolves.toBeUndefined();
96
96
  });
97
97
 
98
- it('should register runtime specifier maps through the base HMR setup', () => {
99
- const registerSpecifierMap = vi.fn();
98
+ it('should register HMR strategies through the base HMR setup', () => {
99
+ const registerStrategy = vi.fn();
100
100
  const hmrManager = {
101
- registerSpecifierMap,
102
- registerStrategy: vi.fn(),
101
+ registerStrategy,
103
102
  registerEntrypoint: vi.fn(),
104
103
  registerScriptEntrypoint: vi.fn(),
105
104
  setPlugins: vi.fn(),
@@ -108,26 +107,22 @@ describe('IntegrationPlugin', () => {
108
107
  broadcast: vi.fn(),
109
108
  getOutputUrl: vi.fn(),
110
109
  getWatchedFiles: vi.fn(() => new Map()),
111
- getSpecifierMap: vi.fn(() => new Map()),
112
110
  getDistDir: vi.fn(() => ''),
113
111
  getPlugins: vi.fn(() => []),
114
112
  getDefaultContext: vi.fn(),
115
113
  handleFileChange: vi.fn(),
116
114
  } satisfies IHmrManager;
117
115
 
118
- const pluginWithRuntimeSpecifiers = new (class extends TestIntegrationPlugin {
119
- override getRuntimeSpecifierMap(): Record<string, string> {
120
- return {
121
- 'test-runtime': '/assets/vendors/test-runtime.js',
122
- };
116
+ const strategy = { matches: vi.fn(() => false), process: vi.fn(), priority: 10, type: 'integration' } as any;
117
+ const pluginWithStrategy = new (class extends TestIntegrationPlugin {
118
+ override getHmrStrategy() {
119
+ return strategy;
123
120
  }
124
121
  })(config);
125
122
 
126
- pluginWithRuntimeSpecifiers.setHmrManager(hmrManager);
123
+ pluginWithStrategy.setHmrManager(hmrManager);
127
124
 
128
- expect(registerSpecifierMap).toHaveBeenCalledWith({
129
- 'test-runtime': '/assets/vendors/test-runtime.js',
130
- });
125
+ expect(registerStrategy).toHaveBeenCalledWith(strategy);
131
126
  });
132
127
 
133
128
  it('should stamp the plugin integration name onto initialized renderers', () => {
@@ -5,15 +5,45 @@ import type { EcoPagesElement } from '../types/public-types.ts';
5
5
  import type { IntegrationRenderer } from '../route-renderer/orchestration/integration-renderer.ts';
6
6
  import { AssetProcessingService } from '../services/assets/asset-processing-service/asset-processing.service.ts';
7
7
  import type { AssetDefinition, ProcessedAsset } from '../services/assets/asset-processing-service/assets.types.ts';
8
+ import { deepMerge } from '../utils/deep-merge.ts';
9
+ import { invariant } from '../utils/invariant.ts';
8
10
  import type { RuntimeCapabilityDeclaration } from './runtime-capability.ts';
9
11
 
10
12
  export type { RuntimeCapabilityDeclaration, RuntimeCapabilityTag } from './runtime-capability.ts';
13
+ export type {
14
+ EcoBuildLoader,
15
+ EcoBuildOnLoadArgs,
16
+ EcoBuildOnLoadResult,
17
+ EcoBuildOnResolveArgs,
18
+ EcoBuildOnResolveResult,
19
+ EcoBuildPlugin,
20
+ EcoBuildPluginBuilder,
21
+ } from '../build/build-types.ts';
22
+
23
+ /**
24
+ * Type-erased integration plugin stored in app-level registries.
25
+ *
26
+ * Ecopages keeps one heterogeneous integration list, while each plugin and
27
+ * renderer still owns its framework-specific render payload type internally.
28
+ */
29
+ export type AnyIntegrationPlugin = IntegrationPlugin<unknown>;
11
30
 
12
31
  export const INTEGRATION_PLUGIN_ERRORS = {
13
32
  NOT_INITIALIZED_WITH_APP_CONFIG: 'Plugin not initialized with app config',
14
33
  NOT_INITIALIZED_WITH_ASSET_SERVICE: 'Plugin not initialized with asset dependency service',
15
34
  } as const;
16
35
 
36
+ export function mergeIntegrationOptions<TDefaults, TOverrides>(
37
+ defaults: TDefaults,
38
+ overrides: TOverrides,
39
+ ): TDefaults & TOverrides {
40
+ return deepMerge(defaults, overrides);
41
+ }
42
+
43
+ export function assertIntegrationInvariant(condition: boolean, message?: string): asserts condition {
44
+ invariant(condition, message);
45
+ }
46
+
17
47
  /**
18
48
  * Base configuration shared by all integration plugins.
19
49
  *
@@ -157,40 +187,22 @@ export abstract class IntegrationPlugin<C = EcoPagesElement> {
157
187
  * ```typescript
158
188
  * getHmrStrategy(): HmrStrategy {
159
189
  * const context = this.hmrManager!.getDefaultContext();
160
- * return new ReactHmrStrategy(context);
190
+ * return new ReactHmrStrategy({ context, pageMetadataCache, runtimeAliasMap });
161
191
  * }
162
192
  * ```
163
193
  */
164
194
  getHmrStrategy?(): HmrStrategy | undefined;
165
195
 
166
- /**
167
- * Returns bare-specifier mappings that should be registered in the active
168
- * runtime specifier registry.
169
- *
170
- * @remarks
171
- * Integrations that own browser runtime bundles can override this to expose
172
- * stable bare specifiers for client-side imports.
173
- *
174
- * Today these mappings are consumed by the development runtime and browser
175
- * bundle aliasing path. They are intentionally generic enough to grow into a
176
- * broader import-map-style facility later without moving framework-specific
177
- * map contents into core.
178
- */
179
- getRuntimeSpecifierMap(): Record<string, string> {
180
- return {};
181
- }
182
-
183
196
  /**
184
197
  * Attaches the shared HMR manager and registers integration-owned development hooks.
185
198
  *
186
199
  * @remarks
187
- * The default implementation registers both runtime bare-specifier mappings and
188
- * the optional integration HMR strategy. Integrations should override this only
189
- * when they need to extend that shared behavior rather than replace it.
200
+ * The default implementation registers the optional integration HMR strategy.
201
+ * Integrations should override this only when they need to extend that shared
202
+ * behavior rather than replace it.
190
203
  */
191
204
  setHmrManager(hmrManager: IHmrManager) {
192
205
  this.hmrManager = hmrManager;
193
- hmrManager.registerSpecifierMap(this.getRuntimeSpecifierMap());
194
206
 
195
207
  const strategy = this.getHmrStrategy?.();
196
208
  if (strategy) {
@@ -5,14 +5,31 @@ import type { EcoPagesAppConfig, IClientBridge } from '../types/internal-types.t
5
5
  import type { AssetDefinition } from '../services/assets/asset-processing-service/assets.types.ts';
6
6
  import { DEFAULT_ECOPAGES_WORK_DIR } from '../config/constants.ts';
7
7
  import { GENERATED_BASE_PATHS } from '../config/constants.ts';
8
+ import { deepMerge } from '../utils/deep-merge.ts';
8
9
  import type { RuntimeCapabilityDeclaration } from './runtime-capability.ts';
9
10
 
10
11
  export type { RuntimeCapabilityDeclaration, RuntimeCapabilityTag } from './runtime-capability.ts';
12
+ export type {
13
+ EcoBuildLoader,
14
+ EcoBuildOnLoadArgs,
15
+ EcoBuildOnLoadResult,
16
+ EcoBuildOnResolveArgs,
17
+ EcoBuildOnResolveResult,
18
+ EcoBuildPlugin,
19
+ EcoBuildPluginBuilder,
20
+ } from '../build/build-types.ts';
11
21
 
12
22
  export const PROCESSOR_ERRORS = {
13
23
  CACHE_DIRECTORY_NOT_SET: 'Cache directory not set in context',
14
24
  } as const;
15
25
 
26
+ export function mergeProcessorOptions<TDefaults, TOverrides>(
27
+ defaults: TDefaults,
28
+ overrides: TOverrides,
29
+ ): TDefaults & TOverrides {
30
+ return deepMerge(defaults, overrides);
31
+ }
32
+
16
33
  function resolveGeneratedPath(
17
34
  type: keyof typeof GENERATED_BASE_PATHS,
18
35
  options: { root: string; module: string; subPath?: string },