@ecopages/core 0.2.0-alpha.11 → 0.2.0-alpha.13

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 (57) hide show
  1. package/CHANGELOG.md +7 -10
  2. package/README.md +5 -4
  3. package/package.json +30 -6
  4. package/src/adapters/bun/hmr-manager.js +2 -2
  5. package/src/adapters/node/node-hmr-manager.js +2 -2
  6. package/src/adapters/node/server-adapter.d.ts +2 -2
  7. package/src/adapters/node/server-adapter.js +5 -5
  8. package/src/build/build-adapter.d.ts +8 -6
  9. package/src/build/build-adapter.js +44 -7
  10. package/src/eco/eco.js +18 -118
  11. package/src/eco/eco.utils.d.ts +1 -40
  12. package/src/eco/eco.utils.js +5 -35
  13. package/src/hmr/hmr-strategy.d.ts +8 -6
  14. package/src/integrations/ghtml/ghtml-renderer.d.ts +6 -1
  15. package/src/integrations/ghtml/ghtml-renderer.js +29 -28
  16. package/src/plugins/foreign-jsx-override-plugin.d.ts +31 -0
  17. package/src/plugins/foreign-jsx-override-plugin.js +35 -0
  18. package/src/plugins/integration-plugin.d.ts +90 -29
  19. package/src/plugins/integration-plugin.js +62 -19
  20. package/src/route-renderer/GRAPH.md +54 -84
  21. package/src/route-renderer/README.md +11 -19
  22. package/src/route-renderer/orchestration/component-render-context.d.ts +83 -0
  23. package/src/route-renderer/orchestration/component-render-context.js +147 -0
  24. package/src/route-renderer/orchestration/integration-renderer.d.ts +219 -81
  25. package/src/route-renderer/orchestration/integration-renderer.js +415 -171
  26. package/src/route-renderer/orchestration/queued-boundary-runtime.service.d.ts +93 -0
  27. package/src/route-renderer/orchestration/queued-boundary-runtime.service.js +155 -0
  28. package/src/route-renderer/orchestration/render-execution.service.d.ts +8 -70
  29. package/src/route-renderer/orchestration/render-execution.service.js +28 -113
  30. package/src/route-renderer/orchestration/render-output.utils.d.ts +46 -0
  31. package/src/route-renderer/orchestration/render-output.utils.js +65 -0
  32. package/src/route-renderer/orchestration/render-preparation.service.d.ts +0 -6
  33. package/src/route-renderer/orchestration/render-preparation.service.js +5 -13
  34. package/src/route-renderer/orchestration/template-serialization.d.ts +38 -0
  35. package/src/route-renderer/orchestration/template-serialization.js +45 -0
  36. package/src/route-renderer/page-loading/dependency-resolver.js +10 -8
  37. package/src/router/client/navigation-coordinator.js +2 -2
  38. package/src/router/server/fs-router-scanner.js +6 -1
  39. package/src/services/module-loading/node-bootstrap-plugin.js +14 -1
  40. package/src/services/module-loading/page-module-import.service.js +1 -1
  41. package/src/services/runtime-state/dev-graph.service.d.ts +5 -5
  42. package/src/services/runtime-state/dev-graph.service.js +10 -10
  43. package/src/types/public-types.d.ts +18 -3
  44. package/src/utils/html-escaping.d.ts +7 -0
  45. package/src/utils/html-escaping.js +6 -0
  46. package/src/eco/component-render-context.d.ts +0 -105
  47. package/src/eco/component-render-context.js +0 -94
  48. package/src/route-renderer/component-graph/component-graph-executor.d.ts +0 -33
  49. package/src/route-renderer/component-graph/component-graph-executor.js +0 -30
  50. package/src/route-renderer/component-graph/component-graph.d.ts +0 -53
  51. package/src/route-renderer/component-graph/component-graph.js +0 -94
  52. package/src/route-renderer/component-graph/component-marker.d.ts +0 -52
  53. package/src/route-renderer/component-graph/component-marker.js +0 -46
  54. package/src/route-renderer/component-graph/component-reference.d.ts +0 -11
  55. package/src/route-renderer/component-graph/component-reference.js +0 -39
  56. package/src/route-renderer/component-graph/marker-graph-resolver.d.ts +0 -79
  57. package/src/route-renderer/component-graph/marker-graph-resolver.js +0 -117
package/CHANGELOG.md CHANGED
@@ -8,21 +8,18 @@ All notable changes to `@ecopages/core` are documented here.
8
8
 
9
9
  ### Features
10
10
 
