@ecopages/ecopages-jsx 0.2.1-beta.0 → 0.3.0

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.
@@ -4,13 +4,15 @@ import { createMarkupNodeLike } from '@ecopages/jsx';
4
4
  *
5
5
  * @remarks
6
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.
7
+ * renders do not need them. Resolved runtime modules are cached on the policy
8
+ * instance so repeated renders on the same renderer reuse one load without
9
+ * process-wide mutable class state. Light-DOM constructor installation remains
10
+ * process-global and idempotent via {@link ensureRadiantLightDomGlobals}; the
11
+ * Radiant installer also repairs incomplete DOM globals left by another runtime.
10
12
  */
11
13
  export declare class EcopagesJsxRadiantSsrPolicy {
12
- private static runtimeModules;
13
- private static runtimeModulesPromise;
14
+ private runtimeModules;
15
+ private runtimeModulesPromise;
14
16
  private readonly enabled;
15
17
  constructor(enabled: boolean);
16
18
  /**
@@ -27,7 +29,11 @@ export declare class EcopagesJsxRadiantSsrPolicy {
27
29
  * @remarks
28
30
  * The returned node-like wrapper lets the JSX server renderer preserve the
29
31
  * generated host HTML without escaping it back into plain text.
32
+ *
33
+ * The render hook owns the host's JSX children, so `applyChildren` runs inside the host's context providers.
34
+ * A consumer passed as a child of a provider host then resolves that provider in the server HTML instead of
35
+ * rendering its fallback state.
30
36
  */
31
- renderIntrinsicElementMarkup(instance: unknown): ReturnType<typeof createMarkupNodeLike> | undefined;
37
+ renderIntrinsicElementMarkup(instance: unknown, applyChildren?: () => void): ReturnType<typeof createMarkupNodeLike> | undefined;
32
38
  private ensureRuntimeInstalled;
33
39
  }
@@ -1,79 +1,122 @@
1
- import { createMarkupNodeLike } from "@ecopages/jsx";
2
- import { createServerHydrationBindingState, withServerHydrationBindingState } from "@ecopages/jsx/server";
3
- class EcopagesJsxRadiantSsrPolicy {
4
- static runtimeModules;
5
- static runtimeModulesPromise;
6
- enabled;
7
- constructor(enabled) {
8
- this.enabled = enabled;
9
- }
10
- /**
11
- * Ensures the Radiant SSR runtime is installed before a render needs it.
12
- */
13
- async prepareRuntime() {
14
- if (!this.enabled) {
15
- return;
1
+ import { createMarkupNodeLike } from '@ecopages/jsx';
2
+ import { createServerHydrationBindingState, withServerHydrationBindingState } from '@ecopages/jsx/server';
3
+ /**
4
+ * Owns optional Radiant SSR runtime installation for the JSX integration.
5
+ *
6
+ * @remarks
7
+ * Radiant's server bridge and light-DOM shim are loaded lazily because most JSX
8
+ * renders do not need them. Resolved runtime modules are cached on the policy
9
+ * instance so repeated renders on the same renderer reuse one load without
10
+ * process-wide mutable class state. Light-DOM constructor installation remains
11
+ * process-global and idempotent via {@link ensureRadiantLightDomGlobals}; the
12
+ * Radiant installer also repairs incomplete DOM globals left by another runtime.
13
+ */
14
+ export class EcopagesJsxRadiantSsrPolicy {
15
+ runtimeModules;
16
+ runtimeModulesPromise;
17
+ enabled;
18
+ constructor(enabled) {
19
+ this.enabled = enabled;
16
20
  }
17
- await this.ensureRuntimeInstalled();
18
- }
19
- /**
20
- * Runs one render inside Radiant's server runtime when the policy is enabled.
21
- */
22
- async withRuntime(render) {
23
- if (!this.enabled) {
24
- return render();
21
+ /**
22
+ * Ensures the Radiant SSR runtime is installed before a render needs it.
23
+ */
24
+ async prepareRuntime() {
25
+ if (!this.enabled) {
26
+ return;
27
+ }
28
+ await this.ensureRuntimeInstalled();
25
29
  }
26
- const runtimeModules = await EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise;
27
- if (!runtimeModules) {
28
- return render();
30
+ /**
31
+ * Runs one render inside Radiant's server runtime when the policy is enabled.
32
+ */
33
+ async withRuntime(render) {
34
+ if (!this.enabled) {
35
+ return render();
36
+ }
37
+ const runtimeModules = await this.runtimeModulesPromise;
38
+ if (!runtimeModules) {
39
+ return render();
40
+ }
41
+ return runtimeModules.withServerRadiantElementSsrRuntime(render);
29
42
  }
30
- return runtimeModules.withServerRadiantElementSsrRuntime(render);
31
- }
32
- /**
33
- * Converts one Radiant custom-element instance into trusted SSR markup.
34
- *
35
- * @remarks
36
- * The returned node-like wrapper lets the JSX server renderer preserve the
37
- * generated host HTML without escaping it back into plain text.
38
- */
39
- renderIntrinsicElementMarkup(instance) {
40
- const renderBridge = EcopagesJsxRadiantSsrPolicy.runtimeModules?.resolveRadiantElementRenderBridge(instance);
41
- if (!renderBridge) {
42
- return void 0;
43
+ /**
44
+ * Converts one Radiant custom-element instance into trusted SSR markup.
45
+ *
46
+ * @remarks
47
+ * The returned node-like wrapper lets the JSX server renderer preserve the
48
+ * generated host HTML without escaping it back into plain text.
49
+ *
50
+ * The render hook owns the host's JSX children, so `applyChildren` runs inside the host's context providers.
51
+ * A consumer passed as a child of a provider host then resolves that provider in the server HTML instead of
52
+ * rendering its fallback state.
53
+ */
54
+ renderIntrinsicElementMarkup(instance, applyChildren) {
55
+ const runtime = this.runtimeModules;
56
+ if (!runtime?.resolveRadiantElementRenderBridge(instance)) {
57
+ return undefined;
58
+ }
59
+ return createMarkupNodeLike(withServerHydrationBindingState(createServerHydrationBindingState(), () => {
60
+ const service = runtime.createRadiantElementSsrService(instance);
61
+ service.applyLightDom(() => applyChildren?.());
62
+ return service.renderHostToString({ mode: 'hydrate' }, service.getHostAttributes());
63
+ }));
43
64
  }
44
- return createMarkupNodeLike(
45
- withServerHydrationBindingState(
46
- createServerHydrationBindingState(),
47
- () => renderBridge.renderHostToString({
48
- mode: "hydrate"
49
- })
50
- )
51
- );
52
- }
53
- async ensureRuntimeInstalled() {
54
- if (!EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise) {
55
- const radiantLightDomShimEntry = import.meta.resolve("@ecopages/radiant/server/light-dom-shim");
56
- const radiantElementSsrRuntimeModuleUrl = new URL(
57
- "./radiant-element-ssr-bridge.js",
58
- radiantLightDomShimEntry
59
- ).href;
60
- EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise = Promise.all([
61
- import(radiantElementSsrRuntimeModuleUrl),
62
- import(radiantLightDomShimEntry)
63
- ]).then(([radiantElementSsrRuntimeModule, lightDomShimModule2]) => {
64
- const modules = {
65
- installLightDomShim: lightDomShimModule2.installLightDomShim,
66
- resolveRadiantElementRenderBridge: radiantElementSsrRuntimeModule.resolveRadiantElementRenderBridge,
67
- withServerRadiantElementSsrRuntime: radiantElementSsrRuntimeModule.withServerRadiantElementSsrRuntime
68
- };
69
- EcopagesJsxRadiantSsrPolicy.runtimeModules = modules;
70
- return modules;
71
- });
65
+ async ensureRuntimeInstalled() {
66
+ if (!this.runtimeModulesPromise) {
67
+ const radiantLightDomShimEntry = import.meta.resolve('@ecopages/radiant/server/light-dom-shim');
68
+ const radiantElementSsrRuntimeModuleUrl = import.meta
69
+ .resolve('@ecopages/radiant/server/radiant-element-ssr');
70
+ this.runtimeModulesPromise = (async () => {
71
+ const lightDomShimModule = await import(/* @vite-ignore */ radiantLightDomShimEntry);
72
+ ensureRadiantLightDomGlobals(lightDomShimModule.ensureLightDomShim);
73
+ const radiantElementSsrRuntimeModule = (await import(
74
+ /* @vite-ignore */ radiantElementSsrRuntimeModuleUrl));
75
+ const modules = {
76
+ createRadiantElementSsrService: radiantElementSsrRuntimeModule.createRadiantElementSsrService,
77
+ ensureLightDomShim: lightDomShimModule.ensureLightDomShim,
78
+ resolveRadiantElementRenderBridge: radiantElementSsrRuntimeModule.resolveRadiantElementRenderBridge,
79
+ withServerRadiantElementSsrRuntime: radiantElementSsrRuntimeModule.withServerRadiantElementSsrRuntime,
80
+ };
81
+ this.runtimeModules = modules;
82
+ return modules;
83
+ })();
84
+ }
85
+ await this.runtimeModulesPromise;
86
+ const lightDomShimModule = this.runtimeModules;
87
+ if (lightDomShimModule) {
88
+ ensureRadiantLightDomGlobals(lightDomShimModule.ensureLightDomShim);
89
+ }
72
90
  }
73
- const lightDomShimModule = await EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise;
74
- lightDomShimModule.installLightDomShim();
75
- }
76
91
  }
77
- export {
78
- EcopagesJsxRadiantSsrPolicy
79
- };
92
+ /**
93
+ * Installs Radiant light-DOM constructors on `globalThis` once.
94
+ *
95
+ * @remarks
96
+ * Custom-element constructors must be process-global; undoing them between tests
97
+ * is unsafe. Callers that clear globals for isolation must rely on a fresh policy
98
+ * instance so {@link EcopagesJsxRadiantSsrPolicy.prepareRuntime} re-runs the
99
+ * completeness check.
100
+ */
101
+ function ensureRadiantLightDomGlobals(ensureLightDomShim) {
102
+ const window = ensureLightDomShim();
103
+ if (!window) {
104
+ return;
105
+ }
106
+ Object.assign(globalThis, {
107
+ CSS: window.CSS,
108
+ CustomEvent: window.CustomEvent,
109
+ Document: window.Document,
110
+ Element: window.Element,
111
+ Event: window.Event,
112
+ EventTarget: window.EventTarget,
113
+ HTMLScriptElement: window.HTMLScriptElement,
114
+ HTMLElement: window.HTMLElement,
115
+ Node: window.Node,
116
+ document: window.document,
117
+ customElements: window.customElements,
118
+ requestAnimationFrame: window.requestAnimationFrame,
119
+ cancelAnimationFrame: window.cancelAnimationFrame,
120
+ window,
121
+ });
122
+ }
@@ -1,47 +1,12 @@
1
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
2
- import { IntegrationRenderer, type RenderToResponseContext, type RouteModuleLoadOptions } from '@ecopages/core/route-renderer/integration-renderer';
3
- import type { ForeignChildRuntime } from '@ecopages/core/route-renderer/component-render-context';
1
+ import { createIntegrationRenderer, type IntegrationRendererHookFactory } from '@ecopages/core/integration';
4
2
  import { type JsxRenderable } from '@ecopages/jsx';
5
- import { type EcopagesJsxMdxPageModule } from './ecopages-jsx-mdx.js';
6
- import type { EcopagesJsxRendererOptions } from './ecopages-jsx.types.js';
7
- export type { EcopagesJsxRendererConfig, EcopagesJsxRendererOptions } from './ecopages-jsx.types.js';
8
- /**
9
- * Local Ecopages renderer for JSX templates in the docs app.
10
- *
11
- * This keeps the integration scoped to the docs package while supporting
12
- * async page, layout, and html template components on the server.
13
- */
14
- export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderable> {
3
+ import type { EcopagesJsxRendererConfig } from './ecopages-jsx.types.js';
4
+ export type { EcopagesJsxRendererConfig } from './ecopages-jsx.types.js';
5
+ export type EcopagesJsxRenderer = InstanceType<ReturnType<typeof createEcopagesJsxRenderer>>;
6
+ /** Creates the hook-based Ecopages JSX renderer for one plugin configuration. */
7
+ export declare function createEcopagesJsxRenderer(jsxConfig?: EcopagesJsxRendererConfig): ReturnType<typeof createIntegrationRenderer<JsxRenderable>>;
8
+ /** Creates JSX renderer hooks that can also configure a custom renderer subclass. */
9
+ export declare function createEcopagesJsxRendererHookFactory(jsxConfig?: EcopagesJsxRendererConfig): IntegrationRendererHookFactory;
10
+ export declare const EcopagesJsxRenderer: new (options: import("@ecopages/core/integration").IntegrationRendererOptions) => import("@ecopages/core/integration").IntegrationRenderer<JsxRenderable> & {
15
11
  name: string;
16
- private readonly mdxExtensions;
17
- private readonly renderSession;
18
- private readonly radiantSsrPolicy;
19
- private normalizeForeignChildProps;
20
- /**
21
- * Re-renders queued JSX children inside the owning renderer so nested custom
22
- * elements and queued foreign subtrees contribute assets to the same frame.
23
- */
24
- private renderQueuedForeignSubtreeChildren;
25
- /**
26
- * Resolves queued foreign subtrees after JSX has been stringified.
27
- *
28
- * JSX content needs one extra render pass because child foreign subtrees may emit
29
- * additional browser assets while also replacing placeholder tokens.
30
- */
31
- private resolveOwnedForeignSubtreeHtml;
32
- protected createForeignChildRuntime(options: {
33
- renderInput: ComponentRenderInput;
34
- rendererCache: Map<string, IntegrationRenderer<any>>;
35
- }): ForeignChildRuntime;
36
- constructor({ appConfig, assetProcessingService, resolvedIntegrationDependencies, jsxConfig, runtimeOrigin, }: EcopagesJsxRendererOptions);
37
- /** Returns whether the requested page file should be treated as MDX. */
38
- isMdxFile(filePath: string): boolean;
39
- protected importPageFile(file: string, options?: RouteModuleLoadOptions): Promise<EcopagesJsxMdxPageModule>;
40
- render(options: IntegrationRendererRenderOptions<JsxRenderable>): Promise<RouteRendererBody>;
41
- renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
42
- renderToResponse<P = any>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
43
- private renderJsx;
44
- private withCustomElementRenderHook;
45
- private withPreparedRadiantRuntime;
46
- private createIntrinsicCustomElementRenderHook;
47
- }
12
+ };