@ecopages/ecopages-jsx 0.2.0-rc.7 → 0.2.0-rc.8

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/README.md CHANGED
@@ -78,6 +78,23 @@ new MyElement().renderHostToString({ mode: 'hydrate' });
78
78
 
79
79
  The legacy `hydrate: true` option remains compatible, but `mode` is the current SSR contract. Registered intrinsic tags that contain a dash are SSR candidates, and framework-specific host rendering should be adapted through the server custom-element render hook instead of hardcoding framework branches into JSX page code.
80
80
 
81
+ The integration loads Radiant host serialization through the public `@ecopages/radiant/server/radiant-element-ssr` export after installing the light-DOM shim. Applications do not need a separate Radiant SSR side-effect import.
82
+
83
+ ### Registered Radiant scripts (`ssr: true`)
84
+
85
+ Declare custom-element registration once in `dependencies.scripts`. Ecopages JSX preloads entries marked `ssr: true` on the server before render so hosts can SSR without a redundant `import './my-element.script.ts'` in the component file.
86
+
87
+ ```ts
88
+ export const ThemeToggle = eco.component({
89
+ dependencies: {
90
+ scripts: [{ src: './theme-toggle.script.ts', ssr: true, lazy: { 'on:idle': true } }],
91
+ },
92
+ render: () => <theme-toggle />,
93
+ });
94
+ ```
95
+
96
+ Keep `import type` when you only need props from the script module. Do not add `ssr: true` to browser-only scripts that touch `window` or `document` without guards.
97
+
81
98
  ## MDX Support
82
99
 
83
100
  Enable MDX to treat `.mdx` files as JSX routes compiled against the `@ecopages/jsx` runtime.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.0-rc.7",
3
+ "version": "0.2.0-rc.8",
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.7",
37
+ "@ecopages/mdx": "0.2.0-rc.8",
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.7",
42
+ "@ecopages/core": "0.2.0-rc.8",
43
43
  "@ecopages/jsx": "^0.3.0-rc.9",
44
44
  "@ecopages/radiant": "^0.3.0-rc.9"
45
45
  }
