@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
package/README.md CHANGED
@@ -31,14 +31,14 @@ flowchart TD
31
31
  B --> D[App build manifest]
32
32
  B --> E[Build executor]
33
33
  B --> F[Dev graph service]
34
- B --> G[Runtime specifier registry]
35
- B --> H[Host module loader boundary]
36
- H --> I[PageModuleImportService]
34
+ B --> G[Host module loader boundary]
35
+ G --> H[PageModuleImportService]
36
+ E --> H
37
37
  E --> I
38
- E --> J[BrowserBundleService]
39
- I --> K[Runtime app adapter]
40
- K --> L[Bun adapter or Node adapter]
41
- D --> J
38
+ E --> I[BrowserBundleService]
39
+ H --> J[Runtime app adapter]
40
+ J --> K[Bun adapter or Node adapter]
41
+ D --> I
42
42
  ```
43
43
 
44
44
  ### Development Invalidation And HMR Flow
@@ -178,6 +178,12 @@ export const MyButton = eco.component({
178
178
  });
179
179
  ```
180
180
 
181
+ Dependency ownership affects final asset packaging:
182
+
183
+ - Stylesheets and scripts declared from `eco.html()` stay Html-owned and can be emitted as shared app-wide assets.
184
+ - Stylesheets and scripts declared from Pages, Layouts, or Components are resolved into page-owned assets for the rendered route.
185
+ - This split is intentional. Shared Html assets can be cached across routes, while page-owned assets can change without invalidating the global shell.
186
+
181
187
  ### 5. API Handlers
182
188
 
183
189
  Add server-side routes using `defineApiHandler`. Register them on your `app` instance before starting:
@@ -228,3 +234,53 @@ import { defineApiHandler, defineGroupHandler, eco } from '@ecopages/core';
228
234
  Use runtime-specific subpaths only when you explicitly need Bun-native APIs that bypass the universal abstractions:
229
235
 
230
236
  - `@ecopages/core/bun`
237
+
238
+ ## Entry Point Roles
239
+
240
+ The published subpaths are grouped by architectural role rather than by source folder.
241
+
242
+ ### App Authoring
243
+
244
+ Use these entrypoints when building an Ecopages app:
245
+
246
+ - `@ecopages/core`
247
+ - `@ecopages/core/create-app`
248
+ - `@ecopages/core/config-builder`
249
+ - `@ecopages/core/errors`
250
+ - `@ecopages/core/html`
251
+ - `@ecopages/core/hash`
252
+ - `@ecopages/core/declarations`
253
+ - `@ecopages/core/env`
254
+ - `@ecopages/core/bun`
255
+
256
+ ### Browser Navigation
257
+
258
+ Use these entrypoints when a browser runtime needs to coordinate document ownership and link intent:
259
+
260
+ - `@ecopages/core/router/navigation-coordinator`
261
+ - `@ecopages/core/router/link-intent`
262
+
263
+ ### Extension Authoring
264
+
265
+ Use these entrypoints when implementing integrations, processors, or source transforms:
266
+
267
+ - `@ecopages/core/plugins/integration-plugin`
268
+ - `@ecopages/core/plugins/processor`
269
+ - `@ecopages/core/plugins/source-transform`
270
+ - `@ecopages/core/route-renderer/integration-renderer`
271
+ - `@ecopages/core/services/asset-processing-service`
272
+ - `@ecopages/core/hmr/hmr-strategy`
273
+ - `@ecopages/core/integrations/ghtml`
274
+
275
+ ### Host And Runtime Composition
276
+
277
+ Use these entrypoints only when implementing host adapters or framework-owned bundling seams:
278
+
279
+ - `@ecopages/core/dev/host-runtime`
280
+ - `@ecopages/core/build/build-adapter`
281
+ - `@ecopages/core/build/build-types`
282
+ - `@ecopages/core/build/runtime-specifier-alias-plugin`
283
+ - `@ecopages/core/build/runtime-specifier-aliases`
284
+ - `@ecopages/core/plugins/foreign-jsx-override-plugin`
285
+
286
+ These host-facing entrypoints are narrower compatibility seams. App code and most extensions should prefer the app-authoring or extension-authoring surfaces.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-alpha.25",
3
+ "version": "0.2.0-alpha.27",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -66,23 +66,11 @@
66
66
  "types": "./src/router/client/link-intent.ts",
