@ecopages/ecopages-jsx 0.2.0-alpha.12 → 0.2.0-alpha.13

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
@@ -4,18 +4,12 @@
4
4
 
5
5
  ### Features
6
6
 
7
- - Added `@ecopages/ecopages-jsx` integration plugin for server-side JSX rendering via `@ecopages/jsx`
8
- - Added optional `radiant` option (default `true`) to include `@ecopages/radiant` and `@ecopages/signals` vendor bundles; set to `false` for pages that do not use Radiant web components
9
- - Added optional MDX support via `@mdx-js/mdx` compile() when `mdx.enabled: true`
10
- - Added `jsxImportSource: '@ecopages/jsx'` pragma propagation to both SSR and MDX compile pipelines
7
+ - Added the Ecopages JSX integration with optional Radiant runtime support and optional MDX routes compiled against `@ecopages/jsx`.
11
8
 
12
9
  ### Bug Fixes
13
10
 
14
- - Fixed the package contract to require the `@ecopages/radiant` peer dependency because the default JSX integration path depends on Radiant runtime assets.
15
- - Fixed custom Ecopages JSX MDX extension matching to escape full multi-dot extensions in both esbuild and Bun loader filters.
16
- - Fixed the kitchen-sink Ecopages JSX demo to use the shared light-DOM `radiant-counter` host so mixed routes render without leaking Radiant shadow-root SSR behavior into Lit pages.
17
- - Fixed Radiant browser bundles to publish the `@ecopages/jsx/server` import-map entry required by the vendored `@ecopages/radiant` runtime.
18
- - Fixed Ecopages JSX browser bundles to rewrite emitted `@ecopages/jsx` and `@ecopages/radiant` runtime imports to vendor asset URLs and avoid mapping server-only Radiant subpaths into the browser runtime.
19
- - Fixed the vendored Radiant browser runtime to emit exports from curated public subpaths instead of the package root surface.
20
- - Fixed MDX setup to register Bun-only loader hooks only when the integration is running on Bun.
21
- - Fixed deferred template compatibility registration to live on the JSX renderer instead of a side-effect bootstrap module.
11
+ - Fixed mixed-renderer JSX boundary resolution, Radiant/browser runtime asset mapping, required peer dependency wiring, Bun-only MDX loader setup, and multi-dot MDX extension matching.
12
+
13
+ ### Documentation
14
+
15
+ - Updated the README to document JSX route extensions, Radiant defaults, MDX configuration, and mixed-renderer boundary ownership.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ecopages/ecopages-jsx
2
2
 