11
- - Added a shared runtime boundary around `createApp()`, host module loading, and explicit build ownership for Bun-native execution, Node fallback support, and Vite or Nitro hosts.
12
- - Added the browser-safe `eco` export, semantic `eco.html()` and `eco.layout()` helpers, and the internal `.eco` work directory.
13
- - Added the public `EcoPagesAppConfig` export for published integration packages.
11
+ - Added app-owned runtime and build ownership around `createApp()`, host module loading, the browser-safe `eco` export, `eco.html()`, `eco.layout()`, and the published `EcoPagesAppConfig` surface.
14
12
 
15
13
  ### Refactoring
16
14
 
17
15
  - Consolidated runtime state around shared module-loading services, app-owned build execution, and the universal `createApp()` boundary.
18
- - Removed the deprecated `@ecopages/core/node*` public escape hatches and the old thin-host bootstrap internals from core.
16
+ - Simplified route-renderer orchestration around renderer-owned boundary runtimes, shared string-boundary queue helpers, and a smaller component render context.
17
+ - Removed marker-era compatibility capture, the shared route-level fallback resolver, deprecated `@ecopages/core/node*` escape hatches, and other dead route-renderer internals.
19
18
 
20
19
  ### Bug Fixes
21
20
 
22
- - Fixed request-time module loading, host-runner interop, and include or layout HMR across Bun, Vite, and Nitro development flows.
23
- - Fixed preview, static-generation, and browser bundle stability for esbuild-backed production paths.
24
- - Fixed deep marker-graph resolution, watch-mode route refreshes, and duplicate-core fallback references in mixed-runtime rendering.
25
- - Fixed npm package output to rewrite workspace dependency versions and publish built JavaScript and declaration artifacts.
21
+ - Fixed mixed-integration page, layout, document, and component rendering to resolve foreign boundaries inside their owning renderer across the built-in integrations.
22
+ - Fixed host/runtime module loading, published build-helper exports, asset output normalization, explicit render flows, and static or preview build stability across Bun, Node, Vite, and Nitro.
26
23
 
27
24
  ### Documentation
28
25
 
@@ -30,12 +27,12 @@ All notable changes to `@ecopages/core` are documented here.
30
27
 
31
28
  ### Tests
32
29
 
33
- - Added regression coverage for Node fallback paths, shared runtime services, and cross-runtime invalidation behavior.
30
+ - Added regression coverage for app-owned runtime services, Node fallback paths, and cross-runtime invalidation behavior.
34
31
 
35
32
  ---
36
33
 
37
34
  ## Migration Notes
38
35
 
39
- - `createApp` is now the recommended entrypoint. Import it from `@ecopages/core`.
36
+ - `createApp` is now the recommended entrypoint. Import it from `@ecopages/core/create-app`.
40
37
  - `defineApiHandler` keeps the same call shape, but the handler context is now explicitly runtime-agnostic.
41
38
  - The old explicit `renderingMode` config option has been removed and full orchestration is always active.
package/README.md CHANGED
@@ -134,7 +134,7 @@ export default config;
134
134
  Start the application using `createApp`. It will choose the Bun adapter when Bun is available and fall back to Node otherwise.
135
135
 
136
136
  ```typescript
137
- import { createApp } from '@ecopages/core';
137
+ import { createApp } from '@ecopages/core/create-app';
138
138
  import appConfig from './eco.config';
139
139
 
140
140
  const app = await createApp({ appConfig });
@@ -198,7 +198,7 @@ Attach the handler in your `app.ts` entry:
198
198
 
199
199
  ```typescript
200
200
  // app.ts
201
- import { createApp } from '@ecopages/core';
201
+ import { createApp } from '@ecopages/core/create-app';
202
202
  import { helloWorld } from './handlers/hello';
203
203
  import appConfig from './eco.config';
204
204
 
@@ -213,10 +213,11 @@ See the [official documentation](https://ecopages.app) for advanced usage, API h
213
213
 
214
214
  ## Import Structure
215
215
 
216
- Use the root package exports for standard authoring in Bun-native flows:
216
+ Use the `create-app` subpath for runtime startup and the root package for standard authoring helpers:
217
217
 
218
218
  ```ts
219
- import { createApp, defineApiHandler, defineGroupHandler, eco } from '@ecopages/core';
219
+ import { createApp } from '@ecopages/core/create-app';
220
+ import { defineApiHandler, defineGroupHandler, eco } from '@ecopages/core';
220
221
  ```
221
222
 
222
223
  > [!NOTE]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-alpha.11",
3
+ "version": "0.2.0-alpha.13",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,14 +17,14 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-alpha.11",
21
- "@ecopages/logger": "latest",
20
+ "@ecopages/file-system": "0.2.0-alpha.13",
21
+ "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.3",
23
23
  "@worker-tools/html-rewriter": "0.1.0-pre.19",
24
- "chokidar": "^4.0.3",
25
- "esbuild": "^0.27.3",
24
+ "chokidar": "^5.0.0",
25
+ "esbuild": "^0.28.0",
26
26
  "ghtml": "^4.0.2",
27
- "oxc-parser": "^0.110.0",
27
+ "oxc-parser": "^0.124.0",
28
28
  "ws": "^8.18.0"
29
29
  },
