@ecopages/ecopages-jsx 0.2.0-alpha.26 → 0.2.0-alpha.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,25 +8,29 @@
8
8
 
9
9
  ### Bug Fixes
10
10
 
11
+ - Moved Ecopages JSX intrinsic custom-element asset bookkeeping into the active JSX SSR render scope and reinstalls the Radiant light-DOM shim whenever SSR runtime setup reruns so nested renders stay aligned with the current server render contract.
12
+ - Fixed intrinsic custom-element SSR asset hooks to fall back cleanly when they run after the active JSX render frame has already unwound, avoiding spurious server warnings during docs renders.
11
13
  - Fixed lazy Ecopages JSX custom-element dependencies to stay as standalone assets instead of being folded into page-owned bundles, restoring trigger-driven loading for docs components like `theme-toggle`.
12
14
  - Fixed Ecopages JSX page-owned browser bundles to inline their JSX and Radiant runtime imports while skipping separate intrinsic custom-element script tags when the current component tree already imports those scripts.
13
15
  - Fixed intrinsic custom-element script suppression to honor dependency-declared script ownership instead of relying only on source import scanning.
16
+ - Fixed Radiant custom-element SSR bridging so `prop:` values like array props render through the server host bridge without requiring wrapper-level attribute serialization fallbacks.
14
17
  - Aligned the Ecopages JSX browser runtime bundle with the upstream `@ecopages/jsx` runtime shipped by current alpha releases.
15
18
  - Aligned Ecopages JSX peer dependency ranges with the current `@ecopages/jsx` and `@ecopages/radiant` alpha releases.
16
19
  - Aligned Radiant SSR and hydration wiring with the public `@ecopages/radiant/server/render-component` and `@ecopages/radiant/client/hydrator` entrypoints so JSX apps install an explicit client hydrator bootstrap instead of relying on implicit side effects.
17
20
  - Updated the Ecopages JSX Radiant browser runtime for the `RadiantElement` and `RadiantController` API surface and switched the explicit hydrator bootstrap to `@ecopages/radiant/client/install-hydrator`.
18
21
  - Fixed Radiant SSR page inspection to install the light-DOM shim before JSX page modules are imported outside the normal render pass.
19
22
  - Restored direct `EcopagesJsxPlugin` construction so the exported class still accepts the public plugin options shape.
20
- - Fixed intrinsic custom-element asset discovery so Ecopages JSX registers scripts declared with decorator and function-call `customElement(...)` syntax.
21
- - Fixed the Ecopages JSX browser runtime import map so browser builds no longer expose `@ecopages/jsx/server` through the shared JSX vendor bundle.
23
+ - Aligned Ecopages JSX intrinsic custom-element loading with explicit `dependencies.scripts` ownership instead of implicit tag-to-script discovery.
24
+ - Removed implicit JSX integration-generated intrinsic custom-element browser entries so client custom-element loading now follows the normal asset pipeline.
22
25
  - Fixed the Ecopages JSX browser runtime bundle so Radiant custom-element scripts no longer fail on a duplicate `jsxDEV` export cycle.
23
- - Fixed Ecopages JSX boundary payload compatibility coverage and removed the plugin/renderer integration-name import cycle.
26
+ - Fixed Ecopages JSX foreign-subtree payload compatibility coverage and removed the plugin/renderer integration-name import cycle.
24
27
 
25
28
  ### Refactoring
26
29
 
27
- - Removed the JSX browser import-map asset and folded the Radiant install-hydrator entry into the emitted Radiant vendor bundle.
30
+ - Removed the shared JSX runtime bundle service and browser import-map asset in favor of per-script browser entries that prepend `@ecopages/radiant/client/install-hydrator` only when Radiant SSR is enabled.
28
31
  - Replaced Ecopages JSX renderer static and post-construction configuration with instance-owned renderer wiring and extracted shared plugin and renderer types into a dedicated module.
32
+ - Extracted JSX renderer SSR asset-frame scope handling into a dedicated render-session module.
29
33
 
30
34
  ### Tests
31
35
 