67
67
  "default": "./src/router/client/link-intent.ts"
68
68
  },
69
- "./router/client/navigation-coordinator": {
70
- "types": "./src/router/client/navigation-coordinator.ts",
71
- "default": "./src/router/client/navigation-coordinator.ts"
72
- },
73
- "./errors/locals-access-error": {
74
- "types": "./src/errors/locals-access-error.ts",
75
- "default": "./src/errors/locals-access-error.ts"
76
- },
77
69
  "./eco": {
78
70
  "browser": "./src/eco/eco.browser.ts",
79
71
  "types": "./src/eco/eco.ts",
80
72
  "default": "./src/eco/eco.ts"
81
73
  },
82
- "./route-renderer/template-serialization": {
83
- "types": "./src/route-renderer/orchestration/template-serialization.ts",
84
- "default": "./src/route-renderer/orchestration/template-serialization.ts"
85
- },
86
74
  "./declarations": {
87
75
  "types": "./src/declarations.d.ts"
88
76
  },
@@ -101,45 +89,18 @@
101
89
  "types": "./src/adapters/bun/index.ts",
102
90
  "default": "./src/adapters/bun/index.ts"
103
91
  },
104
- "./bun/create-app": {
105
- "types": "./src/adapters/bun/create-app.ts",
106
- "default": "./src/adapters/bun/create-app.ts"
107
- },
108
92
  "./hmr/hmr-strategy": {
109
93
  "types": "./src/hmr/hmr-strategy.ts",
110
94
  "default": "./src/hmr/hmr-strategy.ts"
111
95
  },
