@ecopages/ecopages-jsx 0.2.0-rc.8 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.0-rc.8",
3
+ "version": "0.2.0",
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.8",
37
+ "@ecopages/mdx": "0.2.0",
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.8",
42
+ "@ecopages/core": "0.2.0",
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
  }
@@ -19,6 +19,5 @@ export interface CreateEcopagesJsxMdxLoaderPluginOptions {
19
19
  projectRoot: string;
20
20
  }
21
21
  export declare const createMdxLoaderPlugin: (options: CreateEcopagesJsxMdxLoaderPluginOptions) => EcoBuildPlugin;
22
- export declare const registerBunMdxPlugin: (options: CreateEcopagesJsxMdxLoaderPluginOptions) => Promise<void>;
23
22
  export declare const isMdxFile: (filePath: string, extensions: string[]) => boolean;
24
23
  export declare const normalizeMdxPageModule: (file: string, module: EcopagesJsxMdxPageModule) => EcopagesJsxMdxPageModule;
@@ -18,18 +18,6 @@ export const createMdxLoaderPlugin = (options) => createMdxLoaderPluginCore({
18
18
  loader: 'js',
19
19
  projectRoot: options.projectRoot,
20
20
  });
21
- export const registerBunMdxPlugin = async (options) => {
22
- if (typeof Bun === 'undefined') {
23
- return;
24
- }
25
- const plugin = createMdxLoaderPlugin(options);
26
- Bun.plugin({
27
- name: plugin.name,
28
- setup(build) {
29
- return plugin.setup(build);
30
- },
31
- });
32
- };
33
21
  export const isMdxFile = (filePath, extensions) => {
34
22
  return extensions.some((ext) => filePath.endsWith(ext));
35
23
  };
@@ -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
  /**
@@ -34,7 +33,7 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
34
33
  private isJsxSerializableForeignChild;
35
34
  /**
36
35
  * Re-renders queued JSX children inside the owning renderer so nested custom
37
- * elements and queued foreign subtrees contribute assets to the same frame.
36
+ * elements and queued foreign subtrees resolve here rather than as opaque markup.
38
37
  */
39
38
  private renderQueuedForeignSubtreeChildren;
40
39
  protected createForeignChildRuntime(options: {
@@ -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,14 +1,13 @@
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
- import { createCustomElementSsrPreloadEntrypointResolver } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-ssr-preload-entrypoint';
10
+ import { createSharedServerModuleImporter } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-server-module-importer';
12
11
  /**
13
12
  * Local Ecopages renderer for JSX templates in the docs app.
14
13
  *
@@ -18,7 +17,6 @@ import { createCustomElementSsrPreloadEntrypointResolver } from '@ecopages/core/
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
  /**
@@ -75,26 +73,15 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
75
73
  }
76
74
  /**
77
75
  * Re-renders queued JSX children inside the owning renderer so nested custom
78
- * elements and queued foreign subtrees contribute assets to the same frame.
76
+ * elements and queued foreign subtrees resolve here rather than as opaque markup.
79
77
  */
80
78
  async renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken) {
81
79
  if (children === undefined) {
82
- return { assets: [] };
80
+ return {};
83
81
  }
84
- let assets = [];
85
- let html;
86
- if (typeof children === 'string') {
87
- html = children;
88
- }
89
- else {
90
- const renderedChildren = await this.renderJsx(children);
91
- html = renderedChildren.html;
92
- assets = renderedChildren.assets;
93
- }
94
- html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken);
82
+ const html = typeof children === 'string' ? children : await this.renderJsx(children);
95
83
  return {
96
- assets,
97
- html,
84
+ html: await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken),
98
85
  };
99
86
  }
100
87
  createForeignChildRuntime(options) {
@@ -124,26 +111,25 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
124
111
  runtimeOrigin,
125
112
  });
126
113
  this.mdxExtensions = jsxConfig?.mdxExtensions ?? ['.mdx'];
127
- this.renderSession = new EcopagesJsxRenderSession((assets) => this.htmlTransformer.dedupeProcessedAssets(assets));
128
114
  const radiantSsrEnabled = jsxConfig?.radiantSsrEnabled ?? false;
