@ecopages/core 0.2.0-beta.2 → 0.2.0-beta.21

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 (273) hide show
  1. package/README.md +1 -1
  2. package/package.json +163 -3
  3. package/src/adapters/abstract/application-adapter.d.ts +104 -5
  4. package/src/adapters/abstract/application-adapter.js +125 -1
  5. package/src/adapters/abstract/server-adapter.d.ts +6 -2
  6. package/src/adapters/abstract/ws-pattern-matcher.d.ts +60 -0
  7. package/src/adapters/abstract/ws-pattern-matcher.js +61 -0
  8. package/src/adapters/bun/client-bridge.d.ts +1 -0
  9. package/src/adapters/bun/client-bridge.js +3 -0
  10. package/src/adapters/bun/create-app.d.ts +6 -4
  11. package/src/adapters/bun/create-app.js +65 -43
  12. package/src/adapters/bun/server-adapter.d.ts +90 -22
  13. package/src/adapters/bun/server-adapter.js +266 -84
  14. package/src/adapters/bun/static-preview-host.js +25 -20
  15. package/src/adapters/create-app.d.ts +13 -11
  16. package/src/adapters/create-app.js +2 -51
  17. package/src/adapters/index.d.ts +1 -1
  18. package/src/adapters/index.js +1 -2
  19. package/src/adapters/node/create-app.d.ts +5 -1
  20. package/src/adapters/node/create-app.js +51 -12
  21. package/src/adapters/node/node-client-bridge.d.ts +1 -0
  22. package/src/adapters/node/node-client-bridge.js +3 -0
  23. package/src/adapters/node/server-adapter-dependencies.js +4 -0
  24. package/src/adapters/node/server-adapter.d.ts +62 -20
  25. package/src/adapters/node/server-adapter.js +160 -131
  26. package/src/adapters/node/static-content-server.d.ts +1 -0
  27. package/src/adapters/node/static-content-server.js +10 -2
  28. package/src/adapters/node/static-preview-host.js +27 -12
  29. package/src/adapters/shared/bun-user-websocket-lifecycle.d.ts +22 -0
  30. package/src/adapters/shared/bun-user-websocket-lifecycle.js +96 -0
  31. package/src/adapters/shared/copy-runtime-public-dir.d.ts +6 -0
  32. package/src/adapters/shared/copy-runtime-public-dir.js +24 -0
  33. package/src/adapters/shared/fs-server-response-matcher.d.ts +2 -0
  34. package/src/adapters/shared/fs-server-response-matcher.js +40 -18
  35. package/src/adapters/shared/hmr-entrypoint-registrar.d.ts +21 -30
  36. package/src/adapters/shared/hmr-entrypoint-registrar.js +49 -54
  37. package/src/adapters/shared/hmr-html-response.d.ts +1 -1
  38. package/src/adapters/shared/hmr-html-response.js +4 -1
  39. package/src/adapters/shared/node-http-websocket-upgrades.d.ts +21 -0
  40. package/src/adapters/shared/node-http-websocket-upgrades.js +100 -0
  41. package/src/adapters/shared/port-manager.d.ts +116 -0
  42. package/src/adapters/shared/port-manager.js +196 -0
  43. package/src/adapters/shared/runtime-app-bootstrap.d.ts +2 -1
  44. package/src/adapters/shared/runtime-app-bootstrap.js +3 -6
  45. package/src/adapters/shared/runtime-server-lifecycle.d.ts +46 -0
  46. package/src/adapters/shared/runtime-server-lifecycle.js +50 -0
  47. package/src/adapters/shared/server-adapter.d.ts +2 -1
  48. package/src/adapters/shared/server-adapter.js +13 -6
  49. package/src/adapters/shared/server-route-handler.d.ts +4 -1
  50. package/src/adapters/shared/server-route-handler.js +5 -2
  51. package/src/adapters/shared/server-static-builder.d.ts +23 -32
  52. package/src/adapters/shared/server-static-builder.js +100 -73
  53. package/src/adapters/shared/shared-hmr-manager.d.ts +45 -11
  54. package/src/adapters/shared/shared-hmr-manager.js +204 -58
  55. package/src/adapters/shared/static-preview-host.d.ts +5 -0
  56. package/src/adapters/shared/websocket-lifecycle.d.ts +24 -0
  57. package/src/adapters/shared/websocket-lifecycle.js +55 -0
  58. package/src/build/README.md +77 -14
  59. package/src/build/app-build-manifest-runtime.d.ts +9 -0
  60. package/src/build/app-build-manifest-runtime.js +87 -0
  61. package/src/build/build-adapter.d.ts +17 -319
  62. package/src/build/build-adapter.js +28 -94
  63. package/src/build/build-contracts.d.ts +97 -0
  64. package/src/build/build-contracts.js +0 -0
  65. package/src/build/build-input-fingerprint.d.ts +28 -0
  66. package/src/build/build-input-fingerprint.js +31 -0
  67. package/src/build/build-profile-options.d.ts +7 -0
  68. package/src/build/build-profile-options.js +37 -0
  69. package/src/build/build-runtime.d.ts +24 -0
  70. package/src/build/build-runtime.js +65 -0
  71. package/src/build/deduping-build-executor.d.ts +28 -0
  72. package/src/build/deduping-build-executor.js +56 -0
  73. package/src/build/jsx-ownership-plugins.d.ts +24 -0
  74. package/src/build/jsx-ownership-plugins.js +41 -0
  75. package/src/build/pages-unified-graph-build.d.ts +31 -0
  76. package/src/build/pages-unified-graph-build.js +206 -0
  77. package/src/build/parallel-build-executor.d.ts +21 -0
  78. package/src/build/parallel-build-executor.js +52 -0
  79. package/src/build/production-build-cache.d.ts +20 -0
  80. package/src/build/production-build-cache.js +63 -0
  81. package/src/build/rolldown-adapter-helpers.d.ts +3 -6
  82. package/src/build/rolldown-adapter-helpers.js +71 -6
  83. package/src/build/rolldown-build-adapter.d.ts +2 -6
  84. package/src/build/rolldown-build-adapter.js +3 -3
  85. package/src/build/rolldown-build-invocation-metrics.d.ts +12 -0
  86. package/src/build/rolldown-build-invocation-metrics.js +43 -0
  87. package/src/build/rolldown-plugin-bridge.d.ts +15 -1
  88. package/src/build/rolldown-plugin-bridge.js +20 -5
  89. package/src/build/rolldown-source-transform-pass.d.ts +15 -0
  90. package/src/build/rolldown-source-transform-pass.js +58 -0
  91. package/src/build/runtime-build-executor.d.ts +8 -15
  92. package/src/build/runtime-build-executor.js +12 -15
  93. package/src/build/runtime-build-output-normalizer.d.ts +7 -0
  94. package/src/build/runtime-build-output-normalizer.js +94 -2
  95. package/src/build/server-entry-build-cache.d.ts +44 -0
  96. package/src/build/server-entry-build-cache.js +161 -0
  97. package/src/client/navigation-scripts/classification.d.ts +16 -0
  98. package/src/client/navigation-scripts/classification.js +41 -0
  99. package/src/client/navigation-scripts/index.d.ts +3 -0
  100. package/src/client/navigation-scripts/index.js +23 -0
  101. package/src/client/navigation-scripts/navigation-scripts.test.browser.js +65 -0
  102. package/src/client/navigation-scripts/registry.d.ts +11 -0
  103. package/src/client/navigation-scripts/registry.js +12 -0
  104. package/src/client/navigation-scripts/rerun-queue.d.ts +37 -0
  105. package/src/client/navigation-scripts/rerun-queue.js +73 -0
  106. package/src/client/scroll.d.ts +17 -0
  107. package/src/client/scroll.js +25 -0
  108. package/src/client/scroll.test.browser.js +37 -0
  109. package/src/client/view-transitions.d.ts +16 -0
  110. package/src/client/view-transitions.js +63 -0
  111. package/src/config/config-builder.d.ts +12 -7
  112. package/src/config/config-builder.js +12 -9
  113. package/src/dev/client-bridge-registry.d.ts +5 -0
  114. package/src/dev/client-bridge-registry.js +15 -0
  115. package/src/dev/dev-client-ownership.d.ts +7 -0
  116. package/src/dev/dev-client-ownership.js +6 -0
  117. package/src/dev/hmr-manager-registry.d.ts +8 -0
  118. package/src/dev/hmr-manager-registry.js +15 -0
  119. package/src/dev/host-runtime.d.ts +3 -0
  120. package/src/dev/host-runtime.js +7 -0
  121. package/src/dev/runtime-server-started-message.d.ts +2 -0
  122. package/src/dev/runtime-server-started-message.js +6 -0
  123. package/src/eco/README.md +11 -0
  124. package/src/eco/eco-declared-component.d.ts +14 -0
  125. package/src/eco/eco-declared-component.js +16 -0
  126. package/src/eco/eco.browser.js +5 -6
  127. package/src/eco/eco.js +5 -6
  128. package/src/eco/eco.types.d.ts +2 -2
  129. package/src/eco/page-layout-normalization.d.ts +21 -0
  130. package/src/eco/page-layout-normalization.js +59 -0
  131. package/src/errors/index.d.ts +1 -0
  132. package/src/errors/index.js +7 -1
  133. package/src/errors/undeclared-component-dependency-error.d.ts +11 -0
  134. package/src/errors/undeclared-component-dependency-error.js +17 -0
  135. package/src/hmr/client/hmr-runtime.js +5 -2
  136. package/src/hmr/hmr-entrypoint-output.d.ts +43 -0
  137. package/src/hmr/hmr-entrypoint-output.js +53 -0
  138. package/src/hmr/hmr-strategy.d.ts +15 -0
  139. package/src/hmr/hmr-strategy.js +19 -0
  140. package/src/hmr/strategies/js-hmr-strategy.d.ts +3 -5
  141. package/src/hmr/strategies/js-hmr-strategy.js +67 -19
  142. package/src/hmr/strategies/server-rendered-template-hmr-strategy.d.ts +17 -0
  143. package/src/hmr/strategies/server-rendered-template-hmr-strategy.js +29 -0
  144. package/src/integrations/ghtml/ghtml-renderer.d.ts +2 -6
  145. package/src/integrations/ghtml/ghtml-renderer.js +2 -51
  146. package/src/integrations/ghtml/ghtml.plugin.d.ts +3 -16
  147. package/src/integrations/ghtml/ghtml.plugin.js +7 -14
  148. package/src/plugins/define-integration.d.ts +26 -0
  149. package/src/plugins/define-integration.js +19 -0
  150. package/src/plugins/eco-component-meta-plugin.js +5 -8
  151. package/src/plugins/foreign-jsx-override-plugin.js +4 -6
  152. package/src/plugins/integration-plugin.d.ts +19 -9
  153. package/src/plugins/integration-plugin.js +7 -4
  154. package/src/plugins/jsx-import-source.utils.d.ts +8 -0
  155. package/src/plugins/jsx-import-source.utils.js +26 -0
  156. package/src/plugins/processor.d.ts +19 -4
  157. package/src/plugins/processor.js +22 -4
  158. package/src/plugins/source-transform.d.ts +31 -0
  159. package/src/plugins/source-transform.js +23 -1
  160. package/src/route-renderer/GRAPH.md +8 -8
  161. package/src/route-renderer/README.md +30 -28
  162. package/src/route-renderer/orchestration/component-graph-collectors.d.ts +10 -0
  163. package/src/route-renderer/orchestration/component-graph-collectors.js +143 -0
  164. package/src/route-renderer/orchestration/component-graph.d.ts +36 -0
  165. package/src/route-renderer/orchestration/component-graph.js +98 -0
  166. package/src/route-renderer/orchestration/component-render-context.d.ts +3 -10
  167. package/src/route-renderer/orchestration/component-render-context.js +3 -1
  168. package/src/route-renderer/orchestration/declared-ownership-graph.d.ts +1 -18
  169. package/src/route-renderer/orchestration/declared-ownership-graph.js +5 -31
  170. package/src/route-renderer/orchestration/document-shell-render.service.d.ts +90 -0
  171. package/src/route-renderer/orchestration/document-shell-render.service.js +120 -0
  172. package/src/route-renderer/orchestration/foreign-subtree-execution.service.d.ts +59 -29
  173. package/src/route-renderer/orchestration/foreign-subtree-execution.service.js +164 -35
  174. package/src/route-renderer/orchestration/global-injector-assets.service.d.ts +7 -0
  175. package/src/route-renderer/orchestration/global-injector-assets.service.js +32 -0
  176. package/src/route-renderer/orchestration/integration-renderer.d.ts +18 -54
  177. package/src/route-renderer/orchestration/integration-renderer.js +113 -195
  178. package/src/route-renderer/orchestration/integration-route-render-adapter.d.ts +38 -0
  179. package/src/route-renderer/orchestration/integration-route-render-adapter.js +27 -0
  180. package/src/route-renderer/orchestration/layout-shell-props.service.d.ts +20 -0
  181. package/src/route-renderer/orchestration/layout-shell-props.service.js +31 -0
  182. package/src/route-renderer/orchestration/ownership-validation.service.d.ts +5 -0
  183. package/src/route-renderer/orchestration/ownership-validation.service.js +29 -1
  184. package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.d.ts +9 -0
  185. package/src/route-renderer/orchestration/page-browser-graph-contribution.loader.js +7 -0
  186. package/src/route-renderer/orchestration/page-browser-graph.service.d.ts +25 -0
  187. package/src/route-renderer/orchestration/page-browser-graph.service.js +174 -0
  188. package/src/route-renderer/orchestration/render-output.utils.d.ts +2 -0
  189. package/src/route-renderer/orchestration/render-output.utils.js +4 -0
  190. package/src/route-renderer/orchestration/route-html-finalization.service.d.ts +17 -0
  191. package/src/route-renderer/orchestration/route-html-finalization.service.js +26 -0
  192. package/src/route-renderer/orchestration/route-prepared-options.builder.d.ts +26 -0
  193. package/src/route-renderer/orchestration/route-prepared-options.builder.js +54 -0
  194. package/src/route-renderer/orchestration/route-prepared-options.utils.d.ts +4 -0
  195. package/src/route-renderer/orchestration/route-prepared-options.utils.js +33 -0
  196. package/src/route-renderer/orchestration/route-render-orchestrator.d.ts +8 -39
  197. package/src/route-renderer/orchestration/route-render-orchestrator.js +53 -461
  198. package/src/route-renderer/orchestration/string-markup-renderer.d.ts +14 -0
  199. package/src/route-renderer/orchestration/string-markup-renderer.js +55 -0
  200. package/src/route-renderer/page-loading/component-dependency-collection.js +5 -2
  201. package/src/router/client/link-intent.d.ts +11 -21
  202. package/src/router/client/link-intent.js +10 -0
  203. package/src/router/client/link-intent.test.browser.js +12 -1
  204. package/src/router/client/link-navigation-policy.d.ts +45 -0
  205. package/src/router/client/link-navigation-policy.js +101 -0
  206. package/src/router/client/link-navigation-policy.test.browser.d.ts +1 -0
  207. package/src/router/client/link-navigation-policy.test.browser.js +92 -0
  208. package/src/router/client/navigation-coordinator.d.ts +1 -1
  209. package/src/router/client/navigation-coordinator.js +12 -2
  210. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.d.ts +1 -0
  211. package/src/services/assets/asset-processing-service/browser-runtime-asset.factory.js +1 -0
  212. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.d.ts +4 -2
  213. package/src/services/assets/asset-processing-service/browser-runtime-entry.factory.js +27 -7
  214. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.d.ts +0 -1
  215. package/src/services/assets/asset-processing-service/processors/script/file-script.processor.js +18 -15
  216. package/src/services/assets/asset-processing-service/processors/script/node-module-script.processor.js +17 -3
  217. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.d.ts +0 -3
  218. package/src/services/assets/asset-processing-service/processors/stylesheet/content-stylesheet.processor.js +2 -31
  219. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.d.ts +0 -3
  220. package/src/services/assets/asset-processing-service/processors/stylesheet/file-stylesheet.processor.js +2 -30
  221. package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.d.ts +2 -0
  222. package/src/services/assets/asset-processing-service/processors/stylesheet/stylesheet-processor-pipeline.js +43 -0
  223. package/src/services/assets/browser-bundle.service.d.ts +3 -1
  224. package/src/services/assets/browser-bundle.service.js +17 -5
  225. package/src/services/html/html-transformer.service.d.ts +1 -1
  226. package/src/services/html/html-transformer.service.js +4 -4
  227. package/src/services/invalidation/development-invalidation.service.d.ts +20 -2
  228. package/src/services/invalidation/development-invalidation.service.js +56 -2
  229. package/src/services/module-loading/README.md +49 -0
  230. package/src/services/module-loading/app-server-module-transpiler.service.js +5 -17
  231. package/src/services/module-loading/page-module-import.service.d.ts +5 -1
  232. package/src/services/module-loading/page-module-import.service.js +96 -45
  233. package/src/services/module-loading/route-module-build-cache-registry.d.ts +24 -0
  234. package/src/services/module-loading/route-module-build-cache-registry.js +37 -0
  235. package/src/services/module-loading/route-module-build-cache.d.ts +4 -0
  236. package/src/services/module-loading/route-module-build-cache.js +38 -0
  237. package/src/services/module-loading/route-module-build-cache.store.d.ts +57 -0
  238. package/src/services/module-loading/route-module-build-cache.store.js +207 -0
  239. package/src/services/module-loading/route-module-build-manifest.d.ts +61 -0
  240. package/src/services/module-loading/route-module-build-manifest.js +116 -0
  241. package/src/services/module-loading/route-module-dependency-hasher.d.ts +81 -0
  242. package/src/services/module-loading/route-module-dependency-hasher.js +122 -0
  243. package/src/services/module-loading/server-module-transpiler.service.js +1 -1
  244. package/src/static-site-generator/README.md +33 -0
  245. package/src/static-site-generator/static-build-invalidation.d.ts +10 -0
  246. package/src/static-site-generator/static-build-invalidation.js +59 -0
  247. package/src/static-site-generator/static-export-context.d.ts +22 -0
  248. package/src/static-site-generator/static-export-context.js +0 -0
  249. package/src/static-site-generator/static-site-generator.d.ts +17 -20
  250. package/src/static-site-generator/static-site-generator.js +188 -85
  251. package/src/types/internal-types.d.ts +16 -9
  252. package/src/types/public-types.d.ts +144 -40
  253. package/src/utils/parse-cli-args.d.ts +2 -0
  254. package/src/utils/parse-cli-args.js +6 -0
  255. package/src/utils/server-utils.module.d.ts +3 -2
  256. package/src/utils/server-utils.module.js +5 -6
  257. package/src/utils/static-file-extensions.d.ts +12 -0
  258. package/src/utils/static-file-extensions.js +48 -0
  259. package/src/watchers/project-watcher-ignore.d.ts +11 -0
  260. package/src/watchers/project-watcher-ignore.js +16 -0
  261. package/src/watchers/project-watcher.d.ts +26 -3
  262. package/src/watchers/project-watcher.js +132 -30
  263. package/src/watchers/project-watcher.test-helpers.js +5 -5
  264. package/src/build/rolldown-dev-build-adapter.d.ts +0 -82
  265. package/src/build/rolldown-dev-build-adapter.js +0 -166
  266. package/src/hmr/hmr.postcss.test.e2e.js +0 -31
  267. package/src/hmr/hmr.test.e2e.js +0 -43
  268. package/src/route-renderer/orchestration/ownership-planning.service.d.ts +0 -24
  269. package/src/route-renderer/orchestration/ownership-planning.service.js +0 -63
  270. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.d.ts +0 -91
  271. package/src/route-renderer/orchestration/queued-foreign-subtree-resolution.service.js +0 -170
  272. /package/src/{hmr/hmr.postcss.test.e2e.d.ts → client/navigation-scripts/navigation-scripts.test.browser.d.ts} +0 -0
  273. /package/src/{hmr/hmr.test.e2e.d.ts → client/scroll.test.browser.d.ts} +0 -0