@@ -59,7 +59,8 @@ export class EcopagesJsxRadiantSsrPolicy {
59
59
  async ensureRuntimeInstalled() {
60
60
  if (!this.runtimeModulesPromise) {
61
61
  const radiantLightDomShimEntry = import.meta.resolve('@ecopages/radiant/server/light-dom-shim');
62
- const radiantElementSsrRuntimeModuleUrl = new URL('./radiant-element-ssr-bridge.js', radiantLightDomShimEntry).href;
62
+ const radiantElementSsrRuntimeModuleUrl = import.meta
63
+ .resolve('@ecopages/radiant/server/radiant-element-ssr');
63
64
  this.runtimeModulesPromise = (async () => {
64
65
  const lightDomShimModule = await import(radiantLightDomShimEntry);
65
66
  ensureRadiantLightDomGlobals(lightDomShimModule.ensureLightDomShim);
@@ -16,6 +16,7 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
16
16
  private readonly mdxExtensions;
17
17
  private readonly renderSession;
18
18
  private readonly radiantSsrPolicy;
19
+ private readonly ssrScriptPreloader;
19
20
  /**
20
21
  * Serializes foreign-child props for string-first boundaries.
21
22
  *
@@ -7,6 +7,8 @@ import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
7
7
  import { isMdxFile, normalizeMdxPageModule, } from './ecopages-jsx-mdx.js';
8
8
  import { EcopagesJsxRenderSession } from './ecopages-jsx-render-session.js';
9
9
  import { EcopagesJsxRadiantSsrPolicy } from './ecopages-jsx-radiant-ssr-policy.js';
10
+ 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
12
  /**
11
13
  * Local Ecopages renderer for JSX templates in the docs app.
12
14
  *
@@ -18,6 +20,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
18
20
  mdxExtensions;
19
21
  renderSession;
20
22
  radiantSsrPolicy;
23
+ ssrScriptPreloader;
21
24
  /**
22
25
  * Serializes foreign-child props for string-first boundaries.
23
26
  *
@@ -122,8 +125,26 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
122
125
  });
123
126
  this.mdxExtensions = jsxConfig?.mdxExtensions ?? ['.mdx'];
124
127
  this.renderSession = new EcopagesJsxRenderSession((assets) => this.htmlTransformer.dedupeProcessedAssets(assets));
125
- this.radiantSsrPolicy =
126
- jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(jsxConfig?.radiantSsrEnabled ?? false);
128
+ const radiantSsrEnabled = jsxConfig?.radiantSsrEnabled ?? false;
129
+ this.radiantSsrPolicy = jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(radiantSsrEnabled);
130
+ const preferSourceImports = typeof Bun !== 'undefined';
131
+ this.ssrScriptPreloader = new CustomElementScriptPreloader({
132
+ 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,
140
+ logLabel: 'ecopages-jsx',
141
+ preferSourceImports,
142
+ resolveDependencyPath: (componentDir, sourcePath) => this.resolveDependencyPath(componentDir, sourcePath),
143
+ resolvePreloadEntrypoint: createCustomElementSsrPreloadEntrypointResolver({
144
+ preferSourceImports,
145
+ processDependencies: this.assetProcessingService?.processDependencies?.bind(this.assetProcessingService),
146
+ }),
147
+ });
127
148
  }
128
149
  /** Returns whether the requested page file should be treated as MDX. */
129
150
  isMdxFile(filePath) {
@@ -136,126 +157,140 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
136
157
  });
137
158
  }
138
159
  async render(options) {
139
- return await this.withPreparedRadiantRuntime(async () => await this.renderSession.withActiveScope(async () => {
140
- try {
141
- const result = await this.renderPageWithDocumentShell({
142
- page: {
143
- component: options.Page,
144
- props: {
145
- ...options.pageProps,
146
- locals: options.pageLocals,
160
+ return await this.withPreparedRadiantRuntime(async () => {
161
+ await this.ssrScriptPreloader.preloadSsrScripts([
162
+ options.Page,
163
+ options.Layout,
164
+ options.HtmlTemplate,
165
+ ...(options.resolvedPageDependencyComponents ?? []),
166
+ ]);
167
+ return await this.renderSession.withActiveScope(async () => {
168
+ try {
169
+ const result = await this.renderPageWithDocumentShell({
170
+ page: {
171
+ component: options.Page,
172
+ props: {
173
+ ...options.pageProps,
174
+ locals: options.pageLocals,
175
+ },
147
176
  },
148
- },
149
- /**
150
- * @remarks Content components supplied by the Page dependency
151
- * resolver (for example the active MDX entry) carry foreign
152
- * ownership the static Page config cannot declare.
153
- */
154
- foreignChildRoots: options.resolvedPageDependencyComponents,
155
- layouts: resolveDocumentShellLayouts({
156
- layout: options.Layout,
157
- layoutEntries: options.layoutEntries,
158
- params: options.params,
159
- query: options.query,
160
- locals: options.locals,
161
- }),
162
- htmlTemplate: options.HtmlTemplate,
163
- metadata: options.metadata,
164
- pageProps: options.pageProps ?? {},
165
- });
166
- this.recordHmrOwnership([
167
- options.Page,
168
- options.Layout,
169
- options.HtmlTemplate,
170
- ...(options.resolvedPageDependencyComponents ?? []),
171
- ]);
172
- return result;
173
- }
174
- catch (error) {
175
- throw this.createRenderError('Error rendering page', error);
176
- }
177
- }));
177
+ /**
178
+ * @remarks Content components supplied by the Page dependency
179
+ * resolver (for example the active MDX entry) carry foreign
180
+ * ownership the static Page config cannot declare.
181
+ */
182
+ foreignChildRoots: options.resolvedPageDependencyComponents,
183
+ layouts: resolveDocumentShellLayouts({
184
+ layout: options.Layout,
185
+ layoutEntries: options.layoutEntries,
186
+ params: options.params,
187
+ query: options.query,
188
+ locals: options.locals,
189
+ }),
190
+ htmlTemplate: options.HtmlTemplate,
191
+ metadata: options.metadata,
192
+ pageProps: options.pageProps ?? {},
193
+ });
194
+ this.recordHmrOwnership([
195
+ options.Page,
196
+ options.Layout,
197
+ options.HtmlTemplate,
198
+ ...(options.resolvedPageDependencyComponents ?? []),
199
+ ]);
200
+ return result;
201
+ }
202
+ catch (error) {
203
+ throw this.createRenderError('Error rendering page', error);
204
+ }
205
+ });
206
+ });
178
207
  }
179
208
  async renderComponent(input) {
180
- return await this.withPreparedRadiantRuntime(async () => await this.renderSession.withActiveScope(async () => {
181
- const assetFrame = this.renderSession.beginCollectedAssetFrame();
182
- try {
183
- if (typeof input.component !== 'function') {
184
- throw new TypeError('JSX renderer expected a callable component.');
185
- }
186
- const component = input.component;
187
- const componentProps = input.children === undefined
188
- ? input.props
189
- : {
190
- ...input.props,
191
- children: typeof input.children === 'string'
192
- ? createMarkupNodeLike(input.children)
193
- : input.children,
194
- };
195
- const content = await this.withCustomElementRenderHook(() => component(componentProps));
196
- const rendered = await this.renderJsx(content);
197
- const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
198
- currentIntegrationName: this.name,
199
- html: rendered.html,
200
- runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(input, getForeignSubtreeResolutionContextKey(this.name)),
201
- queueLabel: 'Ecopages JSX',
202
- getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
203
- appConfig: this.appConfig,
204
- runtimeOrigin: this.runtimeOrigin,
209
+ return await this.withPreparedRadiantRuntime(async () => {
210
+ await this.ssrScriptPreloader.preloadSsrScripts([input.component]);
211
+ return await this.renderSession.withActiveScope(async () => {
212
+ const assetFrame = this.renderSession.beginCollectedAssetFrame();
213
+ try {
214
+ if (typeof input.component !== 'function') {
215
+ throw new TypeError('JSX renderer expected a callable component.');
216
+ }
217
+ const component = input.component;
218
+ const componentProps = input.children === undefined
219
+ ? input.props
220
+ : {
221
+ ...input.props,
222
+ children: typeof input.children === 'string'
223
+ ? createMarkupNodeLike(input.children)
224
+ : input.children,
225
+ };
226
+ const content = await this.withCustomElementRenderHook(() => component(componentProps));
227
+ const rendered = await this.renderJsx(content);
228
+ const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
205
229
  currentIntegrationName: this.name,
206
- currentRenderer: this,
207
- integrationName,
208
- cache: rendererCache,
209
- }),
210
- applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
211
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
212
- renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken),
213
- });
214
- const componentAssets = input.component.config?.dependencies &&
215
- typeof this.assetProcessingService?.processDependencies === 'function'
216
- ? await this.processComponentDependencies([input.component])
217
- : [];
218
- const assets = this.htmlTransformer.dedupeProcessedAssets([
219
- ...this.renderSession.endCollectedAssetFrame(assetFrame),
220
- ...queuedForeignSubtreeResolution.assets,
221
- ...componentAssets,
222
- ]);
223
- this.recordHmrOwnership([input.component]);
224
- return this.finalizeIslandComponentRender(input, {
225
- html: queuedForeignSubtreeResolution.html,
226
- canAttachAttributes: true,
227
- rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
228
- integrationName: this.name,
229
- assets,
230
- });
231
- }
232
- catch (error) {
233
- this.renderSession.endCollectedAssetFrame(assetFrame);
234
- throw this.createRenderError('Error rendering component', error);
235
- }
236
- }));
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
+ });
245
+ const componentAssets = input.component.config?.dependencies &&
246
+ typeof this.assetProcessingService?.processDependencies === 'function'
247
+ ? await this.processComponentDependencies([input.component])
248
+ : [];
249
+ const assets = this.htmlTransformer.dedupeProcessedAssets([
250
+ ...this.renderSession.endCollectedAssetFrame(assetFrame),
251
+ ...queuedForeignSubtreeResolution.assets,
252
+ ...componentAssets,
253
+ ]);
254
+ this.recordHmrOwnership([input.component]);
255
+ return this.finalizeIslandComponentRender(input, {
256
+ html: queuedForeignSubtreeResolution.html,
257
+ canAttachAttributes: true,
258
+ rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
259
+ integrationName: this.name,
260
+ assets,
261
+ });
262
+ }
263
+ catch (error) {
264
+ this.renderSession.endCollectedAssetFrame(assetFrame);
265
+ throw this.createRenderError('Error rendering component', error);
266
+ }
267
+ });
268
+ });
237
269
  }