129
115
  this.radiantSsrPolicy = jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(radiantSsrEnabled);
130
116
  const preferSourceImports = typeof Bun !== 'undefined';
131
117
  this.ssrScriptPreloader = new CustomElementScriptPreloader({
132
118
  cacheScope: CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.ecopagesJsx,
133
- /**
134
- * @remarks
135
- * Node production builds evaluate registration scripts through the
136
- * asset pipeline during render. Preloading through the app module loader
137
- * registers Radiant hosts in an isolated graph and breaks SSR.
138
- */
139
- enabled: radiantSsrEnabled && preferSourceImports,
119
+ collectForIntegration: ECOPAGES_JSX_PLUGIN_NAME,
120
+ enabled: radiantSsrEnabled,
140
121
  logLabel: 'ecopages-jsx',
141
122
  preferSourceImports,
142
123
  resolveDependencyPath: (componentDir, sourcePath) => this.resolveDependencyPath(componentDir, sourcePath),
143
- resolvePreloadEntrypoint: createCustomElementSsrPreloadEntrypointResolver({
144
- preferSourceImports,
145
- processDependencies: this.assetProcessingService?.processDependencies?.bind(this.assetProcessingService),
146
- }),
124
+ /**
125
+ * @remarks
126
+ * Bun imports the source file through `preferSourceImports`. Node must
127
+ * not: dev asset output is still the `.tsx` source, and a browser bundle
128
+ * is a second Radiant graph. The page server-module loader transpiles
129
+ * the script and externalizes packages, so `customElements.define` hits
130
+ * the same module instances the page already loaded.
131
+ */
132
+ importServerModule: preferSourceImports ? undefined : createSharedServerModuleImporter(this.appConfig),
147
133
  });
148
134
  }
149
135
  /** Returns whether the requested page file should be treated as MDX. */
@@ -164,7 +150,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
164
150
  options.HtmlTemplate,
165
151
  ...(options.resolvedPageDependencyComponents ?? []),
166
152
  ]);