@@ -2,11 +2,18 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { RESOLVED_ASSETS_DIR } from "../../config/constants.js";
5
- import { getAppBuildExecutor } from "../../build/build-adapter.js";
5
+ import {
6
+ removeStaleHmrEntrypointOutput,
7
+ isRegisteredScriptEntrypoint,
8
+ isBrowserOnlyRegisteredScriptEntrypoint
9
+ } from "../../hmr/hmr-entrypoint-output.js";
10
+ import { requireBuildRuntime } from "../../build/build-runtime.js";
6
11
  import { fileSystem } from "@ecopages/file-system";
7
12
  import { HmrStrategyType } from "../../hmr/hmr-strategy.js";
8
13
  import { DefaultHmrStrategy } from "../../hmr/strategies/default-hmr-strategy.js";
9
14
  import { JsHmrStrategy } from "../../hmr/strategies/js-hmr-strategy.js";
15
+ import { ServerRenderedTemplateHmrStrategy } from "../../hmr/strategies/server-rendered-template-hmr-strategy.js";
16
+ import { DevelopmentInvalidationService } from "../../services/invalidation/development-invalidation.service.js";
10
17
  import { appLogger } from "../../global/app-logger.js";
11
18
  import { HmrEntrypointRegistrar } from "./hmr-entrypoint-registrar.js";