112
- "./internal-types": {
113
- "types": "./src/types/internal-types.ts"
96
+ "./dev/host-runtime": {
97
+ "types": "./src/dev/host-runtime.ts",
98
+ "default": "./src/dev/host-runtime.ts"
114
99
  },
115
100
  "./services/asset-processing-service": {
116
101
  "types": "./src/services/assets/asset-processing-service/index.ts",
117
102
  "default": "./src/services/assets/asset-processing-service/index.ts"
118
103
  },
119
- "./services/invalidation/development-invalidation.service": {
120
- "types": "./src/services/invalidation/development-invalidation.service.ts",
121
- "default": "./src/services/invalidation/development-invalidation.service.ts"
122
- },
123
- "./services/module-loading/app-server-module-transpiler.service": {
124
- "types": "./src/services/module-loading/app-server-module-transpiler.service.ts",
125
- "default": "./src/services/module-loading/app-server-module-transpiler.service.ts"
126
- },
127
- "./host-module-loader": {
128
- "types": "./src/services/module-loading/host-module-loader-registry.ts",
129
- "default": "./src/services/module-loading/host-module-loader-registry.ts"
130
- },
131
- "./utils/deep-merge": {
132
- "types": "./src/utils/deep-merge.ts",
133
- "default": "./src/utils/deep-merge.ts"
134
- },
135
- "./utils/invariant": {
136
- "types": "./src/utils/invariant.ts",
137
- "default": "./src/utils/invariant.ts"
138
- },
139
- "./utils/parse-cli-args": {
140
- "types": "./src/utils/parse-cli-args.ts",
141
- "default": "./src/utils/parse-cli-args.ts"
142
- },
143
104
  "./plugins/processor": {
144
105
  "types": "./src/plugins/processor.ts",
145
106
  "default": "./src/plugins/processor.ts"
@@ -172,10 +133,6 @@
172
133
  "types": "./src/plugins/foreign-jsx-override-plugin.ts",
173
134
  "default": "./src/plugins/foreign-jsx-override-plugin.ts"
174
135
  },
175
- "./adapters/bun/client-bridge": {
176
- "types": "./src/adapters/bun/client-bridge.ts",
177
- "default": "./src/adapters/bun/client-bridge.ts"
178
- },
179
136
  "./html": {
180
137
  "import": "./src/utils/html.ts",
181
138
  "require": "./src/utils/html.ts"
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { Server } from 'bun';
12
12
  import { DEFAULT_ECOPAGES_HOSTNAME, DEFAULT_ECOPAGES_PORT } from '../../config/constants.ts';
13
+ import { StaticContentServer } from '../../dev/sc-server.ts';
13
14
  import { appLogger } from '../../global/app-logger.ts';
14
15
  import { getBunRuntime } from '../../utils/runtime.ts';
15
16
  import type { ApiHandlerContext, RouteGroupBuilder } from '../../types/public-types.ts';
@@ -47,6 +48,41 @@ export class BunEcopagesApp<WebSocketData = undefined> extends SharedApplication
47
48
  serverAdapter: BunServerAdapterResult | undefined;
48
49
  private server: Server<WebSocketData> | null = null;
49
50
 
51
+ private async startStaticPreviewServer(port: number, hostname: string): Promise<void> {
52
+ await new Promise((resolve) => setTimeout(resolve, 100));
53
+
54
+ for (let attempt = 0; attempt < 20; attempt += 1) {
55
+ try {
56
+ const previewServer = StaticContentServer.createServer({
57
+ appConfig: this.appConfig,
58
+ options: { port },
59
+ });
60
+
61
+ if (previewServer.server?.port) {
62
+ appLogger.info(`Preview running at http://${hostname}:${previewServer.server.port}`);
63
+ return;
64
+ }
65
+
66
+ break;
67
+ } catch (error) {
68
+ const errorMessage = error instanceof Error ? error.message : String(error);
69
+ const errorCode =
70
+ typeof error === 'object' && error !== null && 'code' in error
71
+ ? String((error as { code?: unknown }).code)
72
+ : undefined;
73
+ const isPortReleaseRace = errorCode === 'EADDRINUSE' || errorMessage.includes('EADDRINUSE');
74
+
75
+ if (!isPortReleaseRace || attempt === 19) {
76
+ throw error;
77
+ }
78
+
79
+ await new Promise((resolve) => setTimeout(resolve, 100));
80
+ }
81
+ }
82
+
83
+ appLogger.error('Failed to start preview server');
84
+ }
85
+
50
86
  public async fetch(request: Request): Promise<Response> {
51
87
  if (!this.serverAdapter) {
52
88
  this.serverAdapter = await this.initializeServerAdapter();
@@ -137,13 +173,22 @@ export class BunEcopagesApp<WebSocketData = undefined> extends SharedApplication
137
173
 
138
174
  const enableHmr = dev || (!preview && !build);
139
175
  const serverOptions = this.serverAdapter.getServerOptions({ enableHmr });
176
+ const configuredHostname = String(serverOptions.hostname ?? DEFAULT_ECOPAGES_HOSTNAME);
177
+ const configuredPort = Number(serverOptions.port ?? DEFAULT_ECOPAGES_PORT);
178
+ const runtimeServerOptions =
179
+ (preview || build) && requiresFetchRuntime
180
+ ? {
181
+ ...serverOptions,
182
+ port: 0,
183
+ }
184
+ : serverOptions;
140
185
 
141
186
  const bun = getBunRuntime();
142
187
  if (!bun) {
143
188
  throw new Error('Bun runtime is required for the Bun adapter');
144
189
  }
145
190
 
146
- const bunServer = bun.serve(serverOptions as Bun.Serve.Options<WebSocketData>);
191
+ const bunServer = bun.serve(runtimeServerOptions as Bun.Serve.Options<WebSocketData>);
147
192
  this.server = bunServer as Server<WebSocketData>;
148
193
 
149
194
  await this.serverAdapter.completeInitialization(this.server).catch((error: Error) => {
@@ -157,8 +202,15 @@ export class BunEcopagesApp<WebSocketData = undefined> extends SharedApplication
157
202
 
158
203
  if (build || preview) {
159
204
  appLogger.debugTime('Building static pages');
160
- await this.serverAdapter.buildStatic({ preview });
205
+ await this.serverAdapter.buildStatic({ preview: false });
161
206
  this.server.stop(true);
207
+
208
+ if (preview) {
209
+ const previewHostname = configuredHostname;
210
+ const previewPort = configuredPort;
211
+ await this.startStaticPreviewServer(previewPort, previewHostname);
212
+ }
213
+
162
214
  appLogger.debugTimeEnd('Building static pages');
163
215
 
164
216
  if (build) {
@@ -227,13 +227,11 @@ test('HmrManager stop clears retained registration state', async () => {
227
227
  fs.writeFileSync(outputPath, 'export default 1;', 'utf8');
228
228
  });
229
229
 
230
- manager.registerSpecifierMap({ react: '/assets/vendors/react.js' });
231
230
  await manager.registerEntrypoint(entrypointPath);
232
231
 
233
232
  manager.stop();
234
233
 
235
234
  assert.equal(manager.getWatchedFiles().size, 0);
236
- assert.equal(manager.getSpecifierMap().size, 0);
237
235
  });
238
236
 
239
237
  test('HmrManager keeps internal browser and server-module outputs out of distDir', async () => {
@@ -20,7 +20,6 @@ import {
20
20
  NoopEntrypointDependencyGraph,
21
21
  setAppEntrypointDependencyGraph,
22
22
  } from '../../services/runtime-state/entrypoint-dependency-graph.service.ts';
23
- import { getAppRuntimeSpecifierRegistry } from '../../services/runtime-state/runtime-specifier-registry.service.ts';
24
23
  import type { ServerModuleTranspiler } from '../../services/module-loading/server-module-transpiler.service.ts';
25
24
  import { resolveInternalExecutionDir, resolveInternalWorkDir } from '../../utils/resolve-work-dir.ts';
26
25
 
@@ -60,7 +59,6 @@ export class HmrManager implements IHmrManager {
60
59
  private readonly entrypointRegistrar: HmrEntrypointRegistrar;
61
60
  private readonly browserBundleService: BrowserBundleService;
62
61
  private readonly entrypointDependencyGraph: ReturnType<typeof getAppEntrypointDependencyGraph>;
63
- private readonly runtimeSpecifierRegistry: ReturnType<typeof getAppRuntimeSpecifierRegistry>;
64
62
  private readonly serverModuleTranspiler: ServerModuleTranspiler;
65
63
  private wsHandler!: {
66
64
  open: (ws: BunSocket) => void;
@@ -86,7 +84,6 @@ export class HmrManager implements IHmrManager {
86
84
  ? existingEntrypointDependencyGraph
87
85
  : new NoopEntrypointDependencyGraph();
88
86
  setAppEntrypointDependencyGraph(this.appConfig, this.entrypointDependencyGraph);
89
- this.runtimeSpecifierRegistry = getAppRuntimeSpecifierRegistry(this.appConfig);
90
87
  this.serverModuleTranspiler = getAppServerModuleTranspiler(this.appConfig);
91
88
  this.cleanDistDir();
92
89
  this.initializeStrategies();
@@ -132,7 +129,6 @@ export class HmrManager implements IHmrManager {
132
129
  private initializeStrategies(): void {
133
130
  const jsContext = {
134
131
  getWatchedFiles: () => this.watchedFiles,
135
- getSpecifierMap: () => this.runtimeSpecifierRegistry.getAll(),
136
132
  getDistDir: () => this.distDir,
137
133
  getPlugins: () => this.plugins,
138
134
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
@@ -168,19 +164,6 @@ export class HmrManager implements IHmrManager {
168
164
  return this.enabled;
169
165
  }
170
166
 
171
- /**
172
- * Registers runtime bare-specifier mappings exposed by integrations.
173
- *
174
- * @remarks
175
- * These mappings are consumed by framework-owned HMR strategies that preserve
176
- * shared runtime imports in browser bundles. The registry stays generic so
177
- * these mappings can later support broader import-map-style runtime features
178
- * without moving integration semantics into core.
179
- */
180
- public registerSpecifierMap(map: Record<string, string>): void {
181
- this.runtimeSpecifierRegistry.register(map);
182
- }
183
-
184
167
  public getWebSocketHandler(): BunSocketHandler {
185
168
  const open = (ws: BunSocket) => {
186
169
  this.bridge.subscribe(ws);
@@ -272,10 +255,6 @@ export class HmrManager implements IHmrManager {
272
255
  return this.watchedFiles;
273
256
  }
274
257
 
275
- public getSpecifierMap(): Map<string, string> {
276
- return this.runtimeSpecifierRegistry.getAll();
277
- }
278
-
279
258
  public getDistDir(): string {
280
259
  return this.distDir;
281
260
  }
@@ -287,7 +266,6 @@ export class HmrManager implements IHmrManager {
287
266
  public getDefaultContext(): DefaultHmrContext {
288
267
  return {
289
268
  getWatchedFiles: () => this.watchedFiles,
290
- getSpecifierMap: () => this.runtimeSpecifierRegistry.getAll(),
291
269
  getDistDir: () => this.distDir,
292
270
  getPlugins: () => this.plugins,
293
271
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
@@ -390,7 +368,7 @@ export class HmrManager implements IHmrManager {
390
368
  * @remarks
391
369
  * Emitted `_hmr` files remain on disk because parallel app processes may share
392
370
  * the same dist directory. The in-memory indexes are cleared so stale
393
- * entrypoints and specifier maps cannot leak through a reused manager object.
371
+ * entrypoints cannot leak through a reused manager object.
394
372
  */
395
373
  public stop() {
396
374
  this.entrypointRegistrations.clear();
@@ -399,7 +377,6 @@ export class HmrManager implements IHmrManager {
399
377
  }
400
378
  this.watchers.clear();
401
379
  this.watchedFiles.clear();
402
- this.runtimeSpecifierRegistry.clear();
403
380
  this.entrypointDependencyGraph.reset();
404
381
  this.plugins = [];
405
382
  }
@@ -11,6 +11,7 @@ import { SharedServerAdapter } from '../shared/server-adapter.ts';
11
11
  import type { ServerAdapterResult } from '../abstract/server-adapter.ts';
12
12
  import { ApiResponseBuilder } from '../shared/api-response.ts';
13
13
  import { installSharedRuntimeBuildExecutor } from '../shared/runtime-bootstrap.ts';
14
+ import { StaticContentServer } from '../../dev/sc-server.ts';
14
15
 
15
16
  import { ServerRouteHandler, type ServerRouteHandlerParams } from '../shared/server-route-handler.ts';
16
17
  import { ServerStaticBuilder, type ServerStaticBuilderParams } from '../shared/server-static-builder.ts';
@@ -352,11 +353,36 @@ export class BunServerAdapter extends SharedServerAdapter<BunServerAdapterParams
352
353
  });
353
354
  }
354
355
 
355
- await this.staticBuilder.build(options, {
356
- router: this.router,
357
- routeRendererFactory: this.routeRendererFactory,
358
- staticRoutes: this.staticRoutes,
356
+ const buildRuntimeOrigin = this.serverInstance
357
+ ? `http://${this.serverInstance.hostname || DEFAULT_ECOPAGES_HOSTNAME}:${this.serverInstance.port || DEFAULT_ECOPAGES_PORT}`
358
+ : undefined;
359
+
360
+ await this.staticBuilder.build(
361
+ { ...options, preview: false, baseUrl: buildRuntimeOrigin },
362
+ {
363
+ router: this.router,
364
+ routeRendererFactory: this.routeRendererFactory,
365
+ staticRoutes: this.staticRoutes,
366
+ },
367
+ );
368
+
369
+ if (!options?.preview) {
370
+ return;
371
+ }
372
+
373
+ const previewHostname = this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME;
374
+ const previewPort = Number(this.serveOptions.port || DEFAULT_ECOPAGES_PORT);
375
+ const previewServer = StaticContentServer.createServer({
376
+ appConfig: this.appConfig,
377
+ options: { port: previewPort },
359
378
  });
379
+
380
+ if (previewServer.server?.port) {
381
+ appLogger.info(`Preview running at http://${previewHostname}:${previewServer.server.port}`);
382
+ return;
383
+ }
384
+
385
+ appLogger.error('Failed to start preview server');
360
386
  }
361
387
 
362
388
  /**
@@ -283,13 +283,11 @@ test('NodeHmrManager stop clears retained registration state', async () => {
283
283
  fs.writeFileSync(outputPath, 'export default 1;', 'utf8');
284
284
  });
285
285
 
286
- manager.registerSpecifierMap({ react: '/assets/vendors/react.js' });
287
286
  await manager.registerEntrypoint(entrypointPath);
288
287
 
289
288
  manager.stop();
290
289
 
291
290
  assert.equal(manager.getWatchedFiles().size, 0);
292
- assert.equal(manager.getSpecifierMap().size, 0);
293
291
  assert.equal(config.runtime?.entrypointDependencyGraph?.getDependencyEntrypoints(entrypointPath).size, 0);
294
292
  });
295
293
 
@@ -19,7 +19,6 @@ import {
19
19
  setAppEntrypointDependencyGraph,
20
20
  type EntrypointDependencyGraph,
21
21
  } from '../../services/runtime-state/entrypoint-dependency-graph.service.ts';
22
- import { getAppRuntimeSpecifierRegistry } from '../../services/runtime-state/runtime-specifier-registry.service.ts';
23
22
  import type { ServerModuleTranspiler } from '../../services/module-loading/server-module-transpiler.service.ts';
24
23
  import { resolveInternalExecutionDir, resolveInternalWorkDir } from '../../utils/resolve-work-dir.ts';
25
24
 
@@ -59,7 +58,6 @@ export class NodeHmrManager implements IHmrManager {
59
58
  private readonly entrypointRegistrar: HmrEntrypointRegistrar;
60
59
  private readonly browserBundleService: BrowserBundleService;
61
60
  private readonly entrypointDependencyGraph: EntrypointDependencyGraph;
62
- private readonly runtimeSpecifierRegistry: ReturnType<typeof getAppRuntimeSpecifierRegistry>;
63
61
  private readonly serverModuleTranspiler: ServerModuleTranspiler;
64
62
 
65
63
  constructor({ appConfig, bridge }: NodeHmrManagerParams) {
@@ -81,7 +79,6 @@ export class NodeHmrManager implements IHmrManager {
81
79
  ? existingEntrypointDependencyGraph
82
80
  : new InMemoryEntrypointDependencyGraph();
83
81
  setAppEntrypointDependencyGraph(this.appConfig, this.entrypointDependencyGraph);
84
- this.runtimeSpecifierRegistry = getAppRuntimeSpecifierRegistry(this.appConfig);
85
82
  this.serverModuleTranspiler = getAppServerModuleTranspiler(this.appConfig);
86
83
  this.cleanDistDir();
87
84
  this.initializeStrategies();
@@ -123,7 +120,6 @@ export class NodeHmrManager implements IHmrManager {
123
120
  private initializeStrategies(): void {
124
121
  const jsContext = {
125
122
  getWatchedFiles: () => this.watchedFiles,
126
- getSpecifierMap: () => this.runtimeSpecifierRegistry.getAll(),
127
123
  getDistDir: () => this.distDir,
128
124
  getPlugins: () => this.plugins,
129
125
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
@@ -154,19 +150,6 @@ export class NodeHmrManager implements IHmrManager {
154
150
  return this.enabled;
155
151
  }
156
152
 
157
- /**
158
- * Registers runtime bare-specifier mappings exposed by integrations.
159
- *
160
- * @remarks
161
- * These mappings are consumed by framework-owned HMR strategies such as the
162
- * React integration strategy when they rewrite browser bundles. The registry
163
- * stays generic so the same mappings can support broader import-map-style
164
- * runtime features later without moving integration semantics into core.
165
- */
166
- public registerSpecifierMap(map: Record<string, string>): void {
167
- this.runtimeSpecifierRegistry.register(map);
168
- }
169
-
170
153
  public async buildRuntime(): Promise<void> {
171
154
  const runtimeSource = path.resolve(import.meta.dirname, '../../hmr/client/hmr-runtime.ts');
172
155
 
@@ -239,10 +222,6 @@ export class NodeHmrManager implements IHmrManager {
239
222
  return this.watchedFiles;
240
223
  }
241
224
 
242
- public getSpecifierMap(): Map<string, string> {
243
- return this.runtimeSpecifierRegistry.getAll();
244
- }
245
-
246
225
  public getDistDir(): string {
247
226
  return this.distDir;
248
227
  }
@@ -254,7 +233,6 @@ export class NodeHmrManager implements IHmrManager {
254
233
  public getDefaultContext(): DefaultHmrContext {
255
234
  return {
256
235
  getWatchedFiles: () => this.watchedFiles,
257
- getSpecifierMap: () => this.runtimeSpecifierRegistry.getAll(),
258
236
  getDistDir: () => this.distDir,
259
237
  getPlugins: () => this.plugins,
260
238
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
@@ -361,8 +339,8 @@ export class NodeHmrManager implements IHmrManager {
361
339
  * @remarks
362
340
  * The manager intentionally does not remove emitted `_hmr` files from disk
363
341
  * because multiple app processes may share the same dist directory during test
364
- * runs. It does clear in-memory indexes so old entrypoints, dependencies, and
365
- * specifier maps cannot leak across a reused manager instance.
342
+ * runs. It does clear in-memory indexes so old entrypoints and dependencies
343
+ * cannot leak across a reused manager instance.
366
344
  */
367
345
  public stop() {
368
346
  this.entrypointRegistrations.clear();
@@ -371,7 +349,6 @@ export class NodeHmrManager implements IHmrManager {
371
349
  }
372
350
  this.watchers.clear();
373
351
  this.watchedFiles.clear();
374
- this.runtimeSpecifierRegistry.clear();
375
352
  this.entrypointDependencyGraph.reset();
376
353
  this.plugins = [];
377
354
  }
@@ -0,0 +1,58 @@
1
+ import type { EcoPagesAppConfig } from '../../types/internal-types.ts';
2
+ import type { EcoFunctionComponent, EcoPageComponent } from '../../types/public-types.ts';
3
+ import type { ExplicitViewRenderer, ExplicitViewRendererResolver } from '../../route-renderer/route-renderer.ts';
4
+
5
+ type ExplicitStaticRenderPreparationResult = {
6
+ renderer: ExplicitViewRenderer;
7
+ props: Record<string, unknown>;
8
+ view: EcoFunctionComponent<Record<string, unknown>, any>;
9
+ };
10
+
11
+ function getViewIntegrationName(view: {
12
+ config?: { integration?: string; __eco?: { integration?: string } };
13
+ }): string | undefined {
14
+ return view.config?.integration ?? view.config?.__eco?.integration;
15
+ }
16
+
17
+ /**
18
+ * Resolves the renderer and static props needed to render one explicit static
19
+ * view at runtime or during static generation.
20
+ */
21
+ export async function prepareExplicitStaticRender(input: {
22
+ routePath: string;
23
+ view: EcoPageComponent<any>;
24
+ params: Record<string, string | string[]>;
25
+ appConfig: EcoPagesAppConfig;
26
+ runtimeOrigin: string;
27
+ routeRendererFactory: ExplicitViewRendererResolver;
28
+ errors: {
29
+ missingIntegration(routePath: string): string;
30
+ noRendererForIntegration(integrationName: string): string;
31
+ };
32
+ }): Promise<ExplicitStaticRenderPreparationResult> {
33
+ const integrationName = getViewIntegrationName(input.view);
34
+ if (!integrationName) {
35
+ throw new Error(input.errors.missingIntegration(input.routePath));
36
+ }
37
+
38
+ const renderer = input.routeRendererFactory.getExplicitViewRenderer(integrationName);
39
+ if (!renderer) {
40
+ throw new Error(input.errors.noRendererForIntegration(integrationName));
41
+ }
42
+
43
+ const props = input.view.staticProps
44
+ ? (
45
+ await input.view.staticProps({
46
+ pathname: { params: input.params },
47
+ appConfig: input.appConfig,
48
+ runtimeOrigin: input.runtimeOrigin,
49
+ })
50
+ ).props
51
+ : {};
52
+
53
+ return {
54
+ renderer,
55
+ props,
56
+ view: input.view as EcoFunctionComponent<Record<string, unknown>, any>,
57
+ };
58
+ }
@@ -246,7 +246,7 @@ describe('ExplicitStaticRouteMatcher', () => {
246
246
  const matcher = new ExplicitStaticRouteMatcher({
247
247
  appConfig: { baseUrl: 'http://localhost:3000' } as any,
248
248
  routeRendererFactory: {
249
- getRendererByIntegration: vi.fn(() => ({
249
+ getExplicitViewRenderer: vi.fn(() => ({
250
250
  renderToResponse,
251
251
  })),
252
252
  } as any,
@@ -269,7 +269,7 @@ describe('ExplicitStaticRouteMatcher', () => {
269
269
  const matcher = new ExplicitStaticRouteMatcher({
270
270
  appConfig: { baseUrl: 'http://localhost:3000' } as any,
271
271
  routeRendererFactory: {
272
- getRendererByIntegration: vi.fn(() => null),
272
+ getExplicitViewRenderer: vi.fn(() => null),
273
273
  } as any,
274
274
  staticRoutes: [createMockRoute('/about', viewWithoutIntegration)],
275
275
  });
@@ -282,7 +282,7 @@ describe('ExplicitStaticRouteMatcher', () => {
282
282
  test('should throw error when renderer is not found', async () => {
283
283
  const view = createMockView('nonexistent-integration');
284
284
  const RendererFactory = {
285
- getRendererByIntegration: vi.fn(() => null),
285
+ getExplicitViewRenderer: vi.fn(() => null),
286
286
  };
287
287
 
288
288
  const matcher = new ExplicitStaticRouteMatcher({
@@ -303,7 +303,7 @@ describe('ExplicitStaticRouteMatcher', () => {
303
303
  const mockResponse = new Response('<html>Test</html>');
304
304
  const RenderToResponse = vi.fn(() => mockResponse);
305
305
  const RendererFactory = {
306
- getRendererByIntegration: vi.fn(() => ({
306
+ getExplicitViewRenderer: vi.fn(() => ({
307
307
  renderToResponse: RenderToResponse,
308
308
  })),
309
309
  };
@@ -331,7 +331,7 @@ describe('ExplicitStaticRouteMatcher', () => {
331
331
  const mockResponse = new Response('<html>Test</html>');
332
332
  const RenderToResponse = vi.fn(() => mockResponse);
333
333
  const RendererFactory = {
334
- getRendererByIntegration: vi.fn(() => ({
334
+ getExplicitViewRenderer: vi.fn(() => ({
335
335
  renderToResponse: RenderToResponse,
336
336
  })),
337
337
  };
@@ -357,7 +357,7 @@ describe('ExplicitStaticRouteMatcher', () => {
357
357
 
358
358
  const mockResponse = new Response('<html>Test</html>');
359
359
  const RendererFactory = {
360
- getRendererByIntegration: vi.fn(() => ({
360
+ getExplicitViewRenderer: vi.fn(() => ({
361
361
  renderToResponse: vi.fn(() => mockResponse),
362
362
  })),
363
363
  };