@ecopages/ecopages-jsx 0.2.0-alpha.16 → 0.2.0-alpha.18

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
@@ -8,12 +8,18 @@
8
8
 
9
9
  ### Bug Fixes
10
10
 
11
+ - Aligned Radiant SSR and hydration wiring with the public `@ecopages/radiant/server/render-component` and `@ecopages/radiant/client/hydrator` entrypoints so JSX apps install an explicit client hydrator bootstrap instead of relying on implicit side effects.
11
12
  - Fixed Radiant SSR page inspection to install the light-DOM shim before JSX page modules are imported outside the normal render pass.
12
13
  - Restored direct `EcopagesJsxPlugin` construction so the exported class still accepts the public plugin options shape.
13
14
  - Fixed intrinsic custom-element asset discovery so Ecopages JSX registers scripts declared with decorator and function-call `customElement(...)` syntax.
14
15
  - Fixed the Ecopages JSX browser runtime import map so browser builds no longer expose `@ecopages/jsx/server` through the shared JSX vendor bundle.
15
16
  - Fixed the Ecopages JSX browser runtime bundle so Radiant custom-element scripts no longer fail on a duplicate `jsxDEV` export cycle.
17
+ - Fixed Ecopages JSX boundary payload compatibility coverage and removed the plugin/renderer integration-name import cycle.
16
18
 
17
19
  ### Refactoring
18
20
 
19
21
  - Replaced Ecopages JSX renderer static and post-construction configuration with instance-owned renderer wiring and extracted shared plugin and renderer types into a dedicated module.
22
+
23
+ ### Tests
24
+
25
+ - Added renderer-level coverage for the boundary payload compatibility contract.
package/README.md CHANGED
@@ -29,7 +29,7 @@ export default config;
29
29
  ## What This Integration Owns
30
30
 
31
31
  - `.tsx` route files by default. Use `extensions` to change the JSX route suffix list.
32
- - Optional Radiant runtime assets and import-map entries.
32
+ - Optional Radiant SSR/runtime wiring through the public `@ecopages/radiant` entrypoints.
33
33
  - Optional `.mdx` routes compiled against the `@ecopages/jsx` runtime.
34
34
 
35
35
  ## Route Extensions
@@ -44,7 +44,13 @@ ecopagesJsxPlugin({
44
44
 
45
45
  ## Radiant Support
46
46
 
47
- Radiant runtime bundles are enabled by default so JSX pages can render and hydrate Radiant custom elements.
47
+ Radiant support is enabled by default. When `radiant: true`, the plugin keeps the ownership split explicit:
48
+
49
+ - Ecopages JSX owns page-level JSX SSR and container hydration.
50
+ - Radiant SSR is activated on the server through `@ecopages/radiant/server/render-component`.
51
+ - Radiant host hydration is activated on the client through an explicit head bootstrap that calls `installRadiantHydrator()` from `@ecopages/radiant/client/hydrator` before intrinsic custom-element modules load.
52
+
53
+ That means server-rendered `RadiantComponent` hosts hydrate in place only when both the SSR markers and the explicit client hydrator are present. Without the client hydrator, Radiant intentionally falls back to a fresh client render on first connect.
48
54
 
49
55
  ```ts
50
56
  ecopagesJsxPlugin({
@@ -52,7 +58,9 @@ ecopagesJsxPlugin({
52
58
  });
53
59
  ```
54
60
 
55
- Set `radiant: false` when your JSX pages do not need the Radiant browser runtime on a given app.
61
+ Set `radiant: false` when your JSX pages do not need Radiant SSR or the Radiant browser runtime on a given app.
62
+
63
+ The plugin bootstrap is intentionally explicit rather than relying on unrelated imports to install the Radiant hydrator as a side effect.
56
64
 
57
65
  ## MDX Support
58
66
 
@@ -79,3 +87,9 @@ export default config;
79
87
  ## Mixed Rendering
80
88
 
81
89
  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.
90
+
91
+ Important:
92
+
93
+ - Components that may render foreign children must declare those children in `config.dependencies.components`.
94
+ - Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not treat rendered HTML alone as the source of truth.
95
+ - Ecopages JSX keeps raw-markup preservation and asset collection inside the JSX renderer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.0-alpha.16",
3
+ "version": "0.2.0-alpha.18",
4
4
  "description": "JSX integration plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -21,8 +21,8 @@
21
21
  "vfile": "^6.0.3"
22
22
  },
23
23
  "peerDependencies": {
24
- "@ecopages/core": "0.2.0-alpha.16",
25
- "@ecopages/jsx": "^0.3.0-alpha.0",
26
- "@ecopages/radiant": "^0.3.0-alpha.0"
24
+ "@ecopages/core": "0.2.0-alpha.18",
25
+ "@ecopages/jsx": "^0.3.0-alpha.1",
26
+ "@ecopages/radiant": "^0.3.0-alpha.1"
27
27
  }
28
28
  }