12
19
  import { BrowserBundleService } from "../../services/assets/browser-bundle.service.js";
@@ -20,29 +27,27 @@ class SharedHmrManager {
20
27
  appConfig;
21
28
  bridge;
22
29
  watchers = /* @__PURE__ */ new Map();
23
- watchedFiles = /* @__PURE__ */ new Map();
24
- entrypointRegistrations = /* @__PURE__ */ new Map();
25
30
  distDir;
26
- plugins = [];
27
- enabled = true;
31
+ enabled = false;
28
32
  strategies = [];
29
33
  entrypointRegistrar;
30
34
  browserBundleService;
35
+ invalidationService;
31
36
  entrypointDependencyGraph;
32
37
  serverModuleTranspiler;
33
- constructor({ appConfig, bridge, registrationTimeoutMs = 4e3 }) {
38
+ runtimeBuildPromise = null;
39
+ runtimeReady = false;
40
+ constructor({ appConfig, bridge }) {
34
41
  this.appConfig = appConfig;
35
42
  this.bridge = bridge;
36
43
  this.distDir = path.join(resolveInternalWorkDir(this.appConfig), RESOLVED_ASSETS_DIR, "_hmr");
37
44
  this.entrypointRegistrar = new HmrEntrypointRegistrar({
38
45
  srcDir: this.appConfig.absolutePaths.srcDir,
39
46
  distDir: this.distDir,
40
- entrypointRegistrations: this.entrypointRegistrations,
41
- watchedFiles: this.watchedFiles,
42
- clearFailedRegistration: (entrypointPath) => this.clearFailedEntrypointRegistration(entrypointPath),
43
- registrationTimeoutMs
47
+ clearFailedRegistration: (entrypointPath) => this.clearFailedEntrypointRegistration(entrypointPath)
44
48
  });
45
49
  this.browserBundleService = new BrowserBundleService(appConfig);
50
+ this.invalidationService = new DevelopmentInvalidationService(appConfig);
46
51
  this.entrypointDependencyGraph = this.createEntrypointDependencyGraph(
47
52
  getAppEntrypointDependencyGraph(appConfig)
48
53
  );
@@ -75,9 +80,8 @@ class SharedHmrManager {
75
80
  }
76
81
  initializeStrategies() {
77
82
  const jsContext = {
78
- getWatchedFiles: () => this.watchedFiles,
83
+ getWatchedFiles: () => this.entrypointRegistrar.getWatchedFiles(),
79
84
  getDistDir: () => this.distDir,
80
- getPlugins: () => this.plugins,
81
85
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
82
86
  getPagesDir: () => this.appConfig.absolutePaths.pagesDir,
83
87
  getLayoutsDir: () => this.appConfig.absolutePaths.layoutsDir,
@@ -86,41 +90,81 @@ class SharedHmrManager {
86
90
  getEntrypointDependencyGraph: () => this.entrypointDependencyGraph,
87
91
  shouldProcessEntrypoint: (entrypointPath) => this.shouldJsStrategyProcessEntrypoint(entrypointPath)
88
92
  };
89
- this.strategies = [new JsHmrStrategy(jsContext), new DefaultHmrStrategy()];
93
+ const invalidationService = new DevelopmentInvalidationService(this.appConfig);
94
+ this.strategies = [
95
+ new JsHmrStrategy(jsContext),
96
+ new ServerRenderedTemplateHmrStrategy(invalidationService),
97
+ new DefaultHmrStrategy()
98
+ ];
90
99
  }
91
100
  registerStrategy(strategy) {
92
101
  this.strategies.push(strategy);
93
102
  }
94
- setPlugins(plugins) {
95
- this.plugins = [...plugins];
96
- }
97
103
  setEnabled(enabled) {
98
104
  this.enabled = enabled;
99
105
  }
100
106
  isEnabled() {
101
107
  return this.enabled;
102
108
  }
109
+ isRuntimeReady() {
110
+ return this.runtimeReady && fileSystem.exists(this.getRuntimePath());
111
+ }
112
+ /**
113
+ * Builds the browser HMR runtime once per manager session and reuses the in-flight build for concurrent callers.
114
+ */
115
+ async ensureRuntimeReady() {
116
+ if (this.runtimeReady) {
117
+ return true;
118
+ }
119
+ if (this.runtimeBuildPromise) {
120
+ return this.runtimeBuildPromise;
121
+ }
122
+ this.runtimeBuildPromise = this.buildRuntimeInternal();
123
+ try {
124
+ const ready = await this.runtimeBuildPromise;
125
+ this.runtimeReady = ready;
126
+ return ready;
127
+ } finally {
128
+ this.runtimeBuildPromise = null;
129
+ }
130
+ }
103
131
  async buildRuntime() {
132
+ await this.ensureRuntimeReady();
133
+ }
134
+ getRuntimePath() {
135
+ return path.join(this.distDir, "_hmr_runtime.js");
136
+ }
137
+ async buildRuntimeInternal() {
104
138
  const runtimeSource = fileURLToPath(import.meta.resolve("@ecopages/core/hmr/client/hmr-runtime"));
139
+ const runtimePath = this.getRuntimePath();
140
+ removeStaleHmrEntrypointOutput(runtimePath, "HMR");
105
141
  try {
106
142
  const result = await this.browserBundleService.bundle({
107
143
  profile: "hmr-runtime",
108
144
  entrypoints: [runtimeSource],
109
145
  outdir: this.distDir,
110
146
  naming: "_hmr_runtime.js",
111
- minify: false,
112
- plugins: this.plugins
147
+ minify: false
113
148
  });
114
149
  if (!result.success) {
115
150
  this.onRuntimeBundleFailure(result.logs);
151
+ return false;
152
+ }
153
+ const emittedRuntime = result.outputs.find(
154
+ (output) => path.resolve(output.path) === path.resolve(runtimePath)
155
+ );
156
+ if (!emittedRuntime || !fileSystem.exists(runtimePath)) {
157
+ this.onRuntimeBundleFailure(
158
+ new Error(`[HMR] Runtime bundle missing expected output at ${runtimePath}`)
159
+ );
160
+ return false;
116
161
  }
162
+ return true;
117
163
  } catch (error) {
118
164
  this.onRuntimeBundleFailure(error);
165
+ return false;
119
166
  }
120
167
  }
121
- getRuntimePath() {
122
- return path.join(this.distDir, "_hmr_runtime.js");
123
- }
124
168
  broadcast(event) {
125
169
  appLogger.debug(
126
170
  `[HMR] Broadcasting ${event.type} event, path=${event.path || "all"}, subscribers=${this.bridge.subscriberCount}`
@@ -128,20 +172,16 @@ class SharedHmrManager {
128
172
  this.bridge.broadcast(event);
129
173
  }
130
174
  async handleFileChange(filePath, options = {}) {
175
+ const resolvedFilePath = path.resolve(filePath);
131
176
  if (this.shouldSkipMissingFileChange(filePath) && !fileSystem.exists(filePath)) {
132
177
  appLogger.debug(`[${this.constructor.name}] Skipping missing file change: ${filePath}`);
133
178
  this.clearFailedEntrypointRegistration(filePath);
134
179
  return;
135
180
  }
136
- const sorted = [...this.strategies].sort((a, b) => b.priority - a.priority);
137
- const strategy = sorted.find((candidate) => {
138
- try {
139
- return candidate.matches(filePath);
140
- } catch (error) {
141
- appLogger.error(error);
142
- return false;
143
- }
144
- });
181
+ if (isRegisteredScriptEntrypoint(this.entrypointRegistrar.getRegistered(), resolvedFilePath)) {
182
+ await this.prepareRegisteredScriptChange(resolvedFilePath);
183
+ }
184
+ const strategy = this.selectChangeStrategy(filePath);
145
185
  if (!strategy) {
146
186
  appLogger.warn(`[HMR] No strategy found for ${filePath}`);
147
187
  return;
@@ -155,27 +195,123 @@ class SharedHmrManager {
155
195
  }
156
196
  }
157
197
  }
198
+ getStrategiesByPriority() {
199
+ return [...this.strategies].sort((left, right) => right.priority - left.priority);
200
+ }
201
+ selectChangeStrategy(filePath) {
202
+ return this.getStrategiesByPriority().find((candidate) => {
203
+ try {
204
+ return candidate.matches(filePath);
205
+ } catch (error) {
206
+ appLogger.error(error);
207
+ return false;
208
+ }
209
+ });
210
+ }
211
+ selectEntrypointEmitter(entrypointPath) {
212
+ return this.getStrategiesByPriority().find((candidate) => {
213
+ if (candidate.type !== HmrStrategyType.INTEGRATION) {
214
+ return false;
215
+ }
216
+ try {
217
+ return candidate.canEmitEntrypoint(entrypointPath);
218
+ } catch (error) {
219
+ appLogger.error(error);
220
+ return false;
221
+ }
222
+ });
223
+ }
224
+ createUnownedEntrypointError(entrypointPath) {
225
+ return new Error(
226
+ `[HMR] No integration owns entrypoint ${entrypointPath}. Page entrypoints must be emitted by their owning integration.`
227
+ );
228
+ }
229
+ /**
230
+ * Materializes one integration-owned page entrypoint during cold registration.
231
+ *
232
+ * @remarks
233
+ * Registration uses {@link HmrStrategy.canEmitEntrypoint} / {@link HmrStrategy.emitEntrypoint},
234
+ * not {@link HmrStrategy.matches} / {@link HmrStrategy.process}. File-change handling stays
235
+ * on `handleFileChange()`.
236
+ */
237
+ async emitIntegrationEntrypoint(entrypointPath, outputPath) {
238
+ const emitter = this.selectEntrypointEmitter(entrypointPath);
239
+ if (!emitter) {
240
+ throw this.createUnownedEntrypointError(entrypointPath);
241
+ }
242
+ appLogger.debug(`[${this.constructor.name}] Selected entrypoint emitter: ${emitter.constructor.name}`);
243
+ await emitter.emitEntrypoint(entrypointPath, outputPath);
244
+ }
245
+ /**
246
+ * Runs server invalidation and integration hooks before rebuilding a registered script entrypoint.
247
+ *
248
+ * @remarks
249
+ * Browser-only `*.script.ts` entrypoints skip server module invalidation and bypass-cache
250
+ * import; only integration change handlers and the downstream strategy rebuild run.
251
+ */
252
+ async prepareRegisteredScriptChange(filePath) {
253
+ const isBrowserOnlyRegisteredScript = isBrowserOnlyRegisteredScriptEntrypoint(filePath);
254
+ if (!isBrowserOnlyRegisteredScript) {
255
+ this.invalidationService.invalidateServerModules([filePath]);
256
+ }
257
+ const handlers = this.appConfig.runtime?.registeredScriptEntrypointChangeHandlers ?? [];
258
+ for (const handler of handlers) {
259
+ try {
260
+ await handler(filePath);
261
+ } catch (error) {
262
+ const message = error instanceof Error ? error.message : `Failed to handle registered script entrypoint change for ${filePath}: ${String(error)}`;
263
+ this.broadcast({ type: "error", message });
264
+ throw error;
265
+ }
266
+ }
267
+ if (isBrowserOnlyRegisteredScript) {
268
+ return;
269
+ }
270
+ const appModuleLoader = this.appConfig.runtime?.appModuleLoader;
271
+ if (!appModuleLoader) {
272
+ return;
273
+ }
274
+ await appModuleLoader.importModule({
275
+ filePath,
276
+ rootDir: this.appConfig.rootDir,
277
+ outdir: path.join(resolveInternalExecutionDir(this.appConfig), ".server-modules"),
278
+ externalPackages: true,
279
+ bypassCache: true
280
+ });
281
+ }
158
282
  getOutputUrl(entrypointPath) {
159
- return this.watchedFiles.get(entrypointPath);
283
+ return this.entrypointRegistrar.getRegistered().get(path.resolve(entrypointPath))?.outputUrl;
284
+ }
285
+ /**
286
+ * Returns the emitted HMR script output when the entrypoint is already registered
287
+ * and its browser bundle exists on disk.
288
+ *
289
+ * @remarks
290
+ * Disk artifacts alone are not enough: a fresh dev session must still register
291
+ * the entrypoint so file watchers can rebuild it on change.
292
+ */
293
+ getResolvedScriptOutput(entrypointPath) {
294
+ const normalizedEntrypoint = path.resolve(entrypointPath);
295
+ const registered = this.entrypointRegistrar.getRegistered().get(normalizedEntrypoint);
296
+ if (!registered || !fileSystem.exists(registered.outputPath)) {
297
+ return void 0;
298
+ }
299
+ return registered;
160
300
  }
161
301
  getWatchedFiles() {
162
- return this.watchedFiles;
302
+ return this.entrypointRegistrar.getWatchedFiles();
163
303
  }
164
304
  getDistDir() {
165
305
  return this.distDir;
166
306
  }
167
- getPlugins() {
168
- return this.plugins;
169
- }
170
307
  getDefaultContext() {
171
308
  return {
172
- getWatchedFiles: () => this.watchedFiles,
309
+ getWatchedFiles: () => this.entrypointRegistrar.getWatchedFiles(),
173
310
  getDistDir: () => this.distDir,
174
- getPlugins: () => this.plugins,
175
311
  getSrcDir: () => this.appConfig.absolutePaths.srcDir,
176
312
  getLayoutsDir: () => this.appConfig.absolutePaths.layoutsDir,
177
313
  getPagesDir: () => this.appConfig.absolutePaths.pagesDir,
178
- getBuildExecutor: () => getAppBuildExecutor(this.appConfig),
314
+ getBuildExecutor: () => requireBuildRuntime(this.appConfig).getProfile("browser-hmr"),
179
315
  getBrowserBundleService: () => this.browserBundleService,
180
316
  getEntrypointDependencyGraph: () => this.entrypointDependencyGraph,
181
317
  importServerModule: async (filePath) => await this.serverModuleTranspiler.importModule({
@@ -185,27 +321,34 @@ class SharedHmrManager {
185
321
  })
186
322
  };
187
323
  }
324
+ stop() {
325
+ this.runtimeReady = false;
326
+ this.entrypointRegistrar.clearAll();
327
+ for (const watcher of this.watchers.values()) {
328
+ watcher.close();
329
+ }
330
+ this.watchers.clear();
331
+ this.entrypointDependencyGraph.reset();
332
+ }
188
333
  clearFailedEntrypointRegistration(entrypointPath) {
189
- this.watchedFiles.delete(entrypointPath);
334
+ this.entrypointRegistrar.clearRegistration(entrypointPath);
190
335
  this.entrypointDependencyGraph.clearEntrypointDependencies(entrypointPath);
191
336
  }
192
337
  async registerEntrypoint(entrypointPath) {
193
- return await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
194
- emit: async (normalizedEntrypoint) => await this.emitStrictEntrypoint(normalizedEntrypoint),
338
+ const resolved = await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
339
+ emit: async (normalizedEntrypoint, outputPath) => await this.emitIntegrationEntrypoint(normalizedEntrypoint, outputPath),
195
340
  getMissingOutputError: (normalizedEntrypoint, outputPath) => new Error(
196
341
  `[HMR] Integration failed to emit entrypoint ${normalizedEntrypoint} to ${outputPath}. Page entrypoints must be produced by their owning integration.`
197
342
  )
198
343
  });
344
+ return resolved.outputUrl;
199
345
  }
200
346
  async registerScriptEntrypoint(entrypointPath) {
201
347
  return await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
202
348
  emit: async (normalizedEntrypoint, outputPath) => await this.emitScriptEntrypoint(normalizedEntrypoint, outputPath),
203
- getMissingOutputError: (normalizedEntrypoint) => new Error(`[HMR] Failed to register script entrypoint: ${normalizedEntrypoint}`)
349
+ getMissingOutputError: (normalizedEntrypoint, outputPath) => new Error(`[HMR] Failed to register script entrypoint: ${normalizedEntrypoint} to ${outputPath}`)
204
350
  });
205
351
  }
206
- async emitStrictEntrypoint(entrypointPath) {
207
- await this.handleFileChange(entrypointPath, { broadcast: false });
208
- }
209
352
  async emitScriptEntrypoint(entrypointPath, outputPath) {
210
353
  const naming = path.relative(this.distDir, outputPath).split(path.sep).join("/");
211
354
  const buildResult = await this.browserBundleService.bundle({
@@ -213,27 +356,30 @@ class SharedHmrManager {
213
356
  entrypoints: [entrypointPath],
214
357
  outdir: this.distDir,
215
358
  naming,
216
- minify: false,
217
- plugins: this.plugins
359
+ minify: false
218
360
  });
219
361
  if (!buildResult.success) {
220
- appLogger.error(`[HMR] Generic script entrypoint build failed for ${entrypointPath}:`, buildResult.logs);
221
- return;
362
+ throw new Error(
363
+ `[HMR] Generic script entrypoint build failed for ${entrypointPath}: ${JSON.stringify(buildResult.logs)}`
364
+ );
365
+ }
366
+ if (!fileSystem.exists(outputPath) && buildResult.outputs.length > 0) {
367
+ const resolvedOutputPath = path.resolve(outputPath);
368
+ const emittedOutput = buildResult.outputs.find((output) => path.resolve(output.path) === resolvedOutputPath)?.path ?? buildResult.outputs.find((output) => path.basename(output.path) === path.basename(outputPath))?.path;
369
+ if (emittedOutput && fileSystem.exists(emittedOutput)) {
370
+ fileSystem.ensureDir(path.dirname(outputPath));
371
+ if (path.resolve(emittedOutput) !== resolvedOutputPath) {
372
+ fileSystem.copyFile(emittedOutput, outputPath);
373
+ }
374
+ }
222
375
  }
223
376
  const entrypointDependencies = buildResult.dependencyGraph?.entrypoints?.[entrypointPath];
224
377
  if (entrypointDependencies) {
225
378
  this.entrypointDependencyGraph.setEntrypointDependencies(entrypointPath, entrypointDependencies);
226
379
  }
227
380
  }
228
- stop() {
229
- this.entrypointRegistrations.clear();
230
- for (const watcher of this.watchers.values()) {
231
- watcher.close();
232
- }
233
- this.watchers.clear();
234
- this.watchedFiles.clear();
235
- this.entrypointDependencyGraph.reset();
236
- this.plugins = [];
381
+ [Symbol.dispose]() {
382
+ this.stop();
237
383
  }
238
384
  }
239
385
  export {
@@ -7,4 +7,9 @@ export interface StaticPreviewHostStartOptions {
7
7
  appConfig: EcoPagesAppConfig;
8
8
  hostname: string;
9
9
  port: number;
10
+ /**
11
+ * When true, bind to the next available port if the preferred one is busy.
12
+ * Should remain false when the port was explicitly configured.
13
+ */
14
+ allowPortFallback?: boolean;
10
15
  }
@@ -0,0 +1,24 @@
1
+ import type { EcopagesSocket, WebSocketCloseInfo } from '../../types/public-types.js';
2
+ /**
3
+ * @remarks
4
+ * Bun and Node expose different raw socket types but share the same
5
+ * {@link OutgoingWebSocketMessage} contract. Adapters supply only `send`/`close`;
6
+ * {@link createEcopagesSocket} owns message normalization and stream forwarding.
7
+ */
8
+ export interface WebSocketSendTransport {
9
+ send(data: string | Uint8Array): void;
10
+ close(code?: number, reason?: string): void;
11
+ }
12
+ /**
13
+ * @remarks
14
+ * Bun and Node adapters previously duplicated outgoing-message dispatch and
15
+ * stream forwarding; this is the shared implementation both transports delegate to.
16
+ */
17
+ export declare function createEcopagesSocket<TContext, TParams extends Record<string, string>>(transport: WebSocketSendTransport, meta: {
18
+ kind: string;
19
+ params: TParams;
20
+ search: Record<string, string>;
21
+ context: TContext;
22
+ }): EcopagesSocket<TContext, TParams>;
23
+ export declare function toWebSocketCloseInfo(code: number, reason: string): WebSocketCloseInfo;
24
+ export declare function invokeWebSocketHandlerHook(kind: string, hookName: string, result: void | Promise<void>): void;
@@ -0,0 +1,55 @@
1
+ import { appLogger } from "../../global/app-logger.js";
2
+ function sendOutgoingMessage(transport, message) {
3
+ if (typeof message === "string") {
4
+ transport.send(message);
5
+ } else if (message instanceof Blob) {
6
+ void message.arrayBuffer().then((buffer) => transport.send(new Uint8Array(buffer)));
7
+ } else if (message instanceof ArrayBuffer) {
8
+ transport.send(new Uint8Array(message));
9
+ } else if (ArrayBuffer.isView(message)) {
10
+ transport.send(new Uint8Array(message.buffer, message.byteOffset, message.byteLength));
11
+ } else {
12
+ transport.send(message);
13
+ }
14
+ }
15
+ function createEcopagesSocket(transport, meta) {
16
+ return {
17
+ kind: meta.kind,
18
+ params: meta.params,
19
+ search: meta.search,
20
+ context: meta.context,
21
+ send: (message) => sendOutgoingMessage(transport, message),
22
+ sendStream: async (stream) => {
23
+ const reader = stream.getReader();
24
+ try {
25
+ while (true) {
26
+ const { value, done } = await reader.read();
27
+ if (done) break;
28
+ if (value) transport.send(value);
29
+ }
30
+ } finally {
31
+ reader.releaseLock();
32
+ }
33
+ },
34
+ close: (code, reason) => transport.close(code, reason)
35
+ };
36
+ }
37
+ function toWebSocketCloseInfo(code, reason) {
38
+ return {
39
+ code,
40
+ reason,
41
+ wasClean: code === 1e3 || code === 1001
42
+ };
43
+ }
44
+ function invokeWebSocketHandlerHook(kind, hookName, result) {
45
+ if (result instanceof Promise) {
46
+ result.catch((error) => {
47
+ appLogger.error(`[WS:${kind}] ${hookName} failed:`, error);
48
+ });
49
+ }
50
+ }
51
+ export {
52
+ createEcopagesSocket,
53
+ invokeWebSocketHandlerHook,
54
+ toWebSocketCloseInfo
55
+ };
@@ -4,13 +4,15 @@ The build layer is the bundler contract for Ecopages. One bundled adapter is the
4
4
 
5
5
  ## Mental Model
6
6
 
7
- Three concentric shapes, plus a serializer and a plugin injector:
7
+ Three concentric shapes, plus profile executors and a plugin injector:
8
8
 
9
9
  | Shape | Lives in | Purpose |
10
10
  | -------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
11
  | `BuildAdapter` | `build-adapter.ts` | Low-level backend. Two implementations: the bundled adapter (the real bundler) and `ViteHostBuildAdapter` (a host-owned boundary marker that throws on direct use). |
12
- | `BuildExecutor` | `build-adapter.ts` | Narrower runtime facade. Only `build` is exposed. Stored on `appConfig.runtime.buildExecutor`. |
13
- | `SerializedBuildExecutor` | `serialized-build-executor.ts` | FIFO queue around any `BuildExecutor`. Used by the dev watch pipeline. |
12
+ | `BuildRuntime` | `build-runtime.ts` | Profile-based executor registry. Single entry point on `appConfig.runtime.buildRuntime`. |
13
+ | `BuildExecutor` | `build-contracts.ts` | Narrower runtime facade. Only `build` is exposed. Retrieved via `buildRuntime.getProfile(...)`. |
14
+ | `SerializedBuildExecutor` | `serialized-build-executor.ts` | FIFO queue around any `BuildExecutor`. Used for server-entry single-flight ordering. |
15
+ | `ParallelBuildExecutor` | `parallel-build-executor.ts` | Concurrency-limited wrapper for independent route-module and HMR browser builds. |
14
16
  | `withBuildExecutorPlugins` | `build-adapter.ts` | Merges app-owned plugins into every `build` call. The single point of plugin injection. |
15
17
 
16
18
  Plus one translation bridge:
@@ -20,29 +22,31 @@ Plus one translation bridge:
20
22
  ## Files
21
23
 
22
24
  - `build-adapter.ts`: types, factories, app-owned helpers, `withBuildExecutorPlugins`.
25
+ - `build-runtime.ts`: profile-based executor installation (`server-entry`, `route-module`, `browser-hmr`).
23
26
  - `build-types.ts`: the `EcoBuildPlugin` contract used by integrations and processors.
24
27
  - `rolldown-build-adapter.ts`: the production `BuildAdapter`. Wraps the bundler and exposes a normalized `BuildResult` (outputs, dependency graph, logs).
25
28
  - `rolldown-plugin-bridge.ts`: `EcoBuildPlugin[]` → bundler-plugin translation.
26
29
  - `serialized-build-executor.ts`: FIFO queue primitive.
27
- - `runtime-build-executor.ts`: dev-watch entrypoint that wraps the app-owned adapter in a `SerializedBuildExecutor + withBuildExecutorPlugins` chain.
30
+ - `runtime-build-executor.ts`: server-adapter entrypoint that installs `BuildRuntime` via `installAppRuntimeBuildExecutor()`.
31
+ - `server-entry-build-cache.ts`: production server-entry bundle cache (`.eco/.server-entry/.build-cache.json` + `dist/.server/manifest.json`).
28
32
  - `*.test.ts`: regression coverage.
29
33
 
30
34
  ## Default Flow
31
35
 
32
- `ConfigBuilder.build()` creates one app-owned adapter, manifest, executor, dev graph, and runtime registry. The executor it stores is the raw `BuildAdapter`; the plugin-wrap step lives in the runtime path.
33
-
34
- When a server adapter initializes, it calls `installAppRuntimeBuildExecutor(appConfig)`. That function reads the existing executor (or falls back to the app-owned adapter) and stores a fresh wrapper:
36
+ `ConfigBuilder.build()` creates one app-owned adapter and manifest. When a server adapter initializes, it calls `installAppRuntimeBuildExecutor(appConfig)`, which installs `BuildRuntime`:
35
37
 
36
38
  ```
37
- BuildExecutor
38
- └─ SerializedBuildExecutor // FIFO queue
39
- └─ withBuildExecutorPlugins // injects app plugins
40
- └─ BuildAdapter (bundled adapter or Vite-host)
39
+ BuildRuntime.getProfile(...)
40
+ ├─ server-entry → SerializedBuildExecutor withBuildExecutorPlugins → RolldownBuildAdapter
41
+ ├─ route-module → ParallelBuildExecutor → withBuildExecutorPlugins RolldownBuildAdapter
42
+ └─ browser-hmr → ParallelBuildExecutor → withBuildExecutorPlugins RolldownBuildAdapter
41
43
  ```
42
44
 
43
- Every dev-watch caller reads `appConfig.runtime.buildExecutor` and gets the merged plugin set without further ceremony.
45
+ All profiles use one-shot Rolldown in both dev and production. Route-module and browser-HMR builds run in parallel; server-entry stays serialized single-flight.
46
+
47
+ Runtime code should call `requireBuildRuntime(appConfig).getProfile('route-module' | 'browser-hmr' | 'server-entry')` instead of reading legacy executor slots.
44
48
 
45
- The exported `defaultBuildAdapter` and the top-level `build()` / `getTranspileOptions()` helpers are non-app-aware escape hatches. New runtime code should prefer `getAppBuildAdapter()`, `getAppBuildExecutor()`, and `getAppTranspileOptions()`.
49
+ The exported `defaultBuildAdapter` and the top-level `build()` / `getTranspileOptions()` helpers are non-app-aware escape hatches. New runtime code should prefer `getAppBuildAdapter()`, `requireBuildRuntime()`, and `getAppTranspileOptions()`.
46
50
 
47
51
  ## Vite-Host Boundary
48
52
 
@@ -81,11 +85,70 @@ Plugin ordering: the bundler's `resolveId` and `load` are "first" hooks. The bri
81
85
 
82
86
  These fields are kept in the type so existing call-sites compile. The proper fix is a more focused `BuildOptions` schema in a follow-up.
83
87
 
88
+ ## Dev / watch path
89
+
90
+ `installBuildRuntime` (via `installAppRuntimeBuildExecutor`) installs profile-based executors:
91
+
92
+ | Profile | Backend | Concurrency |
93
+ | -------------- | ----------------- | -------------------- |
94
+ | `server-entry` | Rolldown one-shot | Serialized |
95
+ | `route-module` | Rolldown one-shot | Parallel |
96
+ | `browser-hmr` | Rolldown one-shot | Parallel (limit ≤ 3) |
97
+
98
+ `hmr-entrypoint` rebuilds use the `browser-hmr` profile. Other browser builds (`browser-script`, `hmr-runtime`) use `route-module`.
99
+
100
+ `ProjectWatcher` deduplicates watch roots, ignores `.eco/` and `dist/`, and coalesces duplicate chokidar events within 150ms.
101
+
102
+ ## Metrics
103
+
104
+ Set `ECOPAGES_ROLLDOWN_BUILD_METRICS=1` to log Rolldown invocation counts during dev and production builds. Compare kitchen-sink dev navigation and `static-build-bench` before/after runtime changes.
105
+
106
+ ## Production build caches
107
+
108
+ Two persisted cache layers accelerate production builds. Both use `.build-cache.json` manifests keyed by dependency hashes and a build-inputs fingerprint.
109
+
110
+ | Cache | On-disk location | Module |
111
+ | -------------------------------------- | --------------------------------------------------- | ----------------------------------- |
112
+ | Server-entry bundle | `.eco/.server-entry/.build-cache.json` | `server-entry-build-cache.ts` |
113
+ | Route-module transpile + static render | `<server-outdir>/.server-modules/.build-cache.json` | `route-module-build-cache.store.ts` |
114
+
115
+ `getInstalledServerEntryBuildExecutor()` returns the `server-entry` profile from `BuildRuntime`. `clearProductionBuildCaches()` wipes both manifest trees, resets in-memory route-module state, and clears `buildRuntime`.
116
+
117
+ The route-module registry (`route-module-build-cache-registry.ts`) shares one `RouteModuleBuildCache` per `(app, outdir)` pair. Legacy `.server-route-modules` outdirs are still read for migration but new writes go to `.server-modules`.
118
+
119
+ ## Unified pages graph
120
+
121
+ Production static exports compile all template pages in one Rolldown invocation when `shouldBuildPagesUnifiedGraph()` is true (default in production; opt out with `ECOPAGES_UNIFIED_PAGES_GRAPH=0`).
122
+
123
+ | Artifact | Location |
124
+ | -------------- | ----------------------------------------------------- |
125
+ | Graph manifest | `.eco/.server-pages-graph/.build-cache.json` |
126
+ | Chunk outputs | `.eco/.server-modules/` (shared with per-route cache) |
127
+
128
+ `StaticSiteGenerator` calls `ensurePagesUnifiedGraphBuilt()` before the export loop. `PageModuleImportService` imports prebuilt chunks via `importPagesUnifiedGraphModule()` and falls back to per-page Rolldown on miss.
129
+
130
+ `ECOPAGES_ROLLDOWN_BUILD_METRICS=1` enables `rolldown-build-invocation-metrics.ts` counters used by bench and parity tests.
131
+
132
+ Build-input fingerprinting lives in `build-input-fingerprint.ts` and is shared with server-entry cache, unified pages graph, and static-render invalidation.
133
+
134
+ ## JSX Ownership Plugins
135
+
136
+ Mixed-integration apps need explicit `@jsxImportSource` handling in two different shapes:
137
+
138
+ | Helper | Use when | Behavior |
139
+ | ------------------------------------ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
140
+ | `getJsxOwnershipPlugins()` | App-wide server/transpile builds | Each JSX integration extension gets its **own** `jsxImportSource` so native files keep their owning runtime. |
141
+ | `getHostScopedJsxOwnershipPlugins()` | One integration's **client** bundle graph | Foreign `.tsx`/`.jsx` files compile with the **host** integration JSX runtime (for example React bundling `.kita.tsx`). |
142
+ | `eco-component-meta-plugin` | Component metadata injection | Prepends the **owning** integration pragma only when injecting `__eco` metadata into native files. |
143
+
144
+ `foreign-jsx-override-plugin.ts` is the shared implementation. Prefer the helpers above instead of calling it directly from integrations.
145
+
84
146
  ## Testing Strategy
85
147
 
86
148
  - `rolldown-build-adapter.test.ts` covers the adapter's `build`, `resolve`, `getTranspileOptions`, and dependency-graph extraction end-to-end.
87
149
  - `rolldown-plugin-bridge.test.ts` covers the `EcoBuildPlugin[]` → plugin translation in isolation.
88
150
  - `build-adapter.test.ts` covers the app-owned helpers, the `BuildOwnership` routing, the `withBuildExecutorPlugins` injection, and the default-fallback behaviour.
89
- - `runtime-build-executor.test.ts` covers the dev-watch wrapper: serialization, plugin injection, and the Vite-host rejection path.
151
+ - `build-runtime.test.ts` covers profile executor installation and parallelism.
152
+ - `runtime-build-executor.test.ts` covers the runtime wrapper: plugin injection and the Vite-host rejection path.
90
153
 
91
154
  If you change option mapping or plugin-bridge semantics, update the adapter and bridge tests first. If you change the app-owned helper contracts, update `build-adapter.test.ts` first.
@@ -0,0 +1,9 @@
1
+ import type { EcoBuildPlugin } from './build-types.js';
2
+ import type { AppBuildManifest } from './build-manifest.js';
3
+ import type { EcoPagesAppConfig } from '../types/internal-types.js';
4
+ export declare function collectConfiguredAppBuildManifestContributions(appConfig: EcoPagesAppConfig): Promise<Pick<AppBuildManifest, 'runtimePlugins' | 'browserBundlePlugins' | 'browserRuntimeManifest'>>;
5
+ export declare function setupAppRuntimePlugins(options: {
6
+ appConfig: EcoPagesAppConfig;
7
+ runtimeOrigin: string;
8
+ onRuntimePlugin?: (plugin: EcoBuildPlugin) => void;
9
+ }): Promise<void>;