30
30
  "exports": {
@@ -64,6 +64,10 @@
64
64
  "types": "./src/eco/eco.d.ts",
65
65
  "default": "./src/eco/eco.js"
66
66
  },
67
+ "./route-renderer/template-serialization": {
68
+ "types": "./src/route-renderer/orchestration/template-serialization.d.ts",
69
+ "default": "./src/route-renderer/orchestration/template-serialization.js"
70
+ },
67
71
  "./declarations": {
68
72
  "types": "./src/declarations.d.ts"
69
73
  },
@@ -145,6 +149,14 @@
145
149
  "types": "./src/build/runtime-specifier-alias-plugin.d.ts",
146
150
  "default": "./src/build/runtime-specifier-alias-plugin.js"
147
151
  },
152
+ "./build/runtime-specifier-aliases": {
153
+ "types": "./src/build/runtime-specifier-aliases.d.ts",
154
+ "default": "./src/build/runtime-specifier-aliases.js"
155
+ },
156
+ "./plugins/foreign-jsx-override-plugin": {
157
+ "types": "./src/plugins/foreign-jsx-override-plugin.d.ts",
158
+ "default": "./src/plugins/foreign-jsx-override-plugin.js"
159
+ },
148
160
  "./adapters/bun/client-bridge": {
149
161
  "types": "./src/adapters/bun/client-bridge.d.ts",
150
162
  "default": "./src/adapters/bun/client-bridge.js"
@@ -197,6 +209,10 @@
197
209
  "types": "./src/eco/eco.d.ts",
198
210
  "default": "./src/eco/eco.js"
199
211
  },
212
+ "./route-renderer/template-serialization.ts": {
213
+ "types": "./src/route-renderer/orchestration/template-serialization.d.ts",
214
+ "default": "./src/route-renderer/orchestration/template-serialization.js"
215
+ },
200
216
  "./declarations.ts": {
201
217
  "types": "./src/declarations.d.ts"
202
218
  },
@@ -278,6 +294,14 @@
278
294
  "types": "./src/build/runtime-specifier-alias-plugin.d.ts",
279
295
  "default": "./src/build/runtime-specifier-alias-plugin.js"
280
296
  },