@@ -16,7 +16,7 @@ type MdxPageModule = EcoPageFile<{
16
16
  */
17
17
  export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderable> {
18
18
  name: string;
19
- private static radiantLightDomShimInstallPromise;
19
+ private static radiantServerRuntimeInstallPromise;
20
20
  private readonly intrinsicCustomElementAssets;
21
21
  private collectedAssetFrames;
22
22
  private readonly mdxExtensions;
@@ -56,7 +56,9 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
56
56
  private endCollectedAssetFrame;
57
57
  private renderJsx;
58
58
  private renderEcoComponent;
59
- private ensureRadiantLightDomShimInstalled;
59
+ private recordCollectedAssets;
60
+ private ensureRadiantServerRuntimeIfEnabled;
61
+ private ensureRadiantServerRuntimeInstalled;
60
62
  private isFunctionComponent;
61
63
  private createComponentProps;
62
64
  private collectComponentAssets;
@@ -4,10 +4,10 @@ import {
4
4
  } from "@ecopages/core/route-renderer/integration-renderer";
5
5
  import { createMarkupNodeLike } from "@ecopages/jsx";
6
6
  import { renderToString, withServerCustomElementRenderHook } from "@ecopages/jsx/server";
7
- import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.plugin.js";
7
+ import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
8
8
  class EcopagesJsxRenderer extends IntegrationRenderer {
9
9
  name = ECOPAGES_JSX_PLUGIN_NAME;
10
- static radiantLightDomShimInstallPromise;
10
+ static radiantServerRuntimeInstallPromise;
11
11
  intrinsicCustomElementAssets;
12
12
  collectedAssetFrames = [];
13
13
  mdxExtensions;
@@ -71,9 +71,7 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
71
71
  return this.mdxExtensions.some((ext) => filePath.endsWith(ext));
72
72
  }
73
73
  async importPageFile(file, options) {
74
- if (this.radiantSsrEnabled) {
75
- await this.ensureRadiantLightDomShimInstalled();
76
- }
74
+ await this.ensureRadiantServerRuntimeIfEnabled();
77
75
  const module = await super.importPageFile(file, options);
78
76
  return this.isMdxFile(file) ? this.normalizeMdxPageModule(file, module) : module;
79
77
  }
@@ -193,46 +191,52 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
193
191
  return this.htmlTransformer.dedupeProcessedAssets(activeFrame);
194
192
  }
195
193
  async renderJsx(value) {
196
- if (this.radiantSsrEnabled) {
197
- await this.ensureRadiantLightDomShimInstalled();
198
- }
194
+ await this.ensureRadiantServerRuntimeIfEnabled();
199
195
  const collectedAssets = [];
200
196
  const html = withServerCustomElementRenderHook(
201
197
  this.createIntrinsicCustomElementRenderHook(collectedAssets),
202
198
  () => renderToString(value)
203
199
  );
204
- const dedupedAssets = this.htmlTransformer.dedupeProcessedAssets(collectedAssets);
205
- const activeFrame = this.collectedAssetFrames[this.collectedAssetFrames.length - 1];
206
- if (activeFrame) {
207
- activeFrame.push(...dedupedAssets);
208
- }
200
+ const dedupedAssets = this.recordCollectedAssets(collectedAssets);
209
201
  return {
210
202
  assets: dedupedAssets,
211
203
  html
212
204
  };
213
205
  }
214
206
  async renderEcoComponent(component, props) {
215
- if (this.radiantSsrEnabled) {
216
- await this.ensureRadiantLightDomShimInstalled();
217
- }
207
+ await this.ensureRadiantServerRuntimeIfEnabled();
218
208
  const collectedAssets = [];
219
209
  const rendered = await withServerCustomElementRenderHook(
220
210
  this.createIntrinsicCustomElementRenderHook(collectedAssets),
221
211
  () => this.invokeComponent(component, props)
222
212
  );
213
+ this.recordCollectedAssets(collectedAssets);
214
+ return rendered;
215
+ }
216
+ recordCollectedAssets(collectedAssets) {
217
+ const dedupedAssets = this.htmlTransformer.dedupeProcessedAssets(collectedAssets);
223
218
  const activeFrame = this.collectedAssetFrames[this.collectedAssetFrames.length - 1];
224
219
  if (activeFrame) {
225
- activeFrame.push(...this.htmlTransformer.dedupeProcessedAssets(collectedAssets));
220
+ activeFrame.push(...dedupedAssets);
226
221
  }
227
- return rendered;
222
+ return dedupedAssets;
223
+ }
224
+ async ensureRadiantServerRuntimeIfEnabled() {
225
+ if (!this.radiantSsrEnabled) {
226
+ return;
227
+ }
228
+ await this.ensureRadiantServerRuntimeInstalled();
228
229
  }
229
- async ensureRadiantLightDomShimInstalled() {
230
- if (!EcopagesJsxRenderer.radiantLightDomShimInstallPromise) {
231
- EcopagesJsxRenderer.radiantLightDomShimInstallPromise = import("@ecopages/radiant/server/light-dom-shim").then((module) => {
232
- module.installLightDomShim();
233
- }).then(() => void 0);
230
+ async ensureRadiantServerRuntimeInstalled() {
231
+ if (!EcopagesJsxRenderer.radiantServerRuntimeInstallPromise) {
232
+ EcopagesJsxRenderer.radiantServerRuntimeInstallPromise = Promise.all([
233
+ import("@ecopages/radiant/server/render-component"),
234
+ import("@ecopages/radiant/server/light-dom-shim").then((module) => {
235
+ module.installLightDomShim();
236
+ })
237
+ ]).then(() => void 0);
234
238
  }
235
- await EcopagesJsxRenderer.radiantLightDomShimInstallPromise;
239
+ await EcopagesJsxRenderer.radiantServerRuntimeInstallPromise;
236
240
  }
237
241
  isFunctionComponent(component) {
238
242
  return typeof component === "function";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Stable integration name shared by the JSX plugin and renderer.
3
+ *
4
+ * Ecopages uses this identifier to match route files, renderer instances, and
5
+ * cross-integration component boundaries.
6
+ */
7
+ export declare const ECOPAGES_JSX_PLUGIN_NAME = "ecopages-jsx";
@@ -0,0 +1,4 @@
1
+ const ECOPAGES_JSX_PLUGIN_NAME = "ecopages-jsx";
2
+ export {
3
+ ECOPAGES_JSX_PLUGIN_NAME
4
+ };
@@ -4,13 +4,6 @@ import type { JsxRenderable } from '@ecopages/jsx';
4
4
  import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
5
5
  import type { EcopagesJsxPluginOptions } from './ecopages-jsx.types.js';
6
6
  export type { EcopagesJsxMdxCompileOptions, EcopagesJsxMdxOptions, EcopagesJsxPluginOptions, EcopagesJsxRendererConfig, } from './ecopages-jsx.types.js';
7
- /**
8
- * Stable integration name shared by the JSX plugin and renderer.
9
- *
10
- * Ecopages uses this identifier to match route files, renderer instances, and
11
- * cross-integration component boundaries.
12
- */
13
- export declare const ECOPAGES_JSX_PLUGIN_NAME = "ecopages-jsx";
14
7
  /** JSX integration plugin for Ecopages, supporting `.tsx` templates and optional Radiant web components. */
15
8
  export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable> {
16
9
  renderer: typeof EcopagesJsxRenderer;
@@ -3,9 +3,9 @@ import path from "node:path";
3
3
  import { IntegrationPlugin } from "@ecopages/core/plugins/integration-plugin";
4
4
  import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
5
5
  import { VFile } from "vfile";
6
+ import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
6
7
  import { EcopagesJsxRenderer } from "./ecopages-jsx-renderer.js";
7
8
  import { JsxRuntimeBundleService } from "./services/jsx-runtime-bundle.service.js";
8
- const ECOPAGES_JSX_PLUGIN_NAME = "ecopages-jsx";
9
9
  const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
10
  const mergePluginLists = (...lists) => {
11
11
  const merged = lists.flatMap((list) => list ? [...list] : []);
@@ -271,7 +271,6 @@ class EcopagesJsxPlugin extends IntegrationPlugin {
271
271
  }
272
272
  const ecopagesJsxPlugin = (options) => new EcopagesJsxPlugin(options);
273
273
  export {
274
- ECOPAGES_JSX_PLUGIN_NAME,
275
274
  EcopagesJsxPlugin,
276
275
  ecopagesJsxPlugin
277
276
  };
@@ -33,8 +33,13 @@ export type EcopagesJsxPluginOptions = Omit<IntegrationPluginConfig, 'name' | 'e
33
33
  /** Optional JSX route extensions. Defaults to `.tsx`. */
34
34
  extensions?: string[];
35
35
  /**
36
- * Whether to include the `@ecopages/radiant` and `@ecopages/signals` vendor
37
- * bundles and bare-specifier mappings.
36
+ * Whether to include the Radiant integration contract for JSX apps.
37
+ *
38
+ * When enabled, Ecopages JSX:
39
+ * - imports `@ecopages/radiant/server/render-component` before Radiant SSR
40
+ * - exposes browser-safe Radiant bare specifiers through the runtime import map
41
+ * - injects an explicit client bootstrap that calls `installRadiantHydrator()`
42
+ * before intrinsic custom-element modules load
38
43
  *
39
44
  * Set to `false` when pages do not use Radiant web components.
40
45
  * @default true
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
12
12
  import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
13
+ export declare const RADIANT_HYDRATOR_BOOTSTRAP_ATTRIBUTE = "data-ecopages-jsx-radiant-hydrator";
13
14
  export interface JsxRuntimeBundleServiceConfig {
14
15
  radiant: boolean;
15
16
  rootDir?: string;
@@ -44,9 +45,12 @@ export declare class JsxRuntimeBundleService {
44
45
  * script plus one `createBrowserRuntimeScriptAsset` per vendor bundle.
45
46
  */
46
47
  getDependencies(): Promise<AssetDefinition[]>;
48
+ private createRadiantHydratorBootstrapAsset;
49
+ private createRadiantHydratorBootstrapSource;
50
+ private getArtifactsDir;
51
+ private getEntryImportPath;
47
52
  private getOrCreateSpecifierMap;
48
53
  private getOrCreateJsxEntryModulePath;
49
- private getRadiantBrowserRuntimeSpecifiers;
50
54
  private getRadiantBrowserRuntimeModules;
51
55
  private resolvePackageExportModulePath;
52
56
  private getOrCreateRadiantEntryModulePath;
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
3
3
  import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
4
4
  import {
5
+ BROWSER_RUNTIME_SCRIPT_ATTRIBUTES,
5
6
  buildBrowserRuntimeAssetUrl,
6
7
  createBrowserRuntimeScriptAsset,
7
8
  AssetFactory
@@ -10,6 +11,7 @@ const VENDOR_FILE_NAMES = {
10
11
  jsx: "ecopages-jsx-esm.js",
11
12
  radiant: "ecopages-radiant-esm.js"
12
13
  };
14
+ const RADIANT_HYDRATOR_BOOTSTRAP_ATTRIBUTE = "data-ecopages-jsx-radiant-hydrator";
13
15
  function getNamedExportNamesFromModuleSource(source) {
14
16
  const exportNames = /* @__PURE__ */ new Set();
15
17
  for (const match of source.matchAll(/export\s*\{([^}]+)\}/g)) {
@@ -37,6 +39,9 @@ function isBrowserRuntimeRadiantSpecifier(exportKey) {
37
39
  if (exportKey === "." || exportKey.startsWith("./context/")) {
38
40
  return true;
39
41
  }
42
+ if (exportKey === "./client/hydrator") {
43
+ return true;
44
+ }
40
45
  if (exportKey.startsWith("./decorators/") || exportKey.startsWith("./helpers/")) {
41
46
  return true;
42
47
  }
@@ -105,13 +110,18 @@ class JsxRuntimeBundleService {
105
110
  bundle: false,
106
111
  content: JSON.stringify({ imports: specifierMap }, null, 2),
107
112
  attributes: { type: "importmap" }
108
- }),
113
+ })
114
+ ];
115
+ if (this.config.radiant) {
116
+ deps.push(this.createRadiantHydratorBootstrapAsset());
117
+ }
118
+ deps.push(
109
119
  createBrowserRuntimeScriptAsset({
110
120
  importPath: jsxEntryModulePath,
111
121
  name: "ecopages-jsx-esm",
112
122
  fileName: VENDOR_FILE_NAMES.jsx
113
123
  })
114
- ];
124
+ );
115
125
  if (this.config.radiant) {
116
126
  const radiantEntryModulePath = await this.getOrCreateRadiantEntryModulePath();
117
127
  deps.push(
@@ -124,6 +134,31 @@ class JsxRuntimeBundleService {
124
134
  }
125
135
  return deps;
126
136
  }
137
+ createRadiantHydratorBootstrapAsset() {
138
+ return AssetFactory.createInlineContentScript({
139
+ position: "head",
140
+ bundle: false,
141
+ content: this.createRadiantHydratorBootstrapSource(),
142
+ attributes: {
143
+ ...BROWSER_RUNTIME_SCRIPT_ATTRIBUTES,
144
+ [RADIANT_HYDRATOR_BOOTSTRAP_ATTRIBUTE]: "true"
145
+ }
146
+ });
147
+ }
148
+ createRadiantHydratorBootstrapSource() {
149
+ return [
150
+ "import { installRadiantHydrator } from '@ecopages/radiant/client/hydrator';",
151
+ "installRadiantHydrator();"
152
+ ].join("\n");
153
+ }
154
+ getArtifactsDir() {
155
+ const rootDir = this.config.rootDir ?? process.cwd();
156
+ return path.join(rootDir, "node_modules", ".cache", "ecopages-browser-runtime");
157
+ }
158
+ getEntryImportPath(fromDir, targetPath) {
159
+ const relativeModulePath = path.relative(fromDir, targetPath).split(path.sep).join("/");
160
+ return relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
161
+ }
127
162
  getOrCreateSpecifierMap() {
128
163
  if (this.cachedSpecifierMap) {
129
164
  return this.cachedSpecifierMap;
@@ -155,8 +190,7 @@ class JsxRuntimeBundleService {
155
190
  if (this.cachedJsxEntryModulePath) {
156
191
  return this.cachedJsxEntryModulePath;
157
192
  }
158
- const rootDir = this.config.rootDir ?? process.cwd();
159
- const artifactsDir = path.join(rootDir, "node_modules", ".cache", "ecopages-browser-runtime");
193
+ const artifactsDir = this.getArtifactsDir();
160
194
  const filePath = path.join(artifactsDir, "ecopages-jsx-esm-entry.mjs");
161
195
  const manifestPath = findPackageManifestPath("@ecopages/jsx");
162
196
  const packageDir = path.dirname(realpathSync(manifestPath));
@@ -166,16 +200,12 @@ class JsxRuntimeBundleService {
166
200
  ".",
167
201
  jsxPkg.exports?.["."]
168
202
  );
169
- const relativeModulePath = path.relative(artifactsDir, jsxModulePath).split(path.sep).join("/");
170
- const entryImportPath = relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
203
+ const entryImportPath = this.getEntryImportPath(artifactsDir, jsxModulePath);
171
204
  mkdirSync(artifactsDir, { recursive: true });
172
205
  writeFileSync(filePath, [`export * from '${entryImportPath}';`].join("\n"), "utf8");
173
206
  this.cachedJsxEntryModulePath = filePath;
174
207
  return filePath;
175
208
  }
176
- getRadiantBrowserRuntimeSpecifiers() {
177
- return this.getRadiantBrowserRuntimeModules().map(({ exportKey }) => `@ecopages/radiant${exportKey.slice(1)}`);
178
- }
179
209
  getRadiantBrowserRuntimeModules() {
180
210
  const manifestPath = findPackageManifestPath("@ecopages/radiant");
181
211
  const packageDir = path.dirname(realpathSync(manifestPath));
@@ -205,8 +235,7 @@ class JsxRuntimeBundleService {
205
235
  if (this.cachedRadiantEntryModulePath) {
206
236
  return this.cachedRadiantEntryModulePath;
207
237
  }
208
- const rootDir = this.config.rootDir ?? process.cwd();
209
- const artifactsDir = path.join(rootDir, "node_modules", ".cache", "ecopages-browser-runtime");
238
+ const artifactsDir = this.getArtifactsDir();
210
239
  const filePath = path.join(artifactsDir, "ecopages-radiant-esm-entry.mjs");
211
240
  const seenExports = /* @__PURE__ */ new Set();
212
241
  const statements = [];
@@ -216,8 +245,7 @@ class JsxRuntimeBundleService {
216
245
  if (exportNames.length === 0) {
217
246
  continue;
218
247
  }
219
- const relativeModulePath = path.relative(artifactsDir, module.modulePath).split(path.sep).join("/");
220
- const entryImportPath = relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
248
+ const entryImportPath = this.getEntryImportPath(artifactsDir, module.modulePath);
221
249
  statements.push(`export { ${exportNames.join(", ")} } from '${entryImportPath}';`);
222
250
  for (const exportName of exportNames) {
223
251
  seenExports.add(exportName);
@@ -229,5 +257,6 @@ class JsxRuntimeBundleService {
229
257
  }
230
258
  }
231
259
  export {
232
- JsxRuntimeBundleService
260
+ JsxRuntimeBundleService,
261
+ RADIANT_HYDRATOR_BOOTSTRAP_ATTRIBUTE
233
262
  };