@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
@@ -5,9 +5,6 @@ import {
5
5
  AssetFactory
6
6
  } from "../../services/assets/asset-processing-service/index.js";
7
7
  import { buildGlobalInjectorBootstrapContent, buildGlobalInjectorMapScript } from "../../eco/global-injector-map.js";
8
- import {
9
- runWithComponentRenderContext
10
- } from "../../eco/component-render-context.js";
11
8
  class RenderPreparationService {
12
9
  appConfig;
13
10
  assetProcessingService;
@@ -52,13 +49,13 @@ class RenderPreparationService {
52
49
  const allDependencies = [...resolvedDependencies, ...usedIntegrationDependencies, ...pageDeps];
53
50
  let componentRender;
54
51
  if (callbacks.shouldRenderPageComponent({ Page, Layout, options: routeOptions })) {
55
- componentRender = await this.renderPageRoot({
56
- currentIntegrationName,
52
+ const pageRootRender = await this.renderPageRoot({
57
53
  Page,
58
54
  props,
59
55
  routeOptions,
60
56
  callbacks
61
57
  });
58
+ componentRender = pageRootRender.componentRender;
62
59
  if (componentRender.assets?.length) {
63
60
  allDependencies.push(...componentRender.assets);
64
61
  }
@@ -196,12 +193,8 @@ class RenderPreparationService {
196
193
  * @returns Structured component render result.
197
194
  */
198
195
  async renderPageRoot(input) {
199
- const execution = await runWithComponentRenderContext(
200
- {
201
- currentIntegration: input.currentIntegrationName,
202
- boundaryContext: input.callbacks.getComponentRenderBoundaryContext()
203
- },
204
- async () => input.callbacks.renderPageComponent({
196
+ return {
197
+ componentRender: await input.callbacks.renderPageComponent({
205
198
  component: input.Page,
206
199
  props: {
207
200
  ...input.props,
@@ -209,8 +202,7 @@ class RenderPreparationService {
209
202
  query: input.routeOptions.query || {}
210
203
  }
211
204
  })
212
- );
213
- return execution.value;
205
+ };
214
206
  }
215
207
  /**
216
208
  * Builds the runtime assets needed to bootstrap global lazy trigger execution.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Minimal template-result shape that core can serialize generically.
3
+ *
4
+ * @remarks
5
+ * Core intentionally models only the transport shape used during deferred
6
+ * cross-integration child passing: alternating static string segments plus
7
+ * interpolated runtime values. Concrete runtime markers that identify a given
8
+ * framework's template payload must stay outside core and be registered by the
9
+ * owning integration package.
10
+ */
11
+ export type SerializableTemplateShape = {
12
+ strings: readonly string[];
13
+ values?: readonly unknown[];
14
+ };
15
+ /**
16
+ * Integration-owned adapter that teaches core how to serialize one deferred
17
+ * template payload shape.
18
+ *
19
+ * @remarks
20
+ * The separation here is intentional: core owns the HTML serialization logic,
21
+ * but integrations own runtime-shape detection. That keeps package-specific
22
+ * markers such as framework template sentinels out of core while still letting
23
+ * deferred children round-trip through mixed renderer boundaries.
24
+ */
25
+ export type DeferredTemplateSerializer<TTemplate extends SerializableTemplateShape = SerializableTemplateShape> = {
26
+ matches(value: unknown): value is TTemplate;
27
+ serialize(template: TTemplate, serializeValue: (value: unknown) => string | undefined): string;
28
+ };
29
+ /**
30
+ * Serializes a generic template shape into HTML.
31
+ *
32
+ * @remarks
33
+ * This handles only HTML reconstruction semantics: quoted attribute values,
34
+ * boolean attributes, and omission of client-only event or property bindings.
35
+ * It does not decide whether an arbitrary value belongs to a framework-specific
36
+ * template runtime; integrations must make that decision before delegating here.
37
+ */
38
+ export declare function serializeTemplateShape(template: SerializableTemplateShape, serializeValue: (value: unknown) => string | undefined): string;
@@ -0,0 +1,45 @@
1
+ import { escapeHtmlAttribute } from "../../utils/html-escaping.js";
2
+ const ATTRIBUTE_TAIL_PATTERN = /(\s+)([@.?!]?)([^\s"'<>/=`@.?!]+)=$/;
3
+ function getSerializableTemplateAttribute(stringPart) {
4
+ const match = ATTRIBUTE_TAIL_PATTERN.exec(stringPart);
5
+ if (!match) return void 0;
6
+ return {
7
+ leading: stringPart.slice(0, match.index),
8
+ whitespace: match[1],
9
+ prefix: match[2],
10
+ name: match[3]
11
+ };
12
+ }
13
+ function serializeTemplateShape(template, serializeValue) {
14
+ const values = template.values ?? [];
15
+ let html = "";
16
+ for (let index = 0; index < values.length; index += 1) {
17
+ const stringPart = template.strings[index] ?? "";
18
+ const serializedValue = serializeValue(values[index]);
19
+ const attribute = getSerializableTemplateAttribute(stringPart);
20
+ if (!attribute) {
21
+ html += stringPart;
22
+ html += serializedValue ?? "";
23
+ continue;
24
+ }
25
+ html += attribute.leading;
26
+ if (attribute.prefix === "@" || attribute.prefix === "!" || attribute.prefix === ".") {
27
+ continue;
28
+ }
29
+ if (attribute.prefix === "?") {
30
+ if (serializedValue) {
31
+ html += `${attribute.whitespace}${attribute.name}`;
32
+ }
33
+ continue;
34
+ }
35
+ if (serializedValue === void 0) {
36
+ continue;
37
+ }
38
+ html += `${attribute.whitespace}${attribute.name}="${escapeHtmlAttribute(serializedValue)}"`;
39
+ }
40
+ html += template.strings[values.length] ?? "";
41
+ return html;
42
+ }
43
+ export {
44
+ serializeTemplateShape
45
+ };
@@ -34,7 +34,12 @@ function extractEcopagesVirtualImports(file) {
34
34
  const namedImports = [];
35
35
  for (const spec of node.specifiers ?? []) {
36
36
  if (spec.type === "ImportSpecifier") {
37
- const importedName = spec.imported?.type === "Identifier" ? spec.imported.name : spec.imported?.type === "Literal" ? spec.imported.value : spec.local?.name;
37
+ let importedName = spec.local?.name;
38
+ if (spec.imported?.type === "Identifier") {
39
+ importedName = spec.imported.name;
40
+ } else if (spec.imported?.type === "Literal") {
41
+ importedName = spec.imported.value;
42
+ }
38
43
  namedImports.push(importedName);
39
44
  }
40
45
  }
@@ -62,7 +67,7 @@ function createModuleScriptName(from, imports) {
62
67
  return `module-${hash}`;
63
68
  }
64
69
  function createNamedImportModuleSource(from, imports) {
65
- const namedImports = imports.join(", ");
70
+ const namedImports = [...new Set(imports)].sort().join(", ");
66
71
  return `export { ${namedImports} } from '${from}';`;
67
72
  }
68
73
  function createNamespaceImportModuleSource(from) {
@@ -259,14 +264,11 @@ class DependencyResolverService {
259
264
  }
260
265
  scriptDependencyKeys.add(depKey2);
261
266
  dependencies.push(
262
- AssetFactory.createContentScript({
267
+ AssetFactory.createInlineContentScript({
263
268
  position: "head",
264
269
  content,
265
- attributes: {
266
- type: "module",
267
- defer: "",
268
- ...attributes
269
- }
270
+ bundle: false,
271
+ attributes
270
272
  })
271
273
  );
272
274
  continue;
@@ -16,7 +16,7 @@ function getCandidateOwners(currentOwner, registrations, excludedOwner) {
16
16
  }
17
17
  return owners;
18
18
  }
19
- function createEcoNavigationRuntime(_windowObject) {
19
+ function createEcoNavigationRuntime() {
20
20
  const registrations = /* @__PURE__ */ new Map();
21
21
  const listeners = /* @__PURE__ */ new Set();
22
22
  let owner = "none";
@@ -204,7 +204,7 @@ function getEcoNavigationRuntime(windowObject = window) {
204
204
  const runtimeWindow = windowObject;
205
205
  runtimeWindow.__ECO_PAGES__ = runtimeWindow.__ECO_PAGES__ || {};
206
206
  if (!runtimeWindow.__ECO_PAGES__.navigation) {
207
- runtimeWindow.__ECO_PAGES__.navigation = createEcoNavigationRuntime(runtimeWindow);
207
+ runtimeWindow.__ECO_PAGES__.navigation = createEcoNavigationRuntime();
208
208
  }
209
209
  return runtimeWindow.__ECO_PAGES__.navigation;
210
210
  }
@@ -113,7 +113,12 @@ class FSRouterScanner {
113
113
  const filePath = path.join(this.dir, file);
114
114
  const isCatchAll = filePath.includes("[...");
115
115
  const isDynamic = !isCatchAll && filePath.includes("[") && filePath.includes("]");
116
- const kind = isCatchAll ? "catch-all" : isDynamic ? "dynamic" : "exact";
116
+ let kind = "exact";
117
+ if (isCatchAll) {
118
+ kind = "catch-all";
119
+ } else if (isDynamic) {
120
+ kind = "dynamic";
121
+ }
117
122
  return { route, routePath, filePath, kind };
118
123
  }
119
124
  async scan() {
@@ -104,7 +104,20 @@ function resolveNodeBootstrapDependency(args, options) {
104
104
  try {
105
105
  resolvedPath2 = resolveFromCore(args.path);
106
106
  } catch {
107
- resolvedPath2 = resolveSpecifier(args.path, resolveParent);
107
+ try {
108
+ resolvedPath2 = resolveSpecifier(args.path, resolveParent);
109
+ } catch {
110
+ const packageName = getPackageNameFromSpecifier(args.path);
111
+ const candidatePath = path.join(options.projectDir, "node_modules", packageName);
112
+ const candidatePackageJson = path.join(candidatePath, "package.json");
113
+ if (existsSync(candidatePackageJson)) {
114
+ ensureRuntimePackageLink(options.runtimeNodeModulesDir, args.path, candidatePackageJson);
115
+ return { path: args.path, external: true };
116
+ }
117
+ }
118
+ }
119
+ if (!resolvedPath2) {
120
+ return void 0;
108
121
  }
109
122
  if (resolvedPath2.includes(`${path.sep}node_modules${path.sep}`)) {
110
123
  ensureRuntimePackageLink(options.runtimeNodeModulesDir, args.path, resolvedPath2);
@@ -114,7 +114,7 @@ class PageModuleImportService {
114
114
  entrypoints: [filePath],
115
115
  root: rootDir,
116
116
  outdir,
117
- target: "node",
117
+ target: "es2022",
118
118
  format: "esm",
119
119
  sourcemap: "none",
120
120
  splitting: splitting ?? true,
@@ -32,7 +32,7 @@ export declare class NoopDevGraphService implements DevGraphService {
32
32
  /**
33
33
  * Invalidates all server-side module state by incrementing the shared version.
34
34
  */
35
- invalidateServerModules(_changedFiles?: string[]): void;
35
+ invalidateServerModules(changedFiles?: string[]): void;
36
36
  /**
37
37
  * Indicates that this graph cannot target invalidation to one entrypoint set.
38
38
  */
@@ -41,19 +41,19 @@ export declare class NoopDevGraphService implements DevGraphService {
41
41
  * Returns an empty entrypoint set because this implementation stores no
42
42
  * dependency graph metadata.
43
43
  */
44
- getDependencyEntrypoints(_filePath: string): Set<string>;
44
+ getDependencyEntrypoints(filePath: string): Set<string>;
45
45
  /**
46
46
  * Accepts dependency updates to preserve interface compatibility, but stores no
47
47
  * graph state in the noop implementation.
48
48
  */
49
- setEntrypointDependencies(_entrypointPath: string, _dependencies: string[]): void;
49
+ setEntrypointDependencies(entrypointPath: string, dependencies: string[]): void;
50
50
  /**
51
51
  * Clears one entrypoint from the graph.
52
52
  *
53
53
  * @remarks
54
54
  * There is no stored graph state in this implementation, so this is a no-op.
55
55
  */
56
- clearEntrypointDependencies(_entrypointPath: string): void;
56
+ clearEntrypointDependencies(entrypointPath: string): void;
57
57
  /**
58
58
  * Resets graph-owned state for a fresh runtime cycle.
59
59
  */
@@ -82,7 +82,7 @@ export declare class InMemoryDevGraphService implements DevGraphService {
82
82
  * modules. Selective dependency lookups are used by callers that need to limit
83
83
  * browser rebuild work.
84
84
  */
85
- invalidateServerModules(_changedFiles?: string[]): void;
85
+ invalidateServerModules(changedFiles?: string[]): void;
86
86
  /**
87
87
  * Indicates that this graph can answer dependency-to-entrypoint lookups.
88
88
  */
@@ -21,8 +21,8 @@ class NoopDevGraphService {
21
21
  /**
22
22
  * Invalidates all server-side module state by incrementing the shared version.
23
23
  */
24
- invalidateServerModules(_changedFiles) {
25
- this.invalidationState.invalidateServerModules(_changedFiles);
24
+ invalidateServerModules(changedFiles) {
25
+ this.invalidationState.invalidateServerModules(changedFiles);
26
26
  }
27
27
  /**
28
28
  * Indicates that this graph cannot target invalidation to one entrypoint set.
@@ -34,15 +34,15 @@ class NoopDevGraphService {
34
34
  * Returns an empty entrypoint set because this implementation stores no
35
35
  * dependency graph metadata.
36
36
  */
37
- getDependencyEntrypoints(_filePath) {
38
- return this.dependencyGraph.getDependencyEntrypoints(_filePath);
37
+ getDependencyEntrypoints(filePath) {
38
+ return this.dependencyGraph.getDependencyEntrypoints(filePath);
39
39
  }
40
40
  /**
41
41
  * Accepts dependency updates to preserve interface compatibility, but stores no
42
42
  * graph state in the noop implementation.
43
43
  */
44
- setEntrypointDependencies(_entrypointPath, _dependencies) {
45
- this.dependencyGraph.setEntrypointDependencies(_entrypointPath, _dependencies);
44
+ setEntrypointDependencies(entrypointPath, dependencies) {
45
+ this.dependencyGraph.setEntrypointDependencies(entrypointPath, dependencies);
46
46
  }
47
47
  /**
48
48
  * Clears one entrypoint from the graph.
@@ -50,8 +50,8 @@ class NoopDevGraphService {
50
50
  * @remarks
51
51
  * There is no stored graph state in this implementation, so this is a no-op.
52
52
  */
53
- clearEntrypointDependencies(_entrypointPath) {
54
- this.dependencyGraph.clearEntrypointDependencies(_entrypointPath);
53
+ clearEntrypointDependencies(entrypointPath) {
54
+ this.dependencyGraph.clearEntrypointDependencies(entrypointPath);
55
55
  }
56
56
  /**
57
57
  * Resets graph-owned state for a fresh runtime cycle.
@@ -78,8 +78,8 @@ class InMemoryDevGraphService {
78
78
  * modules. Selective dependency lookups are used by callers that need to limit
79
79
  * browser rebuild work.
80
80
  */
81
- invalidateServerModules(_changedFiles) {
82
- this.invalidationState.invalidateServerModules(_changedFiles);
81
+ invalidateServerModules(changedFiles) {
82
+ this.invalidationState.invalidateServerModules(changedFiles);
83
83
  }
84
84
  /**
85
85
  * Indicates that this graph can answer dependency-to-entrypoint lookups.
@@ -2,6 +2,7 @@ import type { Readable } from 'node:stream';
2
2
  import type { ApiResponseBuilder } from '../adapters/shared/api-response.js';
3
3
  import type { BuildExecutor } from '../build/build-adapter.js';
4
4
  import type { EcoBuildPlugin } from '../build/build-types.js';
5
+ import type { ComponentBoundaryRuntime } from '../route-renderer/orchestration/component-render-context.js';
5
6
  import type { EcoPageComponent } from '../eco/eco.types.js';
6
7
  import type { EcoPagesAppConfig } from './internal-types.js';
7
8
  import type { HmrStrategy } from '../hmr/hmr-strategy.js';
@@ -11,10 +12,9 @@ import type { CacheStats, CacheStrategy } from '../services/cache/cache.types.js
11
12
  import type { InteractionEventsString as ScriptsInjectorInteractionEventsString } from '@ecopages/scripts-injector/types';
12
13
  export type { EcoPagesAppConfig } from './internal-types.js';
13
14
  export type { EcoPageComponent } from '../eco/eco.types.js';
14
- export type { MarkerGraphContext } from '../route-renderer/component-graph/marker-graph-resolver.js';
15
15
  export type { ProcessedAsset } from '../services/assets/asset-processing-service/assets.types.js';
16
16
  import type { StandardSchema, StandardSchemaResult, StandardSchemaSuccessResult, StandardSchemaFailureResult, StandardSchemaIssue, InferOutput } from '../services/validation/standard-schema.types.js';
17
- export type { StandardSchema, StandardSchemaResult, StandardSchemaSuccessResult, StandardSchemaFailureResult, StandardSchemaIssue, InferOutput, };
17
+ export type { StandardSchema, StandardSchemaResult, StandardSchemaSuccessResult, StandardSchemaFailureResult, StandardSchemaIssue, InferOutput, ComponentBoundaryRuntime, };
18
18
  export type InteractionEventsString = ScriptsInjectorInteractionEventsString;
19
19
  export type DependencyLazyTrigger = {
20
20
  'on:idle': true;
@@ -262,6 +262,21 @@ export type EcoComponentDependencies = {
262
262
  components?: EcoComponent[];
263
263
  };
264
264
  export type EcoPagesElement = string | Promise<string>;
265
+ /**
266
+ * Serializable child payloads accepted by cross-integration deferred rendering.
267
+ *
268
+ * This models the broad value shapes that EcoPages already flattens when a
269
+ * foreign component boundary serializes its children for another integration.
270
+ * It is intentionally transport-oriented rather than framework-native, so it
271
+ * can be shared across Kita, Lit, React, and Ecopages JSX authoring surfaces
272
+ * without coupling core types to any one renderer.
273
+ */
274
+ export type EcoChildren = string | Promise<string> | number | bigint | boolean | null | undefined | readonly EcoChildren[] | {
275
+ strings: readonly string[];
276
+ values?: readonly EcoChildren[];
277
+ } | {
278
+ [key: string]: EcoChildren;
279
+ };
265
280
  /**
266
281
  * Represents the input configuration for EcoPages.
267
282
  */
@@ -667,7 +682,7 @@ export type IntegrationRendererRenderOptions<C = EcoPagesElement> = RouteRendere
667
682
  export interface ComponentRenderInput {
668
683
  component: EcoComponent;
669
684
  props: Record<string, unknown>;
670
- children?: string;
685
+ children?: unknown;
671
686
  integrationContext?: unknown;
672
687
  }
673
688
  export interface ComponentRenderResult {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Escapes a string for safe use inside a double-quoted HTML attribute value.
3
+ *
4
+ * @param value Raw attribute value.
5
+ * @returns Escaped attribute-safe string.
6
+ */
7
+ export declare function escapeHtmlAttribute(value: string): string;
@@ -0,0 +1,6 @@
1
+ function escapeHtmlAttribute(value) {
2
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3
+ }
4
+ export {
5
+ escapeHtmlAttribute
6
+ };
@@ -1,105 +0,0 @@
1
- import type { EcoComponent } from '../types/public-types.js';
2
- import type { MarkerNodeId } from '../route-renderer/component-graph/component-marker.js';
3
- /**
4
- * Outcome returned by boundary policy during one component render pass.
5
- *
6
- * - `inline`: render the target component immediately in the current pass
7
- * - `defer`: emit an `eco-marker` and resolve it during the marker graph phase
8
- */
9
- export type BoundaryRenderMode = 'inline' | 'defer';
10
- /**
11
- * Input provided to boundary policy when a component boundary is reached.
12
- *
13
- * This keeps `eco.component()` decoupled from concrete integration/plugin
14
- * objects while still giving policy enough information to decide whether the
15
- * boundary should render immediately or be deferred.
16
- */
17
- export type BoundaryRenderDecisionInput = {
18
- currentIntegration: string;
19
- targetIntegration?: string;
20
- component: EcoComponent;
21
- };
22
- /**
23
- * Narrow render-pass facade used by `eco.component()` for boundary decisions.
24
- *
25
- * The boundary context is intentionally small so component rendering can remain
26
- * unaware of integration registries, plugin instances, or renderer lifecycles.
27
- */
28
- export type ComponentRenderBoundaryContext = {
29
- /**
30
- * Decides whether the next component boundary should render inline or defer to
31
- * the marker graph stage.
32
- *
33
- * @param input Boundary metadata for the current render pass.
34
- * @returns Boundary rendering mode for the target component.
35
- */
36
- decideBoundaryRender(input: BoundaryRenderDecisionInput): BoundaryRenderMode;
37
- };
38
- /**
39
- * Per-render mutable state used while collecting marker graph references.
40
- *
41
- * Counters generate deterministic ids within one render execution.
42
- */
43
- type ComponentRenderContext = {
44
- currentIntegration: string;
45
- boundaryContext: ComponentRenderBoundaryContext;
46
- nextNodeId: number;
47
- nextPropsRefId: number;
48
- nextSlotRefId: number;
49
- propsByRef: Record<string, Record<string, unknown>>;
50
- slotChildrenByRef: Record<string, MarkerNodeId[]>;
51
- };
52
- /**
53
- * Serializable graph context captured from one render execution.
54
- *
55
- * This payload is merged with explicit page-module graph context before marker
56
- * resolution in the route renderer.
57
- */
58
- export type ComponentGraphContext = {
59
- propsByRef: Record<string, Record<string, unknown>>;
60
- slotChildrenByRef: Record<string, MarkerNodeId[]>;
61
- };
62
- /**
63
- * Returns the current component render context, if one is active.
64
- *
65
- * @returns Active render context or `undefined` outside render execution.
66
- */
67
- export declare function getComponentRenderContext(): ComponentRenderContext | undefined;
68
- /**
69
- * Allocates the next marker node id in the active render context.
70
- *
71
- * @param context Active render context.
72
- * @returns Stable marker node id for this render pass.
73
- */
74
- export declare function createNodeId(context: ComponentRenderContext): MarkerNodeId;
75
- /**
76
- * Allocates the next props reference id in the active render context.
77
- *
78
- * @param context Active render context.
79
- * @returns Props reference key.
80
- */
81
- export declare function createPropsRef(context: ComponentRenderContext): string;
82
- /**
83
- * Allocates the next slot reference id in the active render context.
84
- *
85
- * @param context Active render context.
86
- * @returns Slot reference key.
87
- */
88
- export declare function createSlotRef(context: ComponentRenderContext): string;
89
- /**
90
- * Runs render work under a fresh component render context and returns both:
91
- * - the render result value
92
- * - captured graph reference maps for downstream marker resolution
93
- *
94
- * @param input Execution metadata for current integration and boundary policy.
95
- * @param render Async render function to execute inside the context.
96
- * @returns Render result and captured graph context maps.
97
- */
98
- export declare function runWithComponentRenderContext<T>(input: {
99
- currentIntegration: string;
100
- boundaryContext: ComponentRenderBoundaryContext;
101
- }, render: () => Promise<T>): Promise<{
102
- value: T;
103
- graphContext: ComponentGraphContext;
104
- }>;
105
- export {};
@@ -1,94 +0,0 @@
1
- const GLOBAL_COMPONENT_RENDER_CONTEXT_STATE_KEY = "__ECOPAGES_COMPONENT_RENDER_CONTEXT_STATE__";
2
- function getSharedContextScope() {
3
- const globalProcess = globalThis.process;
4
- if (globalProcess && typeof globalProcess === "object") {
5
- return globalProcess;
6
- }
7
- return globalThis;
8
- }
9
- function getComponentRenderContextState() {
10
- const sharedScope = getSharedContextScope();
11
- sharedScope[GLOBAL_COMPONENT_RENDER_CONTEXT_STATE_KEY] ??= {
12
- contextStack: [],
13
- nodeContextStorage: null,
14
- nodeContextStorageLoader: null
15
- };
16
- return sharedScope[GLOBAL_COMPONENT_RENDER_CONTEXT_STATE_KEY];
17
- }
18
- async function getContextStorage() {
19
- const state = getComponentRenderContextState();
20
- if (state.nodeContextStorage) {
21
- return state.nodeContextStorage;
22
- }
23
- if (state.nodeContextStorageLoader) {
24
- return state.nodeContextStorageLoader;
25
- }
26
- state.nodeContextStorageLoader = import("node:async_hooks").then((module) => {
27
- const storage = new module.AsyncLocalStorage();
28
- state.nodeContextStorage = {
29
- getStore: () => storage.getStore(),
30
- run: (store, callback) => storage.run(store, callback)
31
- };
32
- return state.nodeContextStorage;
33
- }).catch(() => {
34
- state.nodeContextStorage = null;
35
- return null;
36
- }).finally(() => {
37
- state.nodeContextStorageLoader = null;
38
- });
39
- return state.nodeContextStorageLoader;
40
- }
41
- function getComponentRenderContext() {
42
- const state = getComponentRenderContextState();
43
- return state.nodeContextStorage?.getStore() ?? state.contextStack[state.contextStack.length - 1];
44
- }
45
- function createNodeId(context) {
46
- context.nextNodeId += 1;
47
- return `n_${context.nextNodeId}`;
48
- }
49
- function createPropsRef(context) {
50
- context.nextPropsRefId += 1;
51
- return `p_${context.nextPropsRefId}`;
52
- }
53
- function createSlotRef(context) {
54
- context.nextSlotRefId += 1;
55
- return `s_${context.nextSlotRefId}`;
56
- }
57
- async function runWithComponentRenderContext(input, render) {
58
- const context = {
59
- currentIntegration: input.currentIntegration,
60
- boundaryContext: input.boundaryContext,
61
- nextNodeId: 0,
62
- nextPropsRefId: 0,
63
- nextSlotRefId: 0,
64
- propsByRef: {},
65
- slotChildrenByRef: {}
66
- };
67
- const storage = await getContextStorage();
68
- let value;
69
- if (storage) {
70
- value = await storage.run(context, render);
71
- } else {
72
- const state = getComponentRenderContextState();
73
- state.contextStack.push(context);
74
- try {
75
- value = await render();
76
- } finally {
77
- state.contextStack.pop();
78
- }
79
- }
80
- return {
81
- value,
82
- graphContext: {
83
- propsByRef: context.propsByRef,
84
- slotChildrenByRef: context.slotChildrenByRef
85
- }
86
- };
87
- }
88
- export {
89
- createNodeId,
90
- createPropsRef,
91
- createSlotRef,
92
- getComponentRenderContext,
93
- runWithComponentRenderContext
94
- };
@@ -1,33 +0,0 @@
1
- import type { ComponentMarker } from './component-marker.js';
2
- import type { ComponentGraph } from './component-graph.js';
3
- /**
4
- * Render result returned by a graph node resolver.
5
- *
6
- * `html` is inserted in place of the corresponding marker token.
7
- */
8
- export type GraphNodeRenderResult = {
9
- html: string;
10
- };
11
- /**
12
- * Callback used to render one marker node during graph execution.
13
- *
14
- * Resolver implementations may call integration-specific `renderComponent`
15
- * and can use closure state to wire child output into parent render calls.
16
- */
17
- export type GraphNodeResolver = (marker: ComponentMarker) => Promise<GraphNodeRenderResult>;
18
- /**
19
- * Resolves all markers in bottom-up order based on computed graph levels.
20
- *
21
- * Child nodes are resolved first so parent slot content can consume already
22
- * resolved child HTML in subsequent resolver invocations.
23
- *
24
- * Nodes discovered from serialized props may not exist as literal marker tokens in
25
- * `inputHtml`. They still resolve so parent nodes can consume their stitched child
26
- * HTML, while replacement in the outer HTML buffer remains a no-op.
27
- *
28
- * @param inputHtml HTML containing marker tokens.
29
- * @param graph Precomputed marker graph with topological levels.
30
- * @param resolver Async callback that renders each marker node.
31
- * @returns HTML with resolved markers replaced.
32
- */
33
- export declare function resolveComponentGraph(inputHtml: string, graph: ComponentGraph, resolver: GraphNodeResolver): Promise<string>;
@@ -1,30 +0,0 @@
1
- function escapeRegex(value) {
2
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
- }
4
- function replaceMarkerByNodeId(html, nodeId, replacement) {
5
- const pattern = `<eco-marker[^>]*data-eco-node-id="${escapeRegex(nodeId)}"[^>]*><\\/eco-marker>`;
6
- const markerRegex = new RegExp(pattern);
7
- return html.replace(markerRegex, replacement);
8
- }
9
- async function resolveComponentGraph(inputHtml, graph, resolver) {
10
- let html = inputHtml;
11
- const markersById = /* @__PURE__ */ new Map();
12
- for (const node of graph.nodes.values()) {
13
- markersById.set(node.nodeId, node);
14
- }
15
- const levels = [...graph.levels].reverse();
16
- for (const level of levels) {
17
- for (const nodeId of level) {
18
- const marker = markersById.get(nodeId);
19
- if (!marker) {
20
- continue;
21
- }
22
- const result = await resolver(marker);
23
- html = replaceMarkerByNodeId(html, nodeId, result.html);
24
- }
25
- }
26
- return html;
27
- }
28
- export {
29
- resolveComponentGraph
30
- };