297
+ "./build/runtime-specifier-aliases.ts": {
298
+ "types": "./src/build/runtime-specifier-aliases.d.ts",
299
+ "default": "./src/build/runtime-specifier-aliases.js"
300
+ },
301
+ "./plugins/foreign-jsx-override-plugin.ts": {
302
+ "types": "./src/plugins/foreign-jsx-override-plugin.d.ts",
303
+ "default": "./src/plugins/foreign-jsx-override-plugin.js"
304
+ },
281
305
  "./adapters/bun/client-bridge.ts": {
282
306
  "types": "./src/adapters/bun/client-bridge.d.ts",
283
307
  "default": "./src/adapters/bun/client-bridge.js"
@@ -251,7 +251,7 @@ class HmrManager {
251
251
  */
252
252
  async registerEntrypoint(entrypointPath) {
253
253
  return await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
254
- emit: async (normalizedEntrypoint, outputPath) => await this.emitStrictEntrypoint(normalizedEntrypoint, outputPath),
254
+ emit: async (normalizedEntrypoint) => await this.emitStrictEntrypoint(normalizedEntrypoint),
255
255
  getMissingOutputError: (normalizedEntrypoint, outputPath) => new Error(
256
256
  `[HMR] Integration failed to emit entrypoint ${normalizedEntrypoint} to ${outputPath}. Page entrypoints must be produced by their owning integration.`
257
257
  )
@@ -278,7 +278,7 @@ class HmrManager {
278
278
  * strategy processing without broadcasting, and then verifies that the owning
279
279
  * integration emitted the expected file.
280
280
  */
281
- async emitStrictEntrypoint(entrypointPath, _outputPath) {
281
+ async emitStrictEntrypoint(entrypointPath) {
282
282
  await this.handleFileChange(entrypointPath, { broadcast: false });
283
283
  }
284
284
  /**
@@ -225,7 +225,7 @@ class NodeHmrManager {
225
225
  */
226
226
  async registerEntrypoint(entrypointPath) {
227
227
  return await this.entrypointRegistrar.registerEntrypoint(entrypointPath, {
228
- emit: async (normalizedEntrypoint, outputPath) => await this.emitStrictEntrypoint(normalizedEntrypoint, outputPath),
228
+ emit: async (normalizedEntrypoint) => await this.emitStrictEntrypoint(normalizedEntrypoint),
229
229
  getMissingOutputError: (normalizedEntrypoint, outputPath) => new Error(
230
230
  `[HMR] Integration failed to emit entrypoint ${normalizedEntrypoint} to ${outputPath}. Page entrypoints must be produced by their owning integration.`
231
231
  )
@@ -255,7 +255,7 @@ class NodeHmrManager {
255
255
  * 3. Let the strategy chain try to emit the entrypoint without broadcasting.
256
256
  * 4. Fail if the owning integration did not emit the expected output.
257
257
  */
258
- async emitStrictEntrypoint(entrypointPath, _outputPath) {
258
+ async emitStrictEntrypoint(entrypointPath) {
259
259
  await this.handleFileChange(entrypointPath, { broadcast: false });
260
260
  }
261
261
  /**
@@ -132,7 +132,7 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
132
132
  * underlying socket closes early — into a 499 response so it does not
133
133
  * incorrectly surface as a 500 in application logs.
134
134
  */
135
- handleRequest(_request: Request): Promise<Response>;
135
+ handleRequest(request: Request): Promise<Response>;
136
136
  /**
137
137
  * Called once the HTTP server is bound and listening.
138
138
  *
@@ -149,7 +149,7 @@ export declare class NodeServerAdapter extends SharedServerAdapter<NodeServerAda
149
149
  * WebSocket upgrade requests that do not target `/_hmr` are rejected with an
150
150
  * immediate socket destroy to prevent unhandled upgrade leaks.
151
151
  */
152
- completeInitialization(_server: NodeServerInstance): Promise<void>;
152
+ completeInitialization(server: NodeServerInstance): Promise<void>;
153
153
  }
154
154
  /**
155
155
  * Factory function that creates and fully initialises a `NodeServerAdapter`.
@@ -269,12 +269,12 @@ class NodeServerAdapter extends SharedServerAdapter {
269
269
  * underlying socket closes early — into a 499 response so it does not
270
270
  * incorrectly surface as a 500 in application logs.
271
271
  */
272
- async handleRequest(_request) {
272
+ async handleRequest(request) {
273
273
  if (!this.initialized) {
274
274
  throw new Error("Node server adapter is not initialized. Call createAdapter() first.");
275
275
  }
276
276
  try {
277
- return await this.handleSharedRequest(_request, {
277
+ return await this.handleSharedRequest(request, {
278
278
  apiHandlers: this.apiHandlers,
279
279
  errorHandler: this.errorHandler,
280
280
  serverInstance: this.serverInstance,
@@ -303,8 +303,8 @@ class NodeServerAdapter extends SharedServerAdapter {
303
303
  * WebSocket upgrade requests that do not target `/_hmr` are rejected with an
304
304
  * immediate socket destroy to prevent unhandled upgrade leaks.
305
305
  */
306
- async completeInitialization(_server) {
307
- this.serverInstance = _server;
306
+ async completeInitialization(server) {
307
+ this.serverInstance = server;
308
308
  if (this.options?.watch) {
309
309
  const { NodeHmrManager } = await import("./node-hmr-manager.js");
310
310
  const { WebSocketServer } = await import("ws");
@@ -313,7 +313,7 @@ class NodeServerAdapter extends SharedServerAdapter {
313
313
  this.hmrManager = new NodeHmrManager({ appConfig: this.appConfig, bridge: this.bridge });
314
314
  this.hmrManager.setEnabled(true);
315
315
  await this.hmrManager.buildRuntime();
316
- _server.on("upgrade", (req, socket, head) => {
316
+ server.on("upgrade", (req, socket, head) => {
317
317
  const url = new URL(req.url ?? "/", this.runtimeOrigin);
318
318
  if (url.pathname === "/_hmr") {
319
319
  wss.handleUpgrade(req, socket, head, (ws) => {
@@ -107,6 +107,7 @@ export declare class BunBuildAdapter implements BuildAdapter {
107
107
  private mapBunTarget;
108
108
  private mapBunFormat;
109
109
  private getOutputExtension;
110
+ private resolveConcreteOutputPath;
110
111
  private resolveTemplatedOutputPath;
111
112
  private relocateOutputFile;
112
113
  private normalizeBunOutputs;
@@ -129,9 +130,10 @@ export declare function createBuildAdapter(options?: {
129
130
  export declare const defaultBunBuildAdapter: BuildAdapter;
130
131
  export declare const defaultViteHostBuildAdapter: BuildAdapter;
131
132
  /**
132
- * @deprecated Prefer app-owned build state via `getAppBuildAdapter()`.
133
- * This Bun-native fallback remains only for compatibility with older helpers
134
- * and tests that do not yet thread app runtime state explicitly.
133
+ * Bun-native fallback export for callsites that still resolve build state
134
+ * globally.
135
+ *
136
+ * New app-aware code should prefer `getAppBuildAdapter()`.
135
137
  */
136
138
  export declare const defaultBuildAdapter: BuildAdapter;
137
139
  export declare function getDefaultBuildAdapter(ownership?: BuildOwnership): BuildAdapter;
@@ -223,9 +225,9 @@ export declare function setAppBuildExecutor(appConfig: EcoPagesAppConfig, buildE
223
225
  */
224
226
  export declare function build(options: BuildOptions, executor?: BuildExecutor): Promise<BuildResult>;
225
227
  /**
226
- * @deprecated Prefer `getAppTranspileOptions()` for finalized app/runtime work.
227
- * This helper exists only for compatibility with Bun-native callsites that do
228
- * not yet have app context available.
228
+ * Bun-native fallback helper for callsites without app runtime context.
229
+ *
230
+ * New app-aware code should prefer `getAppTranspileOptions()`.
229
231
  */
230
232
  export declare function getTranspileOptions(profile: BuildTranspileProfile): BuildTranspileOptions;
231
233
  export declare function getAppTranspileOptions(appConfig: EcoPagesAppConfig, profile: BuildTranspileProfile): BuildTranspileOptions;
@@ -215,7 +215,26 @@ class BunBuildAdapter {
215
215
  }
216
216
  return options.format === "cjs" || options.format === "esm" ? ".js" : ".js";
217
217
  }
218
- resolveTemplatedOutputPath(options, entrypointPath) {
218
+ resolveConcreteOutputPath(outputPath) {
219
+ if (fs.existsSync(outputPath)) {
220
+ return outputPath;
221
+ }
222
+ if (!outputPath.includes("[hash]")) {
223
+ return outputPath;
224
+ }
225
+ const directory = path.dirname(outputPath);
226
+ if (!fs.existsSync(directory)) {
227
+ return void 0;
228
+ }
229
+ const basenamePattern = path.basename(outputPath);
230
+ const matcher = new RegExp(`^${this.escapeRegExp(basenamePattern).replace(/\\\[hash\\\]/g, "(.+)")}$`);
231
+ const matches = fs.readdirSync(directory).filter((candidate) => matcher.test(candidate)).sort();
232
+ if (matches.length === 0) {
233
+ return void 0;
234
+ }
235
+ return path.join(directory, matches[0]);
236
+ }
237
+ resolveTemplatedOutputPath(options, entrypointPath, concreteOutputPath) {
219
238
  if (!options.outdir) {
220
239
  return void 0;
221
240
  }
@@ -233,6 +252,18 @@ class BunBuildAdapter {
233
252
  resolvedPath += outputExtension;
234
253
  }
235
254
  resolvedPath = resolvedPath.replace(/^\.\//, "");
255
+ if (resolvedPath.includes("[hash]")) {
256
+ if (!concreteOutputPath) {
257
+ return path.join(outdir, resolvedPath);
258
+ }
259
+ const concreteRelativePath = path.relative(outdir, concreteOutputPath).split(path.sep).join("/");
260
+ const matcher = new RegExp(`^${this.escapeRegExp(resolvedPath).replace(/\\\[hash\\\]/g, "(.+)")}$`);
261
+ const match = concreteRelativePath.match(matcher);
262
+ if (!match?.[1]) {
263
+ return concreteOutputPath;
264
+ }
265
+ resolvedPath = resolvedPath.replaceAll("[hash]", match[1]);
266
+ }
236
267
  return path.join(outdir, resolvedPath);
237
268
  }
238
269
  relocateOutputFile(currentPath, targetPath) {
@@ -252,12 +283,16 @@ class BunBuildAdapter {
252
283
  const canMapEntrypointsByIndex = options.entrypoints.length === normalizedOutputs.length;
253
284
  if (canMapEntrypointsByIndex) {
254
285
  for (const [index, entrypointPath] of options.entrypoints.entries()) {
255
- const expectedOutputPath = this.resolveTemplatedOutputPath(options, entrypointPath);
286
+ const concreteOutputPath = this.resolveConcreteOutputPath(normalizedOutputs[index].path);
287
+ const expectedOutputPath = this.resolveTemplatedOutputPath(options, entrypointPath, concreteOutputPath);
256
288
  if (!expectedOutputPath) {
257
289
  continue;
258
290
  }
259
291
  normalizedOutputs[index] = {
260
- path: this.relocateOutputFile(normalizedOutputs[index].path, expectedOutputPath)
292
+ path: this.relocateOutputFile(
293
+ concreteOutputPath ?? normalizedOutputs[index].path,
294
+ expectedOutputPath
295
+ )
261
296
  };
262
297
  }
263
298
  return {
@@ -268,12 +303,13 @@ class BunBuildAdapter {
268
303
  return {
269
304
  ...result,
270
305
  outputs: normalizedOutputs.map((output) => {
271
- if (path.extname(output.path) !== "") {
306
+ const concreteOutputPath = this.resolveConcreteOutputPath(output.path) ?? output.path;
307
+ if (path.extname(concreteOutputPath) !== "") {
272
308
  return output;
273
309
  }
274
- const normalizedPath = `${output.path}.js`;
310
+ const normalizedPath = `${concreteOutputPath}.js`;
275
311
  return {
276
- path: this.relocateOutputFile(output.path, normalizedPath)
312
+ path: this.relocateOutputFile(concreteOutputPath, normalizedPath)
277
313
  };
278
314
  })
279
315
  };
@@ -448,7 +484,8 @@ async function collectConfiguredAppBuildManifestContributions(appConfig) {
448
484
  for (const integration of appConfig.integrations) {
449
485
  integration.setConfig(appConfig);
450
486
  await integration.prepareBuildContributions();
451
- runtimePlugins.push(...integration.plugins);
487
+ runtimePlugins.push(...integration.plugins ?? []);
488
+ browserBundlePlugins.push(...integration.browserBuildPlugins ?? []);
452
489
  }
453
490
  return {
454
491
  runtimePlugins,
package/src/eco/eco.js CHANGED
@@ -1,133 +1,33 @@
1
- import { createNodeId, createPropsRef, createSlotRef, getComponentRenderContext } from "./component-render-context.js";
2
- import { createComponentMarker, parseComponentMarkers } from "../route-renderer/component-graph/component-marker.js";
3
1
  import {
4
- getComponentReference,
5
- registerRuntimeComponentHint
6
- } from "../route-renderer/component-graph/component-reference.js";
7
- import { addTriggerAttribute, isThenable, wrapWithScriptsInjector } from "./eco.utils.js";
8
- function isMarkerSerializableTemplateLike(value) {
9
- return typeof value === "object" && value !== null && Array.isArray(value.strings) && (value.values === void 0 || Array.isArray(value.values));
10
- }
11
- function serializeDeferredChildren(value, seen = /* @__PURE__ */ new Set()) {
12
- if (typeof value === "string") {
13
- return value;
14
- }
15
- if (typeof value === "number" || typeof value === "bigint") {
16
- return String(value);
17
- }
18
- if (typeof value === "boolean" || value == null) {
19
- return "";
20
- }
21
- if (Array.isArray(value)) {
22
- return value.map((item) => serializeDeferredChildren(item, seen) ?? "").join("");
23
- }
24
- if (isMarkerSerializableTemplateLike(value)) {
25
- const values = value.values ?? [];
26
- let html2 = "";
27
- for (let index = 0; index < value.strings.length; index += 1) {
28
- html2 += value.strings[index] ?? "";
29
- if (index < values.length) {
30
- html2 += serializeDeferredChildren(values[index], seen) ?? "";
31
- }
32
- }
33
- return html2;
34
- }
35
- if (typeof value === "object" && value !== null) {
36
- if (seen.has(value)) {
37
- return "";
38
- }
39
- seen.add(value);
40
- const serialized = Object.values(value).map((entry) => serializeDeferredChildren(entry, seen) ?? "").join("");
41
- seen.delete(value);
42
- return serialized.length > 0 ? serialized : void 0;
43
- }
44
- return void 0;
45
- }
46
- function getRuntimeComponentHint(render, options) {
47
- const stack = new Error().stack;
48
- if (stack) {
49
- for (const line of stack.split("\n").slice(1)) {
50
- const trimmed = line.trim();
51
- if (trimmed.includes("/packages/core/src/eco/eco.ts") || trimmed.includes("createComponentFactory") || trimmed.includes("component (") || trimmed.includes("layout (") || trimmed.includes("html (") || trimmed.includes("page (")) {
52
- continue;
53
- }
54
- const match = trimmed.match(/(?:at\s+.*?\()?(.+?:\d+:\d+)\)?$/);
55
- if (match?.[1]) {
56
- return match[1];
57
- }
58
- }
59
- }
60
- const renderSignature = typeof render === "function" ? render.toString() : void 0;
61
- if (!renderSignature) {
62
- return void 0;
63
- }
64
- return JSON.stringify({
65
- integration: options.integration,
66
- render: renderSignature,
67
- stylesheets: options.dependencies?.stylesheets,
68
- scripts: options.dependencies?.scripts
69
- });
70
- }
2
+ finalizeComponentRender,
3
+ interceptComponentBoundary
4
+ } from "../route-renderer/orchestration/component-render-context.js";
5
+ import { isThenable } from "../route-renderer/orchestration/render-output.utils.js";
71
6
  function createComponentFactory(options) {
72
7
  const integrationName = options.integration ?? options.__eco?.integration;
73
8
  const comp = ((props) => {
74
- const renderContext = getComponentRenderContext();
75
- const shouldEmitMarker = renderContext !== void 0 && renderContext.boundaryContext.decideBoundaryRender({
76
- currentIntegration: renderContext.currentIntegration,
77
- targetIntegration: integrationName,
78
- component: comp
79
- }) === "defer";
80
- if (shouldEmitMarker && renderContext) {
81
- const nodeId = createNodeId(renderContext);
82
- const propsRef = createPropsRef(renderContext);
83
- const componentRef = getComponentReference(comp);
84
- const componentProps = props ?? {};
85
- const storedProps = { ...componentProps };
86
- const serializedChildren = serializeDeferredChildren(componentProps.children);
87
- renderContext.propsByRef[propsRef] = serializedChildren === void 0 ? storedProps : { ...storedProps, children: serializedChildren };
88
- let slotRef;
89
- if (typeof serializedChildren === "string" && serializedChildren.includes("<eco-marker")) {
90
- const childMarkers = parseComponentMarkers(serializedChildren);
91
- if (childMarkers.length > 0) {
92
- slotRef = createSlotRef(renderContext);
93
- renderContext.slotChildrenByRef[slotRef] = childMarkers.map((marker) => marker.nodeId);
94
- }
95
- }
96
- return createComponentMarker({
97
- nodeId,
98
- integration: integrationName,
99
- componentRef,
100
- propsRef,
101
- slotRef
102
- });
9
+ const componentProps = props ?? {};
10
+ const renderInline = () => finalizeComponentRender(comp, options.render(props));
11
+ const boundaryRender = interceptComponentBoundary({
12
+ component: comp,
13
+ props: componentProps,
14
+ targetIntegration: integrationName
15
+ });
16
+ if (isThenable(boundaryRender)) {
17
+ return boundaryRender.then(
18
+ (resolvedBoundaryRender) => resolvedBoundaryRender !== void 0 ? resolvedBoundaryRender : renderInline()
19
+ );
103
20
  }
104
- const content = options.render(props);
105
- const lazyTriggers = comp.config?._resolvedLazyTriggers;
106
- if (lazyTriggers && lazyTriggers.length > 0) {
107
- const triggerId = lazyTriggers[0].triggerId;
108
- if (isThenable(content)) {
109
- return content.then((resolvedContent) => addTriggerAttribute(resolvedContent, triggerId));
110
- }
111
- return addTriggerAttribute(content, triggerId);
21
+ if (boundaryRender !== void 0) {
22
+ return boundaryRender;
112
23
  }
113
- const lazyGroups = comp.config?._resolvedLazyScripts;
114
- if (lazyGroups && lazyGroups.length > 0) {
115
- if (isThenable(content)) {
116
- return content.then((resolvedContent) => wrapWithScriptsInjector(resolvedContent, lazyGroups));
117
- }
118
- return wrapWithScriptsInjector(content, lazyGroups);
119
- }
120
- return content;
24
+ return renderInline();
121
25
  });
122
26
  comp.config = {
123
27
  __eco: options.__eco,
124
28
  integration: options.integration,
125
29
  dependencies: options.dependencies
126
30
  };
127
- const runtimeHint = getRuntimeComponentHint(options.render, options);
128
- if (runtimeHint) {
129
- registerRuntimeComponentHint(comp, runtimeHint);
130
- }
131
31
  return comp;
132
32
  }
133
33
  function component(options) {
@@ -1,40 +1 @@
1
- import type { EcoComponent } from '../types/public-types.js';
2
- /**
3
- * Returns `true` when `value` is a thenable (Promise-like) object.
4
- *
5
- * Used to transparently handle both synchronous and asynchronous component
6
- * render results without requiring every caller to branch on `instanceof Promise`.
7
- *
8
- * @typeParam T Expected resolved type of the thenable.
9
- */
10
- export declare function isThenable<T>(value: unknown): value is PromiseLike<T>;
11
- /**
12
- * Injects `data-eco-trigger` into the first real HTML element opening tag of
13
- * a component's rendered output string.
14
- *
15
- * The scan skips over leading whitespace, HTML comments (`<!-- -->`), CDATA
16
- * sections, and doctype declarations so that the attribute is always placed on
17
- * the first actual element — not spurious markup that can precede it.
18
- *
19
- * The insertion point is the end of the element's tag name, before any existing
20
- * attributes or the closing `>`, which produces output like:
21
- *
22
- * ```html
23
- * <my-element data-eco-trigger="eco-trigger-abc123" class="foo">…</my-element>
24
- * ```
25
- *
26
- * When no eligible opening tag is found the original string is returned
27
- * unchanged so callers never receive a broken fragment.
28
- *
29
- * @param content Rendered HTML string (or any value coercible to string).
30
- * @param triggerId Stable trigger identifier produced by `buildResolvedLazyTriggers`.
31
- */
32
- export declare function addTriggerAttribute(content: unknown, triggerId: string): string;
33
- /**
34
- * Wraps rendered component output in a `<scripts-injector>` element that
35
- * carries an inline injector map for the legacy (non-global-injector) path.
36
- *
37
- * @param content Rendered component HTML.
38
- * @param lazyGroups Resolved lazy script groups attached to the component config.
39
- */
40
- export declare function wrapWithScriptsInjector(content: unknown, lazyGroups: NonNullable<EcoComponent['config']>['_resolvedLazyScripts']): string;
1
+ export { addTriggerAttribute, isThenable, wrapWithScriptsInjector, } from '../route-renderer/orchestration/render-output.utils.js';
@@ -1,38 +1,8 @@
1
- import { buildInjectorMapScript } from "./lazy-injector-map.js";
2
- function isThenable(value) {
3
- return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
4
- }
5
- function addTriggerAttribute(content, triggerId) {
6
- const str = String(content);
7
- let i = 0;
8
- while (i < str.length) {
9
- if (str[i] !== "<") {
10
- i++;
11
- continue;
12
- }
13
- const next = str[i + 1];
14
- if (next === "!" || next === "?") {
15
- const end = str.indexOf(">", i);
16
- if (end === -1) break;
17
- i = end + 1;
18
- continue;
19
- }
20
- if (next && /[a-zA-Z]/.test(next)) {
21
- const tagSlice = str.slice(i + 1);
22
- const nameEnd = tagSlice.search(/[\s/>]/);
23
- if (nameEnd === -1) break;
24
- const insertAt = i + 1 + nameEnd;
25
- return `${str.slice(0, insertAt)} data-eco-trigger="${triggerId}"${str.slice(insertAt)}`;
26
- }
27
- break;
28
- }
29
- return str;
30
- }
31
- function wrapWithScriptsInjector(content, lazyGroups) {
32
- const wrappedContent = String(content);
33
- const injectorMapScript = buildInjectorMapScript(lazyGroups ?? []);
34
- return `<scripts-injector><script type="ecopages/injector-map">${injectorMapScript}<\/script>${wrappedContent}<\/scripts-injector>`;
35
- }
1
+ import {
2
+ addTriggerAttribute,
3
+ isThenable,
4
+ wrapWithScriptsInjector
5
+ } from "../route-renderer/orchestration/render-output.utils.js";
36
6
  export {
37
7
  addTriggerAttribute,
38
8
  isThenable,
@@ -18,11 +18,13 @@ import type { ClientBridgeEvent } from '../types/public-types.js';
18
18
  * async process(filePath: string): Promise<HmrAction> {
19
19
  * return {
20
20
  * type: 'broadcast',
21
- * events: [{ type: 'update', path: filePath, timestamp: Date.now() }]
21
+ * events: [{ type: 'update', path: filePath, timestamp: Date.now() }],
22
22
  * };
23
23
  * }
24
24
  * }
25
-
25
+ * ```
26
+ */
27
+ /**
26
28
  * Defines the category of an HMR strategy, which determines its execution priority.
27
29
  * Strategies are evaluated in descending order: INTEGRATION → ASSET → SCRIPT → FALLBACK.
28
30
  *
@@ -63,7 +65,7 @@ export interface HmrAction {
63
65
  type: 'broadcast' | 'none';
64
66
  /**
65
67
  * The HMR events to broadcast, if type is 'broadcast'.
66
- * capable of broadcasting multiple events at once.
68
+ * Multiple events may be broadcast in one action.
67
69
  */
68
70
  events?: ClientBridgeEvent[];
69
71
  }
@@ -75,8 +77,8 @@ export interface HmrAction {
75
77
  * whether they match the changed file path.
76
78
  *
77
79
  * @remarks
78
- * Strategies should be stateless and idempotent. The same file change should always
79
- * produce the same result when processed by the same strategy.
80
+ * Strategies are expected to be stateless and idempotent. The same file change
81
+ * should produce the same result when processed by the same strategy.
80
82
  *
81
83
  * @example
82
84
  * ```typescript
@@ -121,7 +123,7 @@ export declare abstract class HmrStrategy {
121
123
  * Determines if this strategy can handle the given file path.
122
124
  *
123
125
  * @param filePath - Absolute path to the changed file
124
- * @returns True if this strategy should process the file
126
+ * @returns `true` when this strategy should process the file
125
127
  *
126
128
  * @example
127
129
  * ```typescript