32
- - Added renderer-level coverage for the boundary payload compatibility contract.
36
+ - Added renderer-level coverage for the foreign-subtree payload compatibility contract.
package/README.md CHANGED
@@ -5,10 +5,10 @@ Integration plugin for [@ecopages/jsx](https://www.npmjs.com/package/@ecopages/j
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- bun add @ecopages/ecopages-jsx @ecopages/radiant
8
+ bun add @ecopages/ecopages-jsx @ecopages/jsx @ecopages/radiant
9
9
  ```
10
10
 
11
- `@ecopages/radiant` is a required peer dependency for this package.
11
+ `@ecopages/jsx` and `@ecopages/radiant` are required peer dependencies for this package.
12
12
 
13
13
  ## Usage
14
14
 
@@ -62,6 +62,15 @@ Set `radiant: false` when your JSX pages do not need Radiant SSR or the Radiant
62
62
 
63
63
  The plugin bootstrap is intentionally explicit rather than depending on custom-element modules to install the Radiant hydrator opportunistically.
64
64
 
65
+ For server-side custom-element examples, prefer explicit SSR modes over the older boolean-only contract.
66
+
67
+ ```ts
68
+ new MyElement().renderHostToString({ mode: 'plain' });
69
+ new MyElement().renderHostToString({ mode: 'hydrate' });
70
+ ```
71
+
72
+ The legacy `hydrate: true` option remains compatible, but `mode` is the current SSR contract. Registered intrinsic tags that contain a dash are SSR candidates, and framework-specific host rendering should be adapted through the server custom-element render hook instead of hardcoding framework branches into JSX page code.
73
+
65
74
  ## MDX Support
66
75
 
67
76
  Enable MDX to treat `.mdx` files as JSX routes compiled against the `@ecopages/jsx` runtime.
@@ -86,7 +95,7 @@ export default config;
86
95
 
87
96
  ## Mixed Rendering
88
97
 
89
- Ecopages JSX can own the outer page shell or just the nested boundary. When another integration reaches a JSX-owned boundary, Ecopages hands that boundary back to the JSX renderer so it can serialize the correct output before the outer renderer resumes.
98
+ Ecopages JSX can own the outer page shell or just a nested foreign subtree. When another integration reaches a JSX-owned foreign child, Ecopages hands that foreign subtree back to the JSX renderer so it can serialize the correct output before the outer renderer resumes.
90
99
 
91
100
  Important:
92
101
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.0-alpha.26",
3
+ "version": "0.2.0-alpha.27",
4
4
  "description": "JSX integration plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -21,8 +21,8 @@
21
21
  "vfile": "^6.0.3"
22
22
  },
23
23
  "peerDependencies": {
24
- "@ecopages/core": "0.2.0-alpha.26",
25
- "@ecopages/jsx": "0.3.0-alpha.10",
26
- "@ecopages/radiant": "0.3.0-alpha.10"
24
+ "@ecopages/core": "0.2.0-alpha.27",
25
+ "@ecopages/jsx": "0.3.0-alpha.13",
26
+ "@ecopages/radiant": "0.3.0-alpha.13"
27
27
  }
28
28
  }
@@ -0,0 +1,21 @@
1
+ import type { CompileOptions } from '@mdx-js/mdx';
2
+ import type { EcoComponent, EcoComponentConfig, EcoFunctionComponent, EcoPageFile, GetMetadata } from '@ecopages/core';
3
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
4
+ import type { JsxRenderable } from '@ecopages/jsx';
5
+ import type { EcopagesJsxMdxCompileOptions, EcopagesJsxMdxOptions } from './ecopages-jsx.types.js';
6
+ export type ResolvedMdxCompileOptions = EcopagesJsxMdxCompileOptions & Pick<CompileOptions, 'jsxImportSource' | 'jsxRuntime'>;
7
+ export type AsyncEcoComponent<P = Record<string, unknown>, R = JsxRenderable> = EcoFunctionComponent<P, R | Promise<R>>;
8
+ export type EcopagesJsxMdxPageModule = EcoPageFile<{
9
+ config?: EcoComponentConfig;
10
+ layout?: EcoComponent;
11
+ getMetadata?: GetMetadata;
12
+ }>;
13
+ export declare const createMdxExtensionFilter: (extensions: string[], options?: {
14
+ allowQueryString?: boolean;
15
+ }) => RegExp;
16
+ export declare const appendMdxExtensions: (target: string[], mdxExtensions: string[]) => void;
17
+ export declare const resolveMdxCompilerOptions: (mdxOptions: EcopagesJsxMdxOptions) => ResolvedMdxCompileOptions;
18
+ export declare const createMdxLoaderPlugin: (compilerOptions: ResolvedMdxCompileOptions, extensions: string[]) => EcoBuildPlugin;
19
+ export declare const registerBunMdxPlugin: (compilerOptions: ResolvedMdxCompileOptions, extensions: string[]) => Promise<void>;
20
+ export declare const isMdxFile: (filePath: string, extensions: string[]) => boolean;
21
+ export declare const normalizeMdxPageModule: (file: string, module: EcopagesJsxMdxPageModule) => EcopagesJsxMdxPageModule;
@@ -0,0 +1,113 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { rapidhash } from "@ecopages/core/hash";
4
+ import { VFile } from "vfile";
5
+ import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
6
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
+ const mergePluginLists = (...lists) => {
8
+ const merged = lists.flatMap((list) => list ? [...list] : []);
9
+ return merged.length > 0 ? merged : void 0;
10
+ };
11
+ const createMdxExtensionFilter = (extensions, options) => {
12
+ const escaped = extensions.map(escapeRegex);
13
+ const suffix = options?.allowQueryString ? "(\\?.*)?$" : "$";
14
+ return new RegExp(`(${escaped.join("|")})${suffix}`);
15
+ };
16
+ const appendMdxExtensions = (target, mdxExtensions) => {
17
+ for (const ext of mdxExtensions) {
18
+ if (!target.includes(ext)) {
19
+ target.push(ext);
20
+ }
21
+ }
22
+ };
23
+ const resolveMdxCompilerOptions = (mdxOptions) => {
24
+ const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = mdxOptions;
25
+ const resolved = {
26
+ format: "detect",
27
+ outputFormat: "program",
28
+ ...compilerOptions,
29
+ jsxImportSource: "@ecopages/jsx",
30
+ jsxRuntime: "automatic",
31
+ development: process.env.NODE_ENV === "development"
32
+ };
33
+ const mergedRemark = mergePluginLists(compilerOptions?.remarkPlugins, remarkPlugins);
34
+ const mergedRehype = mergePluginLists(compilerOptions?.rehypePlugins, rehypePlugins);
35
+ const mergedRecma = mergePluginLists(compilerOptions?.recmaPlugins, recmaPlugins);
36
+ if (mergedRemark) resolved.remarkPlugins = mergedRemark;
37
+ if (mergedRehype) resolved.rehypePlugins = mergedRehype;
38
+ if (mergedRecma) resolved.recmaPlugins = mergedRecma;
39
+ return resolved;
40
+ };
41
+ const createMdxLoaderPlugin = (compilerOptions, extensions) => {
42
+ const filter = createMdxExtensionFilter(extensions, { allowQueryString: true });
43
+ return {
44
+ name: "ecopages-jsx-mdx-loader",
45
+ setup(build) {
46
+ build.onLoad({ filter }, async (args) => {
47
+ const { compile } = await import("@mdx-js/mdx");
48
+ const filePath = args.path.includes("?") ? args.path.split("?")[0] : args.path;
49
+ const source = await readFile(filePath, "utf-8");
50
+ const compiled = await compile(new VFile({ value: source, path: filePath }), compilerOptions);
51
+ return {
52
+ contents: String(compiled.value),
53
+ loader: "js",
54
+ resolveDir: path.dirname(filePath)
55
+ };
56
+ });
57
+ }
58
+ };
59
+ };
60
+ const registerBunMdxPlugin = async (compilerOptions, extensions) => {
61
+ if (typeof Bun === "undefined") {
62
+ return;
63
+ }
64
+ const filter = createMdxExtensionFilter(extensions);
65
+ Bun.plugin({
66
+ name: "ecopages-jsx-mdx",
67
+ setup(build) {
68
+ build.onLoad({ filter }, async (args) => {
69
+ const { compile } = await import("@mdx-js/mdx");
70
+ const source = await readFile(args.path, "utf-8");
71
+ const compiled = await compile(new VFile({ value: source, path: args.path }), compilerOptions);
72
+ return { contents: String(compiled.value), loader: "js" };
73
+ });
74
+ }
75
+ });
76
+ };
77
+ const isMdxFile = (filePath, extensions) => {
78
+ return extensions.some((ext) => filePath.endsWith(ext));
79
+ };
80
+ const normalizeMdxPageModule = (file, module) => {
81
+ if (typeof module.default !== "function") {
82
+ throw new TypeError("MDX file must export a callable default component.");
83
+ }
84
+ const Page = module.default;
85
+ const normalizedConfig = {
86
+ ...module.config ?? Page.config ?? {},
87
+ ...module.layout ? { layout: module.layout } : {},
88
+ __eco: module.config?.__eco ?? Page.config?.__eco ?? {
89
+ id: String(rapidhash(file)),
90
+ file,
91
+ integration: ECOPAGES_JSX_PLUGIN_NAME
92
+ }
93
+ };
94
+ const wrappedPage = async (props) => await Page(props);
95
+ wrappedPage.config = normalizedConfig;
96
+ if (module.getMetadata ?? Page.metadata) {
97
+ wrappedPage.metadata = module.getMetadata ?? Page.metadata;
98
+ }
99
+ return {
100
+ ...module,
101
+ default: wrappedPage,
102
+ config: normalizedConfig
103
+ };
104
+ };
105
+ export {
106
+ appendMdxExtensions,
107
+ createMdxExtensionFilter,
108
+ createMdxLoaderPlugin,
109
+ isMdxFile,
110
+ normalizeMdxPageModule,
111
+ registerBunMdxPlugin,
112
+ resolveMdxCompilerOptions
113
+ };
@@ -0,0 +1,33 @@
1
+ import { createMarkupNodeLike } from '@ecopages/jsx';
2
+ /**
3
+ * Owns optional Radiant SSR runtime installation for the JSX integration.
4
+ *
5
+ * @remarks
6
+ * Radiant's server bridge and light-DOM shim are loaded lazily because most JSX
7
+ * renders do not need them. Once enabled, the resolved runtime modules are
8
+ * cached statically so repeated page and component renders do not keep paying
9
+ * module-resolution or shim-installation costs.
10
+ */
11
+ export declare class EcopagesJsxRadiantSsrPolicy {
12
+ private static runtimeModules;
13
+ private static runtimeModulesPromise;
14
+ private readonly enabled;
15
+ constructor(enabled: boolean);
16
+ /**
17
+ * Ensures the Radiant SSR runtime is installed before a render needs it.
18
+ */
19
+ prepareRuntime(): Promise<void>;
20
+ /**
21
+ * Runs one render inside Radiant's server runtime when the policy is enabled.
22
+ */
23
+ withRuntime<T>(render: () => T): Promise<T>;
24
+ /**
25
+ * Converts one Radiant custom-element instance into trusted SSR markup.
26
+ *
27
+ * @remarks
28
+ * The returned node-like wrapper lets the JSX server renderer preserve the
29
+ * generated host HTML without escaping it back into plain text.
30
+ */
31
+ renderIntrinsicElementMarkup(instance: unknown): ReturnType<typeof createMarkupNodeLike> | undefined;
32
+ private ensureRuntimeInstalled;
33
+ }
@@ -0,0 +1,80 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import { createMarkupNodeLike } from "@ecopages/jsx";
4
+ import { isServerRenderHydrationActive } from "@ecopages/jsx/server";
5
+ class EcopagesJsxRadiantSsrPolicy {
6
+ static runtimeModules;
7
+ static runtimeModulesPromise;
8
+ enabled;
9
+ constructor(enabled) {
10
+ this.enabled = enabled;
11
+ }
12
+ /**
13
+ * Ensures the Radiant SSR runtime is installed before a render needs it.
14
+ */
15
+ async prepareRuntime() {
16
+ if (!this.enabled) {
17
+ return;
18
+ }
19
+ await this.ensureRuntimeInstalled();
20
+ }
21
+ /**
22
+ * Runs one render inside Radiant's server runtime when the policy is enabled.
23
+ */
24
+ async withRuntime(render) {
25
+ if (!this.enabled) {
26
+ return render();
27
+ }
28
+ const runtimeModules = await EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise;
29
+ if (!runtimeModules) {
30
+ return render();
31
+ }
32
+ return runtimeModules.withServerRadiantElementSsrRuntime(render);
33
+ }
34
+ /**
35
+ * Converts one Radiant custom-element instance into trusted SSR markup.
36
+ *
37
+ * @remarks
38
+ * The returned node-like wrapper lets the JSX server renderer preserve the
39
+ * generated host HTML without escaping it back into plain text.
40
+ */
41
+ renderIntrinsicElementMarkup(instance) {
42
+ const renderBridge = EcopagesJsxRadiantSsrPolicy.runtimeModules?.resolveRadiantElementRenderBridge(instance);
43
+ if (!renderBridge) {
44
+ return void 0;
45
+ }
46
+ return createMarkupNodeLike(
47
+ renderBridge.renderHostToString({
48
+ mode: isServerRenderHydrationActive() ? "hydrate" : "plain"
49
+ })
50
+ );
51
+ }
52
+ async ensureRuntimeInstalled() {
53
+ if (!EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise) {
54
+ const radiantLightDomShimEntry = fileURLToPath(
55
+ import.meta.resolve("@ecopages/radiant/server/light-dom-shim")
56
+ );
57
+ const radiantPackageRoot = path.dirname(path.dirname(path.dirname(radiantLightDomShimEntry)));
58
+ const radiantElementSsrRuntimeModuleUrl = pathToFileURL(
59
+ path.join(radiantPackageRoot, "dist/server/radiant-element-ssr-bridge.js")
60
+ ).href;
61
+ EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise = Promise.all([
62
+ import(radiantElementSsrRuntimeModuleUrl),
63
+ import("@ecopages/radiant/server/light-dom-shim")
64
+ ]).then(([radiantElementSsrRuntimeModule, lightDomShimModule2]) => {
65
+ const modules = {
66
+ installLightDomShim: lightDomShimModule2.installLightDomShim,
67
+ resolveRadiantElementRenderBridge: radiantElementSsrRuntimeModule.resolveRadiantElementRenderBridge,
68
+ withServerRadiantElementSsrRuntime: radiantElementSsrRuntimeModule.withServerRadiantElementSsrRuntime
69
+ };
70
+ EcopagesJsxRadiantSsrPolicy.runtimeModules = modules;
71
+ return modules;
72
+ });
73
+ }
74
+ const lightDomShimModule = await EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise;
75
+ lightDomShimModule.installLightDomShim();
76
+ }
77
+ }
78
+ export {
79
+ EcopagesJsxRadiantSsrPolicy
80
+ };
@@ -0,0 +1,39 @@
1
+ import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
2
+ export declare const ECOPAGES_JSX_SSR_RENDER_STATE_KEY: unique symbol;
3
+ /**
4
+ * Tracks JSX SSR asset-collection state for one active render flow.
5
+ *
6
+ * @remarks
7
+ * The renderer still mirrors this state into `@ecopages/jsx/server` so nested
8
+ * JSX renders and existing SSR-scope probes can observe it, but it also keeps
9
+ * an internal async-local copy. That fallback avoids losing renderer state when
10
+ * host runtimes such as Vite end up loading separate module identities for the
11
+ * JSX server helpers.
12
+ */
13
+ export declare class EcopagesJsxRenderSession {
14
+ private readonly dedupeProcessedAssets;
15
+ constructor(dedupeProcessedAssets: (assets: ProcessedAsset[]) => ProcessedAsset[]);
16
+ /**
17
+ * Runs one render inside the active session scope.
18
+ *
19
+ * @remarks
20
+ * When a render is already active, the current session state is reused and
21
+ * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
22
+ * the same asset frame stack. When no session exists yet, a new state is
23
+ * created and published through both the internal async-local store and the
24
+ * JSX SSR scope bridge.
25
+ */
26
+ withActiveScope<T>(render: () => T): T;
27
+ beginCollectedAssetFrame(): ProcessedAsset[];
28
+ endCollectedAssetFrame(frame: ProcessedAsset[]): ProcessedAsset[];
29
+ recordCollectedAssets(collectedAssets: ProcessedAsset[]): ProcessedAsset[];
30
+ /**
31
+ * Resolves the current render-session state from the local async scope first.
32
+ *
33
+ * @remarks
34
+ * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
35
+ * that still inspects the public SSR scope directly, while the async-local
36
+ * store remains the authoritative source for renderer-owned bookkeeping.
37
+ */
38
+ private getState;
39
+ }
@@ -0,0 +1,78 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { getActiveSsrScopeValue, withActiveSsrScopeValue } from "@ecopages/jsx/server";
3
+ const ECOPAGES_JSX_SSR_RENDER_STATE_KEY = /* @__PURE__ */ Symbol.for("@ecopages/ecopages-jsx.ssr-render-state");
4
+ const renderStateStorage = new AsyncLocalStorage();
5
+ class EcopagesJsxRenderSession {
6
+ dedupeProcessedAssets;
7
+ constructor(dedupeProcessedAssets) {
8
+ this.dedupeProcessedAssets = dedupeProcessedAssets;
9
+ }
10
+ /**
11
+ * Runs one render inside the active session scope.
12
+ *
13
+ * @remarks
14
+ * When a render is already active, the current session state is reused and
15
+ * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
16
+ * the same asset frame stack. When no session exists yet, a new state is
17
+ * created and published through both the internal async-local store and the
18
+ * JSX SSR scope bridge.
19
+ */
20
+ withActiveScope(render) {
21
+ const activeState = renderStateStorage.getStore();
22
+ if (activeState) {
23
+ return withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, activeState, render);
24
+ }
25
+ const jsxScopeState = getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
26
+ if (jsxScopeState) {
27
+ return renderStateStorage.run(jsxScopeState, () => render());
28
+ }
29
+ const state = {
30
+ collectedAssetFrames: []
31
+ };
32
+ return renderStateStorage.run(
33
+ state,
34
+ () => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render)
35
+ );
36
+ }
37
+ beginCollectedAssetFrame() {
38
+ const state = this.getState();
39
+ const frame = [];
40
+ state.collectedAssetFrames.push(frame);
41
+ return frame;
42
+ }
43
+ endCollectedAssetFrame(frame) {
44
+ const activeFrame = this.getState().collectedAssetFrames.pop();
45
+ if (!activeFrame || activeFrame !== frame) {
46
+ return this.dedupeProcessedAssets(frame);
47
+ }
48
+ return this.dedupeProcessedAssets(activeFrame);
49
+ }
50
+ recordCollectedAssets(collectedAssets) {
51
+ const dedupedAssets = this.dedupeProcessedAssets(collectedAssets);
52
+ const state = this.getState();
53
+ const activeFrame = state.collectedAssetFrames[state.collectedAssetFrames.length - 1];
54
+ if (activeFrame) {
55
+ activeFrame.push(...dedupedAssets);
56
+ }
57
+ return dedupedAssets;
58
+ }
59
+ /**
60
+ * Resolves the current render-session state from the local async scope first.
61
+ *
62
+ * @remarks
63
+ * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
64
+ * that still inspects the public SSR scope directly, while the async-local
65
+ * store remains the authoritative source for renderer-owned bookkeeping.
66
+ */
67
+ getState() {
68
+ const state = renderStateStorage.getStore() ?? getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
69
+ if (!state) {
70
+ throw new Error("Ecopages JSX SSR render state is unavailable outside the active render scope.");
71
+ }
72
+ return state;
73
+ }
74
+ }
75
+ export {
76
+ ECOPAGES_JSX_SSR_RENDER_STATE_KEY,
77
+ EcopagesJsxRenderSession
78
+ };
@@ -1,13 +1,9 @@
1
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComponentConfig, EcoPageFile, GetMetadata, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
1
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
2
2
  import { IntegrationRenderer, type RenderToResponseContext, type RouteModuleLoadOptions } from '@ecopages/core/route-renderer/integration-renderer';
3
3
  import { type JsxRenderable } from '@ecopages/jsx';
4
+ import { type EcopagesJsxMdxPageModule } from './ecopages-jsx-mdx.js';
4
5
  import type { EcopagesJsxRendererOptions } from './ecopages-jsx.types.js';
5
6
  export type { EcopagesJsxRendererConfig, EcopagesJsxRendererOptions } from './ecopages-jsx.types.js';
6
- type MdxPageModule = EcoPageFile<{
7
- config?: EcoComponentConfig;
8
- layout?: EcoComponent;
9
- getMetadata?: GetMetadata;
10
- }>;
11
7
  /**
12
8
  * Local Ecopages renderer for JSX templates in the docs app.
13
9
  *
@@ -16,68 +12,29 @@ type MdxPageModule = EcoPageFile<{
16
12
  */
17
13
  export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderable> {
18
14
  name: string;
19
- private static radiantServerRuntimeInstallPromise;
20
- private static readonly SCRIPT_IMPORT_RE;
21
- private readonly intrinsicCustomElementAssets;
22
- private readonly intrinsicCustomElementScriptFiles;
23
- private collectedAssetFrames;
24
- private importedIntrinsicScriptFrames;
25
15
  private readonly mdxExtensions;
26
- private readonly radiantSsrEnabled;
16
+ private readonly renderSession;
17
+ private readonly radiantSsrPolicy;
27
18
  /**
28
19
  * Re-renders queued JSX children inside the owning renderer so nested custom
29
- * elements and queued foreign boundaries contribute assets to the same frame.
20
+ * elements and queued foreign subtrees contribute assets to the same frame.
30
21
  */
31
- private renderQueuedBoundaryChildren;
22
+ private renderQueuedForeignSubtreeChildren;
32
23
  /**
33
- * Resolves queued foreign boundaries after JSX has been stringified.
24
+ * Resolves queued foreign subtrees after JSX has been stringified.
34
25
  *
35
- * JSX content needs one extra render pass because child boundaries may emit
26
+ * JSX content needs one extra render pass because child foreign subtrees may emit
36
27
  * additional browser assets while also replacing placeholder tokens.
37
28
  */
38
- private resolveOwnedBoundaryHtml;
29
+ private resolveOwnedForeignSubtreeHtml;
39
30
  constructor({ appConfig, assetProcessingService, resolvedIntegrationDependencies, jsxConfig, runtimeOrigin, }: EcopagesJsxRendererOptions);
40
31
  /** Returns whether the requested page file should be treated as MDX. */
41
32
  isMdxFile(filePath: string): boolean;
42
- protected importPageFile(file: string, options?: RouteModuleLoadOptions): Promise<MdxPageModule>;
33
+ protected importPageFile(file: string, options?: RouteModuleLoadOptions): Promise<EcopagesJsxMdxPageModule>;
43
34
  render(options: IntegrationRendererRenderOptions<JsxRenderable>): Promise<RouteRendererBody>;
44
35
  renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
45
- protected createComponentBoundaryRuntime(options: {
46
- boundaryInput: ComponentRenderInput;
47
- rendererCache: Map<string, IntegrationRenderer<any>>;
48
- }): import("@ecopages/core").ComponentBoundaryRuntime;
49
36
  renderToResponse<P = any>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
50
- /**
51
- * Normalizes MDX modules into the same page contract as JSX route modules.
52
- *
53
- * MDX files export page metadata alongside generated component code, so the
54
- * renderer folds those exports back into the Ecopages component shape before
55
- * any layout or document-shell logic runs.
56
- */
57
- private normalizeMdxPageModule;
58
- private beginCollectedAssetFrame;
59
- private endCollectedAssetFrame;
60
37
  private renderJsx;
61
- private renderEcoComponent;
62
- private recordCollectedAssets;
63
- private beginImportedIntrinsicScriptFrame;
64
- private endImportedIntrinsicScriptFrame;
65
- private getActiveImportedIntrinsicScriptFiles;
66
- /**
67
- * Collects intrinsic custom-element script files already owned by the current
68
- * component tree through direct source imports or dependency declarations.
69
- */
70
- private collectImportedIntrinsicScriptFiles;
71
- private extractImportedIntrinsicScriptFiles;
72
- private extractConfiguredDependencyScriptFiles;
73
- private resolveImportedIntrinsicScriptFile;
74
- private ensureRadiantServerRuntimeIfEnabled;
75
- private ensureRadiantServerRuntimeInstalled;
76
- private isFunctionComponent;
77
- private createComponentProps;
78
- private collectComponentAssets;
79
- private invokeComponent;
80
- private createEcoMeta;
81
- private wrapMdxPage;
38
+ private withCustomElementRenderHook;
82
39
  private createIntrinsicCustomElementRenderHook;
83
40
  }