@ecopages/ecopages-jsx 0.2.0-rc.11 → 0.2.0-rc.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.0-rc.11",
3
+ "version": "0.2.0-rc.14",
4
4
  "description": "JSX integration plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -34,12 +34,12 @@
34
34
  "directory": "packages/integrations/ecopages-jsx"
35
35
  },
36
36
  "dependencies": {
37
- "@ecopages/mdx": "0.2.0-rc.11",
37
+ "@ecopages/mdx": "0.2.0-rc.14",
38
38
  "@mdx-js/mdx": "^3.1.1",
39
39
  "vfile": "^6.0.3"
40
40
  },
41
41
  "peerDependencies": {
42
- "@ecopages/core": "0.2.0-rc.11",
42
+ "@ecopages/core": "0.2.0-rc.14",
43
43
  "@ecopages/jsx": "^0.3.0-rc.9",
44
44
  "@ecopages/radiant": "^0.3.0-rc.9"
45
45
  }
@@ -1,16 +1,20 @@
1
1
  /**
2
- * Tracks the source files that participate in the active SSR render tree.
2
+ * Tracks the source files that produced the most recent JSX render.
3
3
  *
4
4
  * @remarks
5
- * The JSX integration renderer calls {@link updateEjsxHmrOwnership} after each
6
- * page, component, or view render so the HMR strategy can answer
7
- * "does this changed file affect SSR HTML?" without re-walking the tree on
8
- * every watcher event.
5
+ * The JSX renderer runs each page, component, or view render inside
6
+ * {@link withEjsxHmrOwnershipScope} and records its components with
7
+ * {@link recordEjsxHmrOwnership}. `EcopagesJsxHmrStrategy` reads the result to
8
+ * decide whether a changed file affects SSR HTML without walking the component
9
+ * tree on every watcher event.
9
10
  *
10
- * The state is module-scoped because the HMR strategy and renderer live in
11
- * different call sites (server strategy vs SSR renderer) and share no other
12
- * lifecycle anchor. A fresh state is installed only when the source-file set
13
- * changes, so the renderer's per-render hook stays O(1) in the steady state.
11
+ * The state is module-scoped because the renderer and the HMR strategy share no
12
+ * other lifecycle anchor. Publishing replaces it only when the file set changes,
13
+ * so repeated renders of the same page keep the existing state.
14
+ *
15
+ * Only the latest top-level render is kept. With several pages open, a file
16
+ * used only by a page that rendered earlier is not matched until that page
17
+ * renders again.
14
18
  */
15
19
  import type { EcoComponent } from '@ecopages/core';