167
- return await this.renderSession.withActiveScope(async () => {
153
+ return await withEjsxHmrOwnershipScope(async () => {
168
154
  try {
169
155
  const result = await this.renderPageWithDocumentShell({
170
156
  page: {
@@ -191,7 +177,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
191
177
  metadata: options.metadata,
192
178
  pageProps: options.pageProps ?? {},
193
179
  });
194
- this.recordHmrOwnership([
180
+ recordEjsxHmrOwnership([
195
181
  options.Page,
196
182
  options.Layout,
197
183
  options.HtmlTemplate,
@@ -208,8 +194,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
208
194
  async renderComponent(input) {
209
195
  return await this.withPreparedRadiantRuntime(async () => {
210
196
  await this.ssrScriptPreloader.preloadSsrScripts([input.component]);
211
- return await this.renderSession.withActiveScope(async () => {
212
- const assetFrame = this.renderSession.beginCollectedAssetFrame();
197
+ return await withEjsxHmrOwnershipScope(async () => {
213
198
  try {
214
199
  if (typeof input.component !== 'function') {
215
200
  throw new TypeError('JSX renderer expected a callable component.');
@@ -224,34 +209,17 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
224
209
  : input.children,
225
210
  };
226
211
  const content = await this.withCustomElementRenderHook(() => component(componentProps));
227
- const rendered = await this.renderJsx(content);
228
- const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
229
- currentIntegrationName: this.name,
230
- html: rendered.html,
231
- runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(input, getForeignSubtreeResolutionContextKey(this.name)),
232
- queueLabel: 'Ecopages JSX',
233
- getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
234
- appConfig: this.appConfig,
235
- runtimeOrigin: this.runtimeOrigin,
236
- currentIntegrationName: this.name,
237
- currentRenderer: this,
238
- integrationName,
239
- cache: rendererCache,
240
- }),
241
- applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
242
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
243
- renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken),
244
- });
212
+ const html = await this.renderJsx(content);
213
+ const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtrees(html, this.getQueuedForeignSubtreeContext(input), (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken));
245
214
  const componentAssets = input.component.config?.dependencies &&
246
215
  typeof this.assetProcessingService?.processDependencies === 'function'
247
216
  ? await this.processComponentDependencies([input.component])
248
217
  : [];
249
218
  const assets = this.htmlTransformer.dedupeProcessedAssets([
250
- ...this.renderSession.endCollectedAssetFrame(assetFrame),
251
219
  ...queuedForeignSubtreeResolution.assets,
252
220
  ...componentAssets,
253
221
  ]);
254
- this.recordHmrOwnership([input.component]);
222
+ recordEjsxHmrOwnership([input.component]);
255
223
  return this.finalizeIslandComponentRender(input, {
256
224
  html: queuedForeignSubtreeResolution.html,
257
225
  canAttachAttributes: true,
@@ -261,7 +229,6 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
261
229
  });
262
230
  }
263
231
  catch (error) {
264
- this.renderSession.endCollectedAssetFrame(assetFrame);
265
232
  throw this.createRenderError('Error rendering component', error);
266
233
  }
267
234
  });
@@ -272,7 +239,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
272
239
  const viewComponent = view;
273
240
  const layouts = viewComponent.config?.layouts;
274
241
  await this.ssrScriptPreloader.preloadSsrScripts([view, ...(layouts ?? [])]);
275
- return await this.renderSession.withActiveScope(async () => {
242
+ return await withEjsxHmrOwnershipScope(async () => {
276
243
  try {
277
244
  if (typeof view !== 'function') {
278
245
  throw new TypeError('JSX renderer expected a callable view component.');
@@ -283,7 +250,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
283
250
  ctx,
284
251
  layout: layouts?.[layouts.length - 1],
285
252
  });
286
- this.recordHmrOwnership([view]);
253
+ recordEjsxHmrOwnership([view]);
287
254
  return response;
288
255
  }
289
256
  catch (error) {
@@ -293,13 +260,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
293
260
  });
294
261
  }
295
262
  async renderJsx(value) {
296
- const collectedAssets = [];
297
- const html = await this.withCustomElementRenderHook(() => renderToString(value));
298
- const dedupedAssets = this.renderSession.recordCollectedAssets(collectedAssets);
299
- return {
300
- assets: dedupedAssets,
301
- html,
302
- };
263
+ return await this.withCustomElementRenderHook(() => renderToString(value));
303
264
  }
304
265
  async withCustomElementRenderHook(render) {
305
266
  return await this.radiantSsrPolicy.withRuntime(() => withServerCustomElementRenderHook(this.createIntrinsicCustomElementRenderHook(), render));
@@ -313,12 +274,4 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
313
274
  return instance ? this.radiantSsrPolicy.renderIntrinsicElementMarkup(instance) : undefined;
314
275
  };
315
276
  }
316
- /**
317
- * Records the source files that produced the current render so
318
- * {@link EcopagesJsxHmrStrategy} can match later watcher events without
319
- * re-walking the component tree.
320
- */
321
- recordHmrOwnership(components) {
322
- this.renderSession.mergeHmrOwnership(components);
323
- }
324
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?;
@@ -39,17 +38,14 @@ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable>
39
38
  */
40
39
  getHmrStrategy(): HmrStrategy | undefined;
41
40
  /**
42
- * Registers MDX tooling and completes the base integration setup.
41
+ * Ensures the MDX loader exists, then completes the base integration setup.
42
+ *
43
+ * @remarks
44
+ * Bun registers `integration.plugins` after `setup()` returns. Calling
45
+ * `Bun.plugin` here would install the same MDX loader twice.
43
46
  */
44
47
  setup(): Promise<void>;
45
48
  private ensureMdxLoaderPlugin;
46
- /**
47
- * Registers Bun's MDX loader at runtime setup time.
48
- *
49
- * Build-time contribution collection may run where Bun is absent, so
50
- * this hook stays isolated from manifest preparation.
51
- */
52
- private registerMdxBunPlugin;
53
49
  }
54
50
  /**
55
51
  * Creates the JSX integration plugin with resolved defaults.
@@ -2,7 +2,7 @@ import { IntegrationPlugin, } from '@ecopages/core/plugins/integration-plugin';
2
2
  import { AssetFactory } from '@ecopages/core/services/asset-processing-service';
3
3
  import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
4
4
  import { RADIANT_INSTALL_HYDRATOR_FILEPATH } from './resolve-radiant-install-hydrator.js';
5
- import { appendMdxExtensions, createMdxLoaderPlugin, registerBunMdxPlugin, resolveMdxCompilerOptions, } from './ecopages-jsx-mdx.js';
5
+ import { appendMdxExtensions, createMdxLoaderPlugin, resolveMdxCompilerOptions, } from './ecopages-jsx-mdx.js';
6
6
  import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
7
7
  import { EcopagesJsxHmrStrategy } from './ecopages-jsx-hmr-strategy.js';
8
8
  import { EcopagesJsxDevTransformContributor } from './dev-transform/ecopages-jsx-dev-transform-contributor.js';
@@ -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) {
@@ -137,17 +134,18 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
137
134
  });
138
135
  }
139
136
  /**
140
- * Registers MDX tooling and completes the base integration setup.
137
+ * Ensures the MDX loader exists, then completes the base integration setup.
138
+ *
139
+ * @remarks
140
+ * Bun registers `integration.plugins` after `setup()` returns. Calling
141
+ * `Bun.plugin` here would install the same MDX loader twice.
141
142
  */
142
143
  async setup() {
143
144
  this.ensureMdxLoaderPlugin();
144
- if (typeof Bun !== 'undefined' && this.mdxEnabled && this.mdxCompilerOptions) {
145
- await this.registerMdxBunPlugin();
146
- }
147
145
  await super.setup();
148
146
  }
149
147
  ensureMdxLoaderPlugin() {
150
- if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
148
+ if (!this.mdxCompilerOptions || this.mdxLoaderPlugin) {
151
149
  return;
152
150
  }
153
151
  if (!this.appConfig?.rootDir) {
@@ -159,25 +157,6 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
159
157
  projectRoot: this.appConfig.rootDir,
160
158
  });
161
159
  }
162
- /**
163
- * Registers Bun's MDX loader at runtime setup time.
164
- *
165
- * Build-time contribution collection may run where Bun is absent, so
166
- * this hook stays isolated from manifest preparation.
167
- */
168
- async registerMdxBunPlugin() {
169
- if (typeof Bun === 'undefined' || !this.mdxCompilerOptions) {
170
- return;
171
- }
172
- if (!this.appConfig) {
173
- throw new Error('[EcopagesJsxPlugin] Cannot register Bun MDX plugin: appConfig is not set');
174
- }
175
- await registerBunMdxPlugin({
176
- compilerOptions: this.mdxCompilerOptions,
177
- extensions: this.mdxExtensions,
178
- projectRoot: this.appConfig.rootDir,
179
- });
180
- }
181
160
  }
182
161
  /**
183
162
  * Creates the JSX integration plugin with resolved defaults.
@@ -1,4 +1,4 @@
1
- import { rapidhash } from '@ecopages/core/utils/hash';
1
+ import { rapidhash } from '@ecopages/core/hash';
2
2
  import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
3
3
  /** Shared grouped-build id prefix for page-owned content scripts. */
4
4
  const PAGE_OWNED_CONTENT_SCRIPT_BUNDLE_PREFIX = 'ecopages';
@@ -1,42 +0,0 @@
1
- import type { EcoComponent } from '@ecopages/core';
2
- import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
3
- export declare const ECOPAGES_JSX_SSR_RENDER_STATE_KEY: unique symbol;
4
- /**
5
- * Tracks JSX SSR asset-collection state for one active render flow.
6
- *
7
- * @remarks
8
- * The renderer still mirrors this state into `@ecopages/jsx/server` so nested
9
- * JSX renders and existing SSR-scope probes can observe it, but it also keeps
10
- * an internal async-local copy. That fallback avoids losing renderer state when
11
- * host runtimes such as Vite end up loading separate module identities for the
12
- * JSX server helpers.
13
- */
14
- export declare class EcopagesJsxRenderSession {
15
- private readonly dedupeProcessedAssets;
16
- constructor(dedupeProcessedAssets: (assets: ProcessedAsset[]) => ProcessedAsset[]);
17
- /**
18
- * Runs one render inside the active session scope.
19
- *
20
- * @remarks
21
- * When a render is already active, the current session state is reused and
22
- * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
23
- * the same asset frame stack. When no session exists yet, a new state is
24
- * created and published through both the internal async-local store and the
25
- * JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
26
- * published once when that scope completes.
27
- */
28
- withActiveScope<T>(render: () => T | Promise<T>): Promise<T>;
29
- beginCollectedAssetFrame(): ProcessedAsset[];
30
- endCollectedAssetFrame(frame: ProcessedAsset[]): ProcessedAsset[];
31
- mergeHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
32
- recordCollectedAssets(collectedAssets: ProcessedAsset[]): ProcessedAsset[];
33
- /**
34
- * Resolves the current render-session state from the local async scope first.
35
- *
36
- * @remarks
37
- * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
38
- * that still inspects the public SSR scope directly, while the async-local
39
- * store remains the authoritative source for renderer-owned bookkeeping.
40
- */
41
- private getState;
42
- }
@@ -1,111 +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 asset-collection 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
- dedupeProcessedAssets;
40
- constructor(dedupeProcessedAssets) {
41
- this.dedupeProcessedAssets = dedupeProcessedAssets;
42
- }
43
- /**
44
- * Runs one render inside the active session scope.
45
- *
46
- * @remarks
47
- * When a render is already active, the current session state is reused and
48
- * mirrored back into the JSX SSR scope so nested `renderToString()` calls see
49
- * the same asset frame stack. When no session exists yet, a new state is
50
- * created and published through both the internal async-local store and the
51
- * JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
52
- * published once when that scope completes.
53
- */
54
- async withActiveScope(render) {
55
- const activeState = renderStateStorage.getStore();
56
- if (activeState) {
57
- return await withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, activeState, render);
58
- }
59
- const jsxScopeState = getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
60
- if (jsxScopeState) {
61
- ensurePendingHmrFileOwners(jsxScopeState);
62
- return await runWithCommittedHmrOwnership(jsxScopeState, () => renderStateStorage.run(jsxScopeState, () => render()));
63
- }
64
- const state = {
65
- collectedAssetFrames: [],
66
- pendingHmrFileOwners: new Set(),
67
- };
68
- return await runWithCommittedHmrOwnership(state, () => renderStateStorage.run(state, () => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render)));
69
- }
70
- beginCollectedAssetFrame() {
71
- const state = this.getState();
72
- const frame = [];
73
- state.collectedAssetFrames.push(frame);
74
- return frame;
75
- }
76
- endCollectedAssetFrame(frame) {
77
- const activeFrame = this.getState().collectedAssetFrames.pop();
78
- if (!activeFrame || activeFrame !== frame) {
79
- return this.dedupeProcessedAssets(frame);
80
- }
81
- return this.dedupeProcessedAssets(activeFrame);
82
- }
83
- mergeHmrOwnership(components) {
84
- mergeEjsxHmrOwnership(ensurePendingHmrFileOwners(this.getState()), components);
85
- }
86
- recordCollectedAssets(collectedAssets) {
87
- const dedupedAssets = this.dedupeProcessedAssets(collectedAssets);
88
- const state = this.getState();
89
- const activeFrame = state.collectedAssetFrames[state.collectedAssetFrames.length - 1];
90
- if (activeFrame) {
91
- activeFrame.push(...dedupedAssets);
92
- }
93
- return dedupedAssets;
94
- }
95
- /**
96
- * Resolves the current render-session state from the local async scope first.
97
- *
98
- * @remarks
99
- * The fallback read from `@ecopages/jsx/server` keeps compatibility with code
100
- * that still inspects the public SSR scope directly, while the async-local
101
- * store remains the authoritative source for renderer-owned bookkeeping.
102
- */
103
- getState() {
104
- const state = renderStateStorage.getStore() ??
105
- getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
106
- if (!state) {
107
- throw new Error('Ecopages JSX SSR render state is unavailable outside the active render scope.');
108
- }
109
- return state;
110
- }
111
- }