3
- Integration plugin for [@ecopages/jsx](https://jsr.io/@ecopages/jsx) templates in Ecopages. It provides server-rendered JSX pages, Radiant-backed web component support, and optional MDX route handling.
3
+ Integration plugin for [@ecopages/jsx](https://jsr.io/@ecopages/jsx) templates in Ecopages. Use it when Ecopages JSX should own `.tsx` routes, Radiant-backed web components, or MDX compiled against the `@ecopages/jsx` runtime.
4
4
 
5
5
  ## Installation
6
6
 
@@ -26,6 +26,22 @@ const config = await new ConfigBuilder()
26
26
  export default config;
27
27
  ```
28
28
 
29
+ ## What This Integration Owns
30
+
31
+ - `.tsx` route files by default. Use `extensions` to change the JSX route suffix list.
32
+ - Optional Radiant runtime assets and import-map entries.
33
+ - Optional `.mdx` routes compiled against the `@ecopages/jsx` runtime.
34
+
35
+ ## Route Extensions
36
+
37
+ Use `extensions` when JSX routes should use a custom suffix instead of the default `.tsx`.
38
+
39
+ ```ts
40
+ ecopagesJsxPlugin({
41
+ extensions: ['.page.tsx'],
42
+ });
43
+ ```
44
+
29
45
  ## Radiant Support
30
46
 
31
47
  Radiant runtime bundles are enabled by default so JSX pages can render and hydrate Radiant custom elements.
@@ -51,6 +67,7 @@ const config = await new ConfigBuilder()
51
67
  ecopagesJsxPlugin({
52
68
  mdx: {
53
69
  enabled: true,
70
+ extensions: ['.mdx', '.md'],
54
71
  },
55
72
  }),
56
73
  ])
@@ -58,3 +75,7 @@ const config = await new ConfigBuilder()
58
75
 
59
76
  export default config;
60
77
  ```
78
+
79
+ ## Mixed Rendering
80
+
81
+ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.0-alpha.12",
3
+ "version": "0.2.0-alpha.13",
4
4
  "description": "JSX integration plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -21,7 +21,7 @@
21
21
  "vfile": "^6.0.3"
22
22
  },
23
23
  "peerDependencies": {
24
- "@ecopages/core": "0.2.0-alpha.12",
24
+ "@ecopages/core": "0.2.0-alpha.13",
25
25
  "@ecopages/jsx": "^0.3.0-alpha.0",
26
26
  "@ecopages/radiant": "^0.3.0-alpha.0"
27
27
  }
@@ -2,11 +2,7 @@ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComp
2
2
  import type { EcoPagesAppConfig } from '@ecopages/core/internal-types';
3
3
  import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
4
4
  import type { AssetProcessingService, ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
5
- import { type DeferredTemplateSerializer, type SerializableTemplateShape } from '@ecopages/core/route-renderer/template-serialization';
6
- import type { JsxRenderable } from '@ecopages/jsx';
7
- type EcopagesJsxTemplateShape = SerializableTemplateShape & {
8
- _$rType$: 1;
9
- };
5
+ import { type JsxRenderable } from '@ecopages/jsx';
10
6
  type MdxPageModule = EcoPageFile<{
11
7
  config?: EcoComponentConfig;
12
8
  layout?: EcoComponent;
@@ -19,21 +15,13 @@ type MdxPageModule = EcoPageFile<{
19
15
  * async page, layout, and html template components on the server.
20
16
  */
21
17
  export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderable> {
22
- /**
23
- * Deferred template serializers owned by the Ecopages JSX renderer.
24
- *
25
- * @remarks
26
- * Ecopages JSX can emit runtime-specific deferred template payloads during
27
- * mixed-integration rendering. Declaring the serializer here keeps JSX
28
- * template-shape knowledge colocated with the renderer while the base
29
- * `IntegrationRenderer` handles registration automatically.
30
- */
31
- static readonly deferredTemplateSerializers: DeferredTemplateSerializer<EcopagesJsxTemplateShape>[];
32
18
  name: string;
33
19
  static mdxExtensions: string[];
34
20
  private intrinsicCustomElementAssets;
35
21
  private collectedAssetFrames;
36
22
  private radiantSsrEnabled;
23
+ private resolveQueuedBoundaryChildren;
24
+ private resolveQueuedBoundaryHtml;
37
25
  constructor({ appConfig, assetProcessingService, resolvedIntegrationDependencies, runtimeOrigin, }: {
38
26
  appConfig: EcoPagesAppConfig;
39
27
  assetProcessingService: AssetProcessingService;
@@ -52,12 +40,13 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
52
40
  protected importPageFile(file: string): Promise<MdxPageModule>;
53
41
  render(options: IntegrationRendererRenderOptions<JsxRenderable>): Promise<RouteRendererBody>;
54
42
  renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
43
+ protected createComponentBoundaryRuntime(options: {
44
+ boundaryInput: ComponentRenderInput;
45
+ rendererCache: Map<string, IntegrationRenderer<any>>;
46
+ }): import("@ecopages/core").ComponentBoundaryRuntime;
55
47
  renderToResponse<P = any>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
56
- private resolveViewMetadata;
57
- private renderDocument;
58
48
  private beginCollectedAssetFrame;
59
49
  private endCollectedAssetFrame;
60
- private mergeCollectedAssets;
61
50
  private renderJsx;
62
51
  private renderEcoComponent;
63
52
  private createIntrinsicCustomElementRenderHook;
@@ -1,19 +1,9 @@
1
1
  import { rapidhash } from "@ecopages/core/hash";
2
2
  import { IntegrationRenderer } from "@ecopages/core/route-renderer/integration-renderer";
3
- import {
4
- serializeTemplateShape
5
- } from "@ecopages/core/route-renderer/template-serialization";
3
+ import { createMarkupNodeLike } from "@ecopages/jsx";
6
4
  import { renderToString, withServerCustomElementRenderHook } from "@ecopages/jsx/server";
7
5
  import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.plugin.js";
8
6
  let radiantLightDomShimInstallPromise;
9
- const ecopagesJsxDeferredTemplateSerializer = {
10
- matches(value) {
11
- return typeof value === "object" && value !== null && value._$rType$ === 1 && Array.isArray(value.strings) && (value.values === void 0 || Array.isArray(value.values));
12
- },
13
- serialize(template, serializeValue) {
14
- return serializeTemplateShape(template, serializeValue);
15
- }
16
- };
17
7
  async function ensureRadiantLightDomShimInstalled() {
18
8
  if (!radiantLightDomShimInstallPromise) {
19
9
  radiantLightDomShimInstallPromise = import("@ecopages/radiant/server/light-dom-shim").then((module) => {
@@ -45,21 +35,38 @@ const wrapMdxPage = (page, {
45
35
  return wrappedPage;
46
36
  };
47
37
  class EcopagesJsxRenderer extends IntegrationRenderer {
48
- /**
49
- * Deferred template serializers owned by the Ecopages JSX renderer.
50
- *
51
- * @remarks
52
- * Ecopages JSX can emit runtime-specific deferred template payloads during
53
- * mixed-integration rendering. Declaring the serializer here keeps JSX
54
- * template-shape knowledge colocated with the renderer while the base
55
- * `IntegrationRenderer` handles registration automatically.
56
- */
57
- static deferredTemplateSerializers = [ecopagesJsxDeferredTemplateSerializer];
58
38
  name = ECOPAGES_JSX_PLUGIN_NAME;
59
39
  static mdxExtensions = [".mdx"];
60
40
  intrinsicCustomElementAssets = /* @__PURE__ */ new Map();
61
41
  collectedAssetFrames = [];
62
42
  radiantSsrEnabled = false;
43
+ async resolveQueuedBoundaryChildren(children, queuedResolutionsByToken, resolveToken) {
44
+ if (children === void 0) {
45
+ return { assets: [] };
46
+ }
47
+ let assets = [];
48
+ let html;
49
+ if (typeof children === "string") {
50
+ html = children;
51
+ } else {
52
+ const renderedChildren = await this.renderJsx(children);
53
+ html = renderedChildren.html;
54
+ assets = renderedChildren.assets;
55
+ }
56
+ html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
57
+ return {
58
+ assets,
59
+ html
60
+ };
61
+ }
62
+ async resolveQueuedBoundaryHtml(html, runtimeContext) {
63
+ return this.resolveRendererOwnedQueuedBoundaryHtml({
64
+ html,
65
+ runtimeContext,
66
+ queueLabel: "Ecopages JSX",
67
+ renderQueuedChildren: async (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.resolveQueuedBoundaryChildren(children, queuedResolutionsByToken, resolveToken)
68
+ });
69
+ }
63
70
  constructor({
64
71
  appConfig,
65
72
  assetProcessingService,
@@ -110,30 +117,27 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
110
117
  };
111
118
  }
112
119
  async render(options) {
113
- const assetFrame = this.beginCollectedAssetFrame();
114
120
  try {
115
- const page = await this.renderEcoComponent(options.Page, {
116
- ...options.pageProps,
117
- locals: options.pageLocals
121
+ return await this.renderPageWithDocumentShell({
122
+ page: {
123
+ component: options.Page,
124
+ props: {
125
+ ...options.pageProps,
126
+ locals: options.pageLocals
127
+ }
128
+ },
129
+ layout: options.Layout ? {
130
+ component: options.Layout,
131
+ props: {
132
+ ...options.pageProps,
133
+ locals: options.locals
134
+ }
135
+ } : void 0,
136
+ htmlTemplate: options.HtmlTemplate,
137
+ metadata: options.metadata,
138
+ pageProps: options.pageProps ?? {}
118
139
  });
119
- const content = options.Layout ? await this.renderEcoComponent(options.Layout, {
120
- ...options.pageProps,
121
- children: page,
122
- locals: options.locals
123
- }) : page;
124
- const document = await this.renderEcoComponent(
125
- options.HtmlTemplate,
126
- {
127
- metadata: options.metadata,
128
- pageProps: options.pageProps ?? {},
129
- children: content
130
- }
131
- );
132
- const renderedDocument = await this.renderJsx(document);
133
- this.mergeCollectedAssets(this.endCollectedAssetFrame(assetFrame));
134
- return `${this.DOC_TYPE}${renderedDocument.html}`;
135
140
  } catch (error) {
136
- this.endCollectedAssetFrame(assetFrame);
137
141
  throw this.createRenderError("Error rendering page", error);
138
142
  }
139
143
  }
@@ -143,22 +147,32 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
143
147
  if (!isEcoFunctionComponent(input.component)) {
144
148
  throw new TypeError("JSX renderer expected a callable component.");
145
149
  }
146
- const props = input.children === void 0 ? input.props : { ...input.props, children: input.children };
150
+ let props = input.props;
151
+ if (input.children !== void 0) {
152
+ props = {
153
+ ...input.props,
154
+ children: typeof input.children === "string" ? createMarkupNodeLike(input.children) : input.children
155
+ };
156
+ }
147
157
  const content = await this.renderEcoComponent(
148
158
  input.component,
149
159
  props
150
160
  );
151
161
  const rendered = await this.renderJsx(content);
162
+ const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(
163
+ rendered.html,
164
+ this.getQueuedBoundaryRuntime(input)
165
+ );
152
166
  const componentAssets = input.component.config?.dependencies && typeof this.assetProcessingService?.processDependencies === "function" ? await this.processComponentDependencies([input.component]) : [];
153
- const html = rendered.html;
154
167
  const assets = this.htmlTransformer.dedupeProcessedAssets([
155
168
  ...this.endCollectedAssetFrame(assetFrame),
169
+ ...queuedBoundaryResolution.assets,
156
170
  ...componentAssets
157
171
  ]);
158
172
  return {
159
- html,
173
+ html: queuedBoundaryResolution.html,
160
174
  canAttachAttributes: true,
161
- rootTag: this.getRootTagName(html),
175
+ rootTag: this.getRootTagName(queuedBoundaryResolution.html),
162
176
  integrationName: this.name,
163
177
  assets
164
178
  };
@@ -167,68 +181,27 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
167
181
  throw this.createRenderError("Error rendering component", error);
168
182
  }
169
183
  }
184
+ createComponentBoundaryRuntime(options) {
185
+ return this.createQueuedBoundaryRuntime({
186
+ boundaryInput: options.boundaryInput,
187
+ rendererCache: options.rendererCache
188
+ });
189
+ }
170
190
  async renderToResponse(view, props, ctx) {
171
191
  try {
172
192
  if (!isEcoFunctionComponent(view)) {
173
193
  throw new TypeError("JSX renderer expected a callable view component.");
174
194
  }
175
- const layout = ctx.partial ? void 0 : view.config?.layout;
176
- await this.prepareViewDependencies(view, layout);
177
- const HtmlTemplate = ctx.partial ? void 0 : await this.getHtmlTemplate();
178
- const metadata = ctx.partial ? void 0 : await this.resolveViewMetadata(view, props);
179
- const assetFrame = this.beginCollectedAssetFrame();
180
- const capturedRender = await this.captureHtmlRender(async () => {
181
- const viewContent = await this.renderEcoComponent(view, props);
182
- if (ctx.partial) {
183
- return (await this.renderJsx(viewContent)).html;
184
- }
185
- return (await this.renderDocument(viewContent, {
186
- metadata,
187
- pageProps: props ?? {},
188
- layout
189
- })).html;
195
+ return await this.renderViewWithDocumentShell({
196
+ view,
197
+ props,
198
+ ctx,
199
+ layout: view.config?.layout
190
200
  });
191
- this.mergeCollectedAssets(this.endCollectedAssetFrame(assetFrame));
192
- const html = await this.finalizeCapturedHtmlRender({
193
- html: capturedRender.html,
194
- graphContext: capturedRender.graphContext,
195
- componentsToResolve: HtmlTemplate ? layout ? [HtmlTemplate, layout, view] : [HtmlTemplate, view] : [view],
196
- partial: ctx.partial
197
- });
198
- return this.createHtmlResponse(html, ctx);
199
201
  } catch (error) {
200
202
  throw this.createRenderError("Error rendering view", error);
201
203
  }
202
204
  }
203
- async resolveViewMetadata(view, props) {
204
- return view.metadata ? await view.metadata({
205
- params: {},
206
- query: {},
207
- props,
208
- appConfig: this.appConfig
209
- }) : this.appConfig.defaultMetadata;
210
- }
211
- async renderDocument(content, {
212
- metadata,
213
- pageProps,
214
- layout
215
- }) {
216
- const resolvedContent = layout ? await this.renderEcoComponent(layout, {
217
- ...pageProps,
218
- children: content
219
- }) : content;
220
- const HtmlTemplate = await this.getHtmlTemplate();
221
- const document = await this.renderEcoComponent(HtmlTemplate, {
222
- metadata,
223
- pageProps,
224
- children: resolvedContent
225
- });
226
- const renderedDocument = await this.renderJsx(document);
227
- return {
228
- assets: renderedDocument.assets,
229
- html: `${this.DOC_TYPE}${renderedDocument.html}`
230
- };
231
- }
232
205
  beginCollectedAssetFrame() {
233
206
  const frame = [];
234
207
  this.collectedAssetFrames.push(frame);
@@ -241,14 +214,6 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
241
214
  }
242
215
  return this.htmlTransformer.dedupeProcessedAssets(activeFrame);
243
216
  }
244
- mergeCollectedAssets(assets) {
245
- if (assets.length === 0) {
246
- return;
247
- }
248
- this.htmlTransformer.setProcessedDependencies(
249
- this.htmlTransformer.dedupeProcessedAssets([...this.htmlTransformer.getProcessedDependencies(), ...assets])
250
- );
251
- }
252
217
  async renderJsx(value) {
253
218
  if (this.radiantSsrEnabled) {
254
219
  await ensureRadiantLightDomShimInstalled();
@@ -94,14 +94,6 @@ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable>
94
94
  * completes the base integration setup.
95
95
  */
96
96
  setup(): Promise<void>;
97
- /**
98
- * Defers boundaries only when another integration renders a component that is
99
- * owned by this JSX integration.
100
- */
101
- shouldDeferComponentBoundary(input: {
102
- currentIntegration: string;
103
- targetIntegration?: string;
104
- }): boolean;
105
97
  private ensureMdxLoaderPlugin;
106
98
  private setupMdxBunPlugin;
107
99
  private buildIntrinsicCustomElementAssetRegistry;
@@ -149,13 +149,6 @@ class EcopagesJsxPlugin extends IntegrationPlugin {
149
149
  await this.buildIntrinsicCustomElementAssetRegistry();
150
150
  await super.setup();
151
151
  }
152
- /**
153
- * Defers boundaries only when another integration renders a component that is
154
- * owned by this JSX integration.
155
- */
156
- shouldDeferComponentBoundary(input) {
157
- return input.targetIntegration === this.name && input.currentIntegration !== this.name;
158
- }
159
152
  ensureMdxLoaderPlugin() {
160
153
  if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
161
154
  return;