238
270
  async renderToResponse(view, props, ctx) {
239
- return await this.withPreparedRadiantRuntime(async () => await this.renderSession.withActiveScope(async () => {
240
- try {
241
- if (typeof view !== 'function') {
242
- throw new TypeError('JSX renderer expected a callable view component.');
271
+ return await this.withPreparedRadiantRuntime(async () => {
272
+ const viewComponent = view;
273
+ const layouts = viewComponent.config?.layouts;
274
+ await this.ssrScriptPreloader.preloadSsrScripts([view, ...(layouts ?? [])]);
275
+ return await this.renderSession.withActiveScope(async () => {
276
+ try {
277
+ if (typeof view !== 'function') {
278
+ throw new TypeError('JSX renderer expected a callable view component.');
279
+ }
280
+ const response = await this.renderViewWithDocumentShell({
281
+ view: viewComponent,
282
+ props: props,
283
+ ctx,
284
+ layout: layouts?.[layouts.length - 1],
285
+ });
286
+ this.recordHmrOwnership([view]);
287
+ return response;
288
+ }
289
+ catch (error) {
290
+ throw this.createRenderError('Error rendering view', error);
243
291
  }
244
- const viewComponent = view;
245
- const layouts = viewComponent.config?.layouts;
246
- const response = await this.renderViewWithDocumentShell({
247
- view: viewComponent,
248
- props: props,
249
- ctx,
250
- layout: layouts?.[layouts.length - 1],
251
- });
252
- this.recordHmrOwnership([view]);
253
- return response;
254
- }
255
- catch (error) {
256
- throw this.createRenderError('Error rendering view', error);
257
- }
258
- }));
292
+ });
293
+ });
259
294
  }
260
295
  async renderJsx(value) {
261
296
  const collectedAssets = [];
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
+ import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, invalidateCustomElementScriptPreload, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
2
3
  const CUSTOM_ELEMENT_TAG_PATTERN = /@customElement\s*\(\s*['"`]([^'"`]+)['"`]/u;
3
4
  /**
4
5
  * Reads the custom-element tag name from a registered script module source file.
@@ -38,6 +39,7 @@ export function clearRadiantCustomElementDefinition(tagName) {
38
39
  * Clears the SSR custom-element registry entry for one registered Radiant script module.
39
40
  */
40
41
  export function invalidateRadiantRegisteredScriptSsrRegistration(scriptPath) {
42
+ invalidateCustomElementScriptPreload(scriptPath, CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.ecopagesJsx);
41
43
  const tagName = resolveRadiantCustomElementTag(scriptPath);
42
44
  if (!tagName) {
43
45
  return false;