@ecopages/ecopages-jsx 0.2.0-beta.31 → 0.2.0-beta.33

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.
@@ -1,105 +1,112 @@
1
- import { createMarkupNodeLike } from "@ecopages/jsx";
2
- import { createServerHydrationBindingState, withServerHydrationBindingState } from "@ecopages/jsx/server";
3
- class EcopagesJsxRadiantSsrPolicy {
4
- runtimeModules;
5
- 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}.
12
+ */
13
+ export class EcopagesJsxRadiantSsrPolicy {
14
+ runtimeModules;
15
+ runtimeModulesPromise;
16
+ enabled;
17
+ constructor(enabled) {
18
+ this.enabled = enabled;
16
19
  }
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();
20
+ /**
21
+ * Ensures the Radiant SSR runtime is installed before a render needs it.
22
+ */
23
+ async prepareRuntime() {
24
+ if (!this.enabled) {
25
+ return;
26
+ }
27
+ await this.ensureRuntimeInstalled();
25
28
  }
26
- const runtimeModules = await this.runtimeModulesPromise;
27
- if (!runtimeModules) {
28
- return render();
29
+ /**
30
+ * Runs one render inside Radiant's server runtime when the policy is enabled.
31
+ */
32
+ async withRuntime(render) {
33
+ if (!this.enabled) {
34
+ return render();
35
+ }
36
+ const runtimeModules = await this.runtimeModulesPromise;
37
+ if (!runtimeModules) {
38
+ return render();
39
+ }
40
+ return runtimeModules.withServerRadiantElementSsrRuntime(render);
29
41
  }
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 = this.runtimeModules?.resolveRadiantElementRenderBridge(instance);
41
- if (!renderBridge) {
42
- return void 0;
42
+ /**
43
+ * Converts one Radiant custom-element instance into trusted SSR markup.
44
+ *
45
+ * @remarks
46
+ * The returned node-like wrapper lets the JSX server renderer preserve the
47
+ * generated host HTML without escaping it back into plain text.
48
+ */
49
+ renderIntrinsicElementMarkup(instance) {
50
+ const renderBridge = this.runtimeModules?.resolveRadiantElementRenderBridge(instance);
51
+ if (!renderBridge) {
52
+ return undefined;
53
+ }
54
+ return createMarkupNodeLike(withServerHydrationBindingState(createServerHydrationBindingState(), () => renderBridge.renderHostToString({
55
+ mode: 'hydrate',
56
+ })));
43
57
  }
44
- return createMarkupNodeLike(
45
- withServerHydrationBindingState(
46
- createServerHydrationBindingState(),
47
- () => renderBridge.renderHostToString({
48
- mode: "hydrate"
49
- })
50
- )
51
- );
52
- }
53
- async ensureRuntimeInstalled() {
54
- if (!this.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
- this.runtimeModulesPromise = (async () => {
61
- const lightDomShimModule2 = await import(radiantLightDomShimEntry);
62
- ensureRadiantLightDomGlobals(lightDomShimModule2.installLightDomShim);
63
- const radiantElementSsrRuntimeModule = await import(radiantElementSsrRuntimeModuleUrl);
64
- const modules = {
65
- installLightDomShim: lightDomShimModule2.installLightDomShim,
66
- resolveRadiantElementRenderBridge: radiantElementSsrRuntimeModule.resolveRadiantElementRenderBridge,
67
- withServerRadiantElementSsrRuntime: radiantElementSsrRuntimeModule.withServerRadiantElementSsrRuntime
68
- };
69
- this.runtimeModules = modules;
70
- return modules;
71
- })();
58
+ async ensureRuntimeInstalled() {
59
+ if (!this.runtimeModulesPromise) {
60
+ const radiantLightDomShimEntry = import.meta.resolve('@ecopages/radiant/server/light-dom-shim');
61
+ const radiantElementSsrRuntimeModuleUrl = new URL('./radiant-element-ssr-bridge.js', radiantLightDomShimEntry).href;
62
+ this.runtimeModulesPromise = (async () => {
63
+ const lightDomShimModule = await import(radiantLightDomShimEntry);
64
+ ensureRadiantLightDomGlobals(lightDomShimModule.installLightDomShim);
65
+ const radiantElementSsrRuntimeModule = (await import(radiantElementSsrRuntimeModuleUrl));
66
+ const modules = {
67
+ installLightDomShim: lightDomShimModule.installLightDomShim,
68
+ resolveRadiantElementRenderBridge: radiantElementSsrRuntimeModule.resolveRadiantElementRenderBridge,
69
+ withServerRadiantElementSsrRuntime: radiantElementSsrRuntimeModule.withServerRadiantElementSsrRuntime,
70
+ };
71
+ this.runtimeModules = modules;
72
+ return modules;
73
+ })();
74
+ }
75
+ await this.runtimeModulesPromise;
76
+ const lightDomShimModule = this.runtimeModules;
77
+ if (lightDomShimModule) {
78
+ ensureRadiantLightDomGlobals(lightDomShimModule.installLightDomShim);
79
+ }
72
80
  }
73
- await this.runtimeModulesPromise;
74
- const lightDomShimModule = this.runtimeModules;
75
- if (lightDomShimModule) {
76
- ensureRadiantLightDomGlobals(lightDomShimModule.installLightDomShim);
77
- }
78
- }
79
81
  }
82
+ /**
83
+ * Installs Radiant light-DOM constructors on `globalThis` once.
84
+ *
85
+ * @remarks
86
+ * Custom-element constructors must be process-global; undoing them between tests
87
+ * is unsafe. Callers that clear globals for isolation must rely on a fresh policy
88
+ * instance so {@link EcopagesJsxRadiantSsrPolicy.prepareRuntime} re-runs install.
89
+ */
80
90
  function ensureRadiantLightDomGlobals(installLightDomShim) {
81
- if (typeof globalThis.HTMLElement !== "undefined") {
82
- return;
83
- }
84
- const window = installLightDomShim();
85
- if (!window) {
86
- return;
87
- }
88
- Object.assign(globalThis, {
89
- CSS: window.CSS,
90
- CustomEvent: window.CustomEvent,
91
- Document: window.Document,
92
- Element: window.Element,
93
- Event: window.Event,
94
- EventTarget: window.EventTarget,
95
- HTMLScriptElement: window.HTMLScriptElement,
96
- HTMLElement: window.HTMLElement,
97
- Node: window.Node,
98
- document: window.document,
99
- customElements: window.customElements,
100
- window
101
- });
91
+ if (typeof globalThis.HTMLElement !== 'undefined') {
92
+ return;
93
+ }
94
+ const window = installLightDomShim();
95
+ if (!window) {
96
+ return;
97
+ }
98
+ Object.assign(globalThis, {
99
+ CSS: window.CSS,
100
+ CustomEvent: window.CustomEvent,
101
+ Document: window.Document,
102
+ Element: window.Element,
103
+ Event: window.Event,
104
+ EventTarget: window.EventTarget,
105
+ HTMLScriptElement: window.HTMLScriptElement,
106
+ HTMLElement: window.HTMLElement,
107
+ Node: window.Node,
108
+ document: window.document,
109
+ customElements: window.customElements,
110
+ window,
111
+ });
102
112
  }
103
- export {
104
- EcopagesJsxRadiantSsrPolicy
105
- };
@@ -1,78 +1,82 @@
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");
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { getActiveSsrScopeValue, withActiveSsrScopeValue } from '@ecopages/jsx/server';
3
+ export const ECOPAGES_JSX_SSR_RENDER_STATE_KEY = Symbol.for('@ecopages/ecopages-jsx.ssr-render-state');
4
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);
5
+ /**
6
+ * Tracks JSX SSR asset-collection state for one active render flow.
7
+ *
8
+ * @remarks
9
+ * The renderer still mirrors this state into `@ecopages/jsx/server` so nested
10
+ * JSX renders and existing SSR-scope probes can observe it, but it also keeps
11
+ * an internal async-local copy. That fallback avoids losing renderer state when
12
+ * host runtimes such as Vite end up loading separate module identities for the
13
+ * JSX server helpers.
14
+ */
15
+ export class EcopagesJsxRenderSession {
16
+ dedupeProcessedAssets;
17
+ constructor(dedupeProcessedAssets) {
18
+ this.dedupeProcessedAssets = dedupeProcessedAssets;
24
19
  }
25
- const jsxScopeState = getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
26
- if (jsxScopeState) {
27
- return renderStateStorage.run(jsxScopeState, () => render());
20
+ /**
21
+ * Runs one render inside the active session scope.
22
+ *
23
+ * @remarks
24
+ * When a render is already active, the current session state is reused and
25
+ * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
26
+ * the same asset frame stack. When no session exists yet, a new state is
27
+ * created and published through both the internal async-local store and the
28
+ * JSX SSR scope bridge.
29
+ */
30
+ withActiveScope(render) {
31
+ const activeState = renderStateStorage.getStore();
32
+ if (activeState) {
33
+ return withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, activeState, render);
34
+ }
35
+ const jsxScopeState = getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
36
+ if (jsxScopeState) {
37
+ return renderStateStorage.run(jsxScopeState, () => render());
38
+ }
39
+ const state = {
40
+ collectedAssetFrames: [],
41
+ };
42
+ return renderStateStorage.run(state, () => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render));
28
43
  }
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);
44
+ beginCollectedAssetFrame() {
45
+ const state = this.getState();
46
+ const frame = [];
47
+ state.collectedAssetFrames.push(frame);
48
+ return frame;
47
49
  }
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);
50
+ endCollectedAssetFrame(frame) {
51
+ const activeFrame = this.getState().collectedAssetFrames.pop();
52
+ if (!activeFrame || activeFrame !== frame) {
53
+ return this.dedupeProcessedAssets(frame);
54
+ }
55
+ return this.dedupeProcessedAssets(activeFrame);
56
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.");
57
+ recordCollectedAssets(collectedAssets) {
58
+ const dedupedAssets = this.dedupeProcessedAssets(collectedAssets);
59
+ const state = this.getState();
60
+ const activeFrame = state.collectedAssetFrames[state.collectedAssetFrames.length - 1];
61
+ if (activeFrame) {
62
+ activeFrame.push(...dedupedAssets);
63
+ }
64
+ return dedupedAssets;
65
+ }
66
+ /**
67
+ * Resolves the current render-session state from the local async scope first.
68
+ *
69
+ * @remarks
70
+ * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
71
+ * that still inspects the public SSR scope directly, while the async-local
72
+ * store remains the authoritative source for renderer-owned bookkeeping.
73
+ */
74
+ getState() {
75
+ const state = renderStateStorage.getStore() ??
76
+ getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
77
+ if (!state) {
78
+ throw new Error('Ecopages JSX SSR render state is unavailable outside the active render scope.');
79
+ }
80
+ return state;
71
81
  }
72
- return state;
73
- }
74
82
  }
75
- export {
76
- ECOPAGES_JSX_SSR_RENDER_STATE_KEY,
77
- EcopagesJsxRenderSession
78
- };