16
20
  export type EjsxHmrOwnershipState = {
@@ -32,13 +36,21 @@ export declare function getEjsxHmrOwnership(): EjsxHmrOwnershipState;
32
36
  */
33
37
  export declare function updateEjsxHmrOwnership(components: ReadonlyArray<EcoComponent | Partial<EcoComponent> | undefined>): void;
34
38
  /**
35
- * Merges one render tree into a pending ownership set during an active render.
39
+ * Runs one render and publishes the files it recorded once the outermost scope ends.
40
+ *
41
+ * @remarks
42
+ * Nested renders, such as components and foreign subtrees resolved inside a
43
+ * page, join the outer scope, so the published set covers the whole render tree
44
+ * instead of the last nested render.
36
45
  */
37
- export declare function mergeEjsxHmrOwnership(target: Set<string>, components: ReadonlyArray<EcoComponent | undefined>): void;
46
+ export declare function withEjsxHmrOwnershipScope<T>(render: () => T | Promise<T>): Promise<T>;
38
47
  /**
39
- * Publishes the merged ownership set for the completed render.
48
+ * Adds the source files of `components`, their declared dependencies and their
49
+ * layouts to the active render scope.
50
+ *
51
+ * @throws Error when called outside {@link withEjsxHmrOwnershipScope}.
40
52
  */
41
- export declare function publishEjsxHmrOwnership(files: ReadonlySet<string>): void;
53
+ export declare function recordEjsxHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
42
54
  /**
43
55
  * Resets the state to empty. Used by tests and by HMR manager teardown.
44
56
  */
@@ -1,17 +1,22 @@
1
1
  /**
2
- * Tracks the source files that participate in the active SSR render tree.
2
+ * Tracks the source files that produced the most recent JSX render.
3
3
  *
4
4
  * @remarks
5
- * The JSX integration renderer calls {@link updateEjsxHmrOwnership} after each
6
- * page, component, or view render so the HMR strategy can answer
7
- * "does this changed file affect SSR HTML?" without re-walking the tree on
8
- * every watcher event.
5
+ * The JSX renderer runs each page, component, or view render inside
6
+ * {@link withEjsxHmrOwnershipScope} and records its components with
7
+ * {@link recordEjsxHmrOwnership}. `EcopagesJsxHmrStrategy` reads the result to
8
+ * decide whether a changed file affects SSR HTML without walking the component
9
+ * tree on every watcher event.
9
10
  *
10
- * The state is module-scoped because the HMR strategy and renderer live in
11
- * different call sites (server strategy vs SSR renderer) and share no other
12
- * lifecycle anchor. A fresh state is installed only when the source-file set
13
- * changes, so the renderer's per-render hook stays O(1) in the steady state.
11
+ * The state is module-scoped because the renderer and the HMR strategy share no
12
+ * other lifecycle anchor. Publishing replaces it only when the file set changes,
13
+ * so repeated renders of the same page keep the existing state.
14
+ *
15
+ * Only the latest top-level render is kept. With several pages open, a file
16
+ * used only by a page that rendered earlier is not matched until that page
17
+ * renders again.
14
18
  */
19
+ import { AsyncLocalStorage } from 'node:async_hooks';
15
20
  import { rapidhash } from '@ecopages/core/hash';
16
21
  import { collectComponentConfigFilePaths } from '@ecopages/core/route-renderer/page-loading/file-scoped-dependency-components';
17
22
  const EMPTY_STATE = {
@@ -19,6 +24,7 @@ const EMPTY_STATE = {
19
24
  hash: '',
20
25
  };
21
26
  let currentState = EMPTY_STATE;
27
+ const pendingFileOwners = new AsyncLocalStorage();
22
28
  /**
23
29
  * Returns the most recently recorded active render tree.
24
30
  */
@@ -38,17 +44,49 @@ export function updateEjsxHmrOwnership(components) {
38
44
  publishEjsxHmrOwnership(collectFileOwners(components));
39
45
  }
40
46
  /**
41
- * Merges one render tree into a pending ownership set during an active render.
47
+ * Runs one render and publishes the files it recorded once the outermost scope ends.
48
+ *
49
+ * @remarks
50
+ * Nested renders, such as components and foreign subtrees resolved inside a
51
+ * page, join the outer scope, so the published set covers the whole render tree
52
+ * instead of the last nested render.
42
53
  */
43
- export function mergeEjsxHmrOwnership(target, components) {
54
+ export async function withEjsxHmrOwnershipScope(render) {
55
+ if (pendingFileOwners.getStore()) {
56
+ return await render();
57
+ }
58
+ const fileOwners = new Set();
59
+ try {
60
+ return await pendingFileOwners.run(fileOwners, render);
61
+ }
62
+ finally {
63
+ if (fileOwners.size > 0) {
64
+ publishEjsxHmrOwnership(fileOwners);
65
+ }
66
+ }
67
+ }
68
+ /**
69
+ * Adds the source files of `components`, their declared dependencies and their
70
+ * layouts to the active render scope.
71
+ *
72
+ * @throws Error when called outside {@link withEjsxHmrOwnershipScope}.
73
+ */
74
+ export function recordEjsxHmrOwnership(components) {
75
+ const fileOwners = pendingFileOwners.getStore();
76
+ if (!fileOwners) {
77
+ throw new Error('Ecopages JSX HMR ownership can only be recorded inside an active render scope.');
78
+ }
44
79
  for (const file of collectFileOwners(components)) {
45
- target.add(file);
80
+ fileOwners.add(file);
46
81
  }
47
82
  }
48
83
  /**
49
- * Publishes the merged ownership set for the completed render.
84
+ * Resets the state to empty. Used by tests and by HMR manager teardown.
50
85
  */
51
- export function publishEjsxHmrOwnership(files) {
86
+ export function resetEjsxHmrOwnership() {
87
+ currentState = EMPTY_STATE;
88
+ }
89
+ function publishEjsxHmrOwnership(files) {
52
90
  const hash = hashFileSet(files);
53
91
  if (hash === currentState.hash) {
54
92
  return;
@@ -58,12 +96,6 @@ export function publishEjsxHmrOwnership(files) {
58
96
  hash,
59
97
  };
60
98
  }
61
- /**
62
- * Resets the state to empty. Used by tests and by HMR manager teardown.
63
- */
64
- export function resetEjsxHmrOwnership() {
65
- currentState = EMPTY_STATE;
66
- }
67
99
  function collectFileOwners(components) {
68
100
  return collectComponentConfigFilePaths(components, { includeLayouts: true });
69
101
  }
@@ -14,7 +14,6 @@ export type { EcopagesJsxRendererConfig, EcopagesJsxRendererOptions } from './ec
14
14
  export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderable> {
15
15
  name: string;
16
16
  private readonly mdxExtensions;
17
- private readonly renderSession;
18
17
  private readonly radiantSsrPolicy;
19
18
  private readonly ssrScriptPreloader;
20
19
  /**
@@ -52,10 +51,4 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
52
51
  private withCustomElementRenderHook;
53
52
  private withPreparedRadiantRuntime;
54
53
  private createIntrinsicCustomElementRenderHook;
55
- /**
56
- * Records the source files that produced the current render so
57
- * {@link EcopagesJsxHmrStrategy} can match later watcher events without
58
- * re-walking the component tree.
59
- */
60
- private recordHmrOwnership;
61
54
  }
@@ -1,11 +1,10 @@
1
1
  import { IntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/integration-renderer';
2
2
  import { resolveDocumentShellLayouts } from '@ecopages/core/route-renderer/orchestration/document-shell/layout-shell-props.service';
3
- import { getForeignSubtreeResolutionContextKey, resolveOwningIntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/foreign-child/owning-renderer-resolution';
4
3
  import { createMarkupNodeLike } from '@ecopages/jsx';
5
4
  import { renderToString, withServerCustomElementRenderHook } from '@ecopages/jsx/server';
6
5
  import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
7
6
  import { isMdxFile, normalizeMdxPageModule, } from './ecopages-jsx-mdx.js';
8
- import { EcopagesJsxRenderSession } from './ecopages-jsx-render-session.js';
7
+ import { recordEjsxHmrOwnership, withEjsxHmrOwnershipScope } from './ecopages-jsx-hmr-ownership.js';
9
8
  import { EcopagesJsxRadiantSsrPolicy } from './ecopages-jsx-radiant-ssr-policy.js';
10
9
  import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, CustomElementScriptPreloader, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
11
10
  import { createSharedServerModuleImporter } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-server-module-importer';
@@ -18,7 +17,6 @@ import { createSharedServerModuleImporter } from '@ecopages/core/route-renderer/
18
17
  export class EcopagesJsxRenderer extends IntegrationRenderer {
19
18
  name = ECOPAGES_JSX_PLUGIN_NAME;
20
19
  mdxExtensions;
21
- renderSession;
22
20
  radiantSsrPolicy;
23
21
  ssrScriptPreloader;
24
22
  /**
@@ -79,19 +77,11 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
79
77
  */
80
78
  async renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken) {
81
79
  if (children === undefined) {
82
- return { assets: [] };
80
+ return {};
83
81
  }
84
- let html;
85
- if (typeof children === 'string') {
86
- html = children;
87
- }
88
- else {
89
- html = await this.renderJsx(children);
90
- }
91
- html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken);
82
+ const html = typeof children === 'string' ? children : await this.renderJsx(children);
92
83
  return {
93
- assets: [],
94
- html,
84
+ html: await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken),
95
85
  };
96
86
  }
97
87
  createForeignChildRuntime(options) {
@@ -121,7 +111,6 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
121
111
  runtimeOrigin,
122
112
  });
123
113
  this.mdxExtensions = jsxConfig?.mdxExtensions ?? ['.mdx'];
124
- this.renderSession = new EcopagesJsxRenderSession();
125
114
  const radiantSsrEnabled = jsxConfig?.radiantSsrEnabled ?? false;
126
115
  this.radiantSsrPolicy = jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(radiantSsrEnabled);
127
116
  const preferSourceImports = typeof Bun !== 'undefined';
@@ -161,7 +150,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
161
150
  options.HtmlTemplate,
162
151
  ...(options.resolvedPageDependencyComponents ?? []),
163
152
  ]);
164
- return await this.renderSession.withActiveScope(async () => {
153
+ return await withEjsxHmrOwnershipScope(async () => {
165
154
  try {
166
155
  const result = await this.renderPageWithDocumentShell({
167
156
  page: {
@@ -188,7 +177,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
188
177
  metadata: options.metadata,
189
178
  pageProps: options.pageProps ?? {},
190
179
  });
191
- this.recordHmrOwnership([
180
+ recordEjsxHmrOwnership([
192
181
  options.Page,
193
182
  options.Layout,
194
183
  options.HtmlTemplate,
@@ -205,7 +194,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
205
194
  async renderComponent(input) {
206
195
  return await this.withPreparedRadiantRuntime(async () => {
207
196
  await this.ssrScriptPreloader.preloadSsrScripts([input.component]);
208
- return await this.renderSession.withActiveScope(async () => {
197
+ return await withEjsxHmrOwnershipScope(async () => {
209
198
  try {
210
199
  if (typeof input.component !== 'function') {
211
200
  throw new TypeError('JSX renderer expected a callable component.');
@@ -221,23 +210,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
221
210
  };
222
211
  const content = await this.withCustomElementRenderHook(() => component(componentProps));
223
212
  const html = await this.renderJsx(content);
224
- const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
225
- currentIntegrationName: this.name,
226
- html,
227
- runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(input, getForeignSubtreeResolutionContextKey(this.name)),
228
- queueLabel: 'Ecopages JSX',
229
- getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
230
- appConfig: this.appConfig,
231
- runtimeOrigin: this.runtimeOrigin,
232
- currentIntegrationName: this.name,
233
- currentRenderer: this,
234
- integrationName,
235
- cache: rendererCache,
236
- }),
237
- applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
238
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
239
- renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken),
240
- });
213
+ const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtrees(html, this.getQueuedForeignSubtreeContext(input), (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken));
241
214
  const componentAssets = input.component.config?.dependencies &&
242
215
  typeof this.assetProcessingService?.processDependencies === 'function'
243
216
  ? await this.processComponentDependencies([input.component])
@@ -246,7 +219,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
246
219
  ...queuedForeignSubtreeResolution.assets,
247
220
  ...componentAssets,
248
221
  ]);
249
- this.recordHmrOwnership([input.component]);
222
+ recordEjsxHmrOwnership([input.component]);
250
223
  return this.finalizeIslandComponentRender(input, {
251
224
  html: queuedForeignSubtreeResolution.html,
252
225
  canAttachAttributes: true,
@@ -266,7 +239,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
266
239
  const viewComponent = view;
267
240
  const layouts = viewComponent.config?.layouts;
268
241
  await this.ssrScriptPreloader.preloadSsrScripts([view, ...(layouts ?? [])]);
269
- return await this.renderSession.withActiveScope(async () => {
242
+ return await withEjsxHmrOwnershipScope(async () => {
270
243
  try {
271
244
  if (typeof view !== 'function') {
272
245
  throw new TypeError('JSX renderer expected a callable view component.');
@@ -277,7 +250,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
277
250
  ctx,
278
251
  layout: layouts?.[layouts.length - 1],
279
252
  });
280
- this.recordHmrOwnership([view]);
253
+ recordEjsxHmrOwnership([view]);
281
254
  return response;
282
255
  }
283
256
  catch (error) {
@@ -301,12 +274,4 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
301
274
  return instance ? this.radiantSsrPolicy.renderIntrinsicElementMarkup(instance) : undefined;
302
275
  };
303
276
  }
304
- /**
305
- * Records the source files that produced the current render so
306
- * {@link EcopagesJsxHmrStrategy} can match later watcher events without
307
- * re-walking the component tree.
308
- */
309
- recordHmrOwnership(components) {
310
- this.renderSession.mergeHmrOwnership(components);
311
- }
312
277
  }
@@ -10,7 +10,6 @@ export type { EcopagesJsxMdxOptions, EcopagesJsxPluginOptions, EcopagesJsxRender
10
10
  export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable> {
11
11
  renderer: typeof EcopagesJsxRenderer;
12
12
  private includeRadiant;
13
- private mdxEnabled;
14
13
  private mdxCompilerOptions?;
15
14
  private mdxExtensions;
16
15
  private mdxLoaderPlugin?;
@@ -30,7 +30,6 @@ const resolvePluginOptions = (options) => {
30
30
  ...baseConfig,
31
31
  extensions,
32
32
  includeRadiant: radiant ?? true,
33
- mdxEnabled,
34
33
  mdxExtensions,
35
34
  mdxCompilerOptions: mdxEnabled && mdx ? resolveMdxCompilerOptions(mdx) : undefined,
36
35
  };
@@ -39,7 +38,6 @@ const resolvePluginOptions = (options) => {
39
38
  export class EcopagesJsxPlugin extends IntegrationPlugin {
40
39
  renderer = EcopagesJsxRenderer;
41
40
  includeRadiant;
42
- mdxEnabled;
43
41
  mdxCompilerOptions;
44
42
  mdxExtensions;
45
43
  mdxLoaderPlugin;
@@ -59,7 +57,7 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
59
57
  }
60
58
  constructor(options) {
61
59
  const config = resolvePluginOptions(options);
62
- const { extensions, includeRadiant, mdxEnabled, mdxExtensions, mdxCompilerOptions, ...baseConfig } = config;
60
+ const { extensions, includeRadiant, mdxExtensions, mdxCompilerOptions, ...baseConfig } = config;
63
61
  super({
64
62
  name: ECOPAGES_JSX_PLUGIN_NAME,
65
63
  extensions,
@@ -67,7 +65,6 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
67
65
  ...baseConfig,
68
66
  });
69
67
  this.includeRadiant = includeRadiant;
70
- this.mdxEnabled = mdxEnabled;
71
68
  this.mdxExtensions = mdxExtensions;
72
69
  this.mdxCompilerOptions = mdxCompilerOptions;
73
70
  if (this.includeRadiant) {
@@ -148,7 +145,7 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
148
145
  await super.setup();
149
146
  }
150
147
  ensureMdxLoaderPlugin() {
151
- if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
148
+ if (!this.mdxCompilerOptions || this.mdxLoaderPlugin) {
152
149
  return;
153
150
  }
154
151
  if (!this.appConfig?.rootDir) {
@@ -1,36 +0,0 @@
1
- import type { EcoComponent } from '@ecopages/core';
2
- export declare const ECOPAGES_JSX_SSR_RENDER_STATE_KEY: unique symbol;
3
- /**
4
- * Tracks JSX SSR HMR-ownership 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
- /**
15
- * Runs one render inside the active session scope.
16
- *
17
- * @remarks
18
- * When a render is already active, the current session state is reused and
19
- * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
20
- * the same HMR-ownership set. When no session exists yet, a new state is
21
- * created and published through both the internal async-local store and the
22
- * JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
23
- * published once when that scope completes.
24
- */
25
- withActiveScope<T>(render: () => T | Promise<T>): Promise<T>;
26
- mergeHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
27
- /**
28
- * Resolves the current render-session state from the local async scope first.
29
- *
30
- * @remarks
31
- * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
32
- * that still inspects the public SSR scope directly, while the async-local
33
- * store remains the authoritative source for renderer-owned bookkeeping.
34
- */
35
- private getState;
36
- }
@@ -1,84 +0,0 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
- import { getActiveSsrScopeValue, withActiveSsrScopeValue } from '@ecopages/jsx/server';
3
- import { mergeEjsxHmrOwnership, publishEjsxHmrOwnership } from './ecopages-jsx-hmr-ownership.js';
4
- export const ECOPAGES_JSX_SSR_RENDER_STATE_KEY = Symbol.for('@ecopages/ecopages-jsx.ssr-render-state');
5
- const renderStateStorage = new AsyncLocalStorage();
6
- function ensurePendingHmrFileOwners(state) {
7
- if (!state.pendingHmrFileOwners) {
8
- state.pendingHmrFileOwners = new Set();
9
- }
10
- return state.pendingHmrFileOwners;
11
- }
12
- function commitPendingHmrOwnership(state) {
13
- const pending = state.pendingHmrFileOwners;
14
- if (!pending || pending.size === 0) {
15
- return;
16
- }
17
- publishEjsxHmrOwnership(pending);
18
- pending.clear();
19
- }
20
- async function runWithCommittedHmrOwnership(state, render) {
21
- try {
22
- return await render();
23
- }
24
- finally {
25
- commitPendingHmrOwnership(state);
26
- }
27
- }
28
- /**
29
- * Tracks JSX SSR HMR-ownership state for one active render flow.
30
- *
31
- * @remarks
32
- * The renderer still mirrors this state into `@ecopages/jsx/server` so nested
33
- * JSX renders and existing SSR-scope probes can observe it, but it also keeps
34
- * an internal async-local copy. That fallback avoids losing renderer state when
35
- * host runtimes such as Vite end up loading separate module identities for the
36
- * JSX server helpers.
37
- */
38
- export class EcopagesJsxRenderSession {
39
- /**
40
- * Runs one render inside the active session scope.
41
- *
42
- * @remarks
43
- * When a render is already active, the current session state is reused and
44
- * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
45
- * the same HMR-ownership set. When no session exists yet, a new state is
46
- * created and published through both the internal async-local store and the
47
- * JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
48
- * published once when that scope completes.
49
- */
50
- async withActiveScope(render) {
51
- const activeState = renderStateStorage.getStore();
52
- if (activeState) {
53
- return await withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, activeState, render);
54
- }
55
- const jsxScopeState = getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
56
- if (jsxScopeState) {
57
- ensurePendingHmrFileOwners(jsxScopeState);
58
- return await runWithCommittedHmrOwnership(jsxScopeState, () => renderStateStorage.run(jsxScopeState, () => render()));
59
- }
60
- const state = {
61
- pendingHmrFileOwners: new Set(),
62
- };
63
- return await runWithCommittedHmrOwnership(state, () => renderStateStorage.run(state, () => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render)));
64
- }
65
- mergeHmrOwnership(components) {
66
- mergeEjsxHmrOwnership(ensurePendingHmrFileOwners(this.getState()), components);
67
- }
68
- /**
69
- * Resolves the current render-session state from the local async scope first.
70
- *
71
- * @remarks
72
- * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
73
- * that still inspects the public SSR scope directly, while the async-local
74
- * store remains the authoritative source for renderer-owned bookkeeping.
75
- */
76
- getState() {
77
- const state = renderStateStorage.getStore() ??
78
- getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
79
- if (!state) {
80
- throw new Error('Ecopages JSX SSR render state is unavailable outside the active render scope.');
81
- }
82
- return state;
83
- }
84
- }