@ecopages/ecopages-jsx 0.2.0-beta.8 → 0.2.0-rc.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.
@@ -1,240 +1,284 @@
1
- import {
2
- IntegrationRenderer
3
- } from "@ecopages/core/route-renderer/integration-renderer";
4
- import { createMarkupNodeLike } from "@ecopages/jsx";
5
- import { renderToString, withServerCustomElementRenderHook } from "@ecopages/jsx/server";
6
- import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
7
- import {
8
- isMdxFile,
9
- normalizeMdxPageModule
10
- } from "./ecopages-jsx-mdx.js";
11
- import { EcopagesJsxRenderSession } from "./ecopages-jsx-render-session.js";
12
- import { EcopagesJsxRadiantSsrPolicy } from "./ecopages-jsx-radiant-ssr-policy.js";
13
- class EcopagesJsxRenderer extends IntegrationRenderer {
14
- name = ECOPAGES_JSX_PLUGIN_NAME;
15
- mdxExtensions;
16
- renderSession;
17
- radiantSsrPolicy;
18
- normalizeForeignChildProps(props) {
19
- if (!("children" in props)) {
20
- return props;
21
- }
22
- const children = props.children;
23
- if (children === void 0 || typeof children === "string") {
24
- return props;
25
- }
26
- if (typeof children === "object" && children !== null && "nodeType" in children && typeof children.nodeType === "number" && "outerHTML" in children && typeof children.outerHTML === "string") {
27
- return {
28
- ...props,
29
- children: children.outerHTML
30
- };
31
- }
32
- return {
33
- ...props,
34
- children: renderToString(children)
35
- };
36
- }
37
- /**
38
- * Re-renders queued JSX children inside the owning renderer so nested custom
39
- * elements and queued foreign subtrees contribute assets to the same frame.
40
- */
41
- async renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken) {
42
- if (children === void 0) {
43
- return { assets: [] };
1
+ import { IntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/integration-renderer';
2
+ import { getForeignSubtreeResolutionContextKey, resolveOwningIntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/foreign-child/owning-renderer-resolution';
3
+ import { createMarkupNodeLike } from '@ecopages/jsx';
4
+ import { renderToString, withServerCustomElementRenderHook } from '@ecopages/jsx/server';
5
+ import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
6
+ import { isMdxFile, normalizeMdxPageModule, } from './ecopages-jsx-mdx.js';
7
+ import { EcopagesJsxRenderSession } from './ecopages-jsx-render-session.js';
8
+ import { EcopagesJsxRadiantSsrPolicy } from './ecopages-jsx-radiant-ssr-policy.js';
9
+ /**
10
+ * Local Ecopages renderer for JSX templates in the docs app.
11
+ *
12
+ * This keeps the integration scoped to the docs package while supporting
13
+ * async page, layout, and html template components on the server.
14
+ */
15
+ export class EcopagesJsxRenderer extends IntegrationRenderer {
16
+ name = ECOPAGES_JSX_PLUGIN_NAME;
17
+ mdxExtensions;
18
+ renderSession;
19
+ radiantSsrPolicy;
20
+ /**
21
+ * Serializes foreign-child props for string-first boundaries.
22
+ *
23
+ * @remarks
24
+ * DOM nodes become `outerHTML`. JSX trees, arrays, and template results are
25
+ * stringified here. Other values are left for core foreign-child interception,
26
+ * which rejects opaque plain objects before queueing. Cross-integration
27
+ * children must use `EcoEmbed`.
28
+ */
29
+ normalizeForeignChildProps(props) {
30
+ if (!('children' in props)) {
31
+ return props;
32
+ }
33
+ const children = props.children;
34
+ if (children === undefined || typeof children === 'string') {
35
+ return props;
36
+ }
37
+ if (typeof children === 'object' &&
38
+ children !== null &&
39
+ 'nodeType' in children &&
40
+ typeof children.nodeType === 'number' &&
41
+ 'outerHTML' in children &&
42
+ typeof children.outerHTML === 'string') {
43
+ return {
44
+ ...props,
45
+ children: children.outerHTML,
46
+ };
47
+ }
48
+ if (this.isJsxSerializableForeignChild(children)) {
49
+ return {
50
+ ...props,
51
+ children: renderToString(children),
52
+ };
53
+ }
54
+ return props;
44
55
  }
45
- let assets = [];
46
- let html;
47
- if (typeof children === "string") {
48
- html = children;
49
- } else {
50
- const renderedChildren = await this.renderJsx(children);
51
- html = renderedChildren.html;
52
- assets = renderedChildren.assets;
56
+ /**
57
+ * Returns whether children are a JSX/template shape this renderer can
58
+ * serialize before foreign-child queueing.
59
+ */
60
+ isJsxSerializableForeignChild(children) {
61
+ if (Array.isArray(children)) {
62
+ return true;
63
+ }
64
+ if (children === null || typeof children !== 'object') {
65
+ return false;
66
+ }
67
+ if ('$$typeof' in children) {
68
+ return true;
69
+ }
70
+ return 'strings' in children && Array.isArray(children.strings);
53
71
  }
54
- html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(
55
- html,
56
- queuedResolutionsByToken,
57
- resolveToken
58
- );
59
- return {
60
- assets,
61
- html
62
- };
63
- }
64
- /**
65
- * Resolves queued foreign subtrees after JSX has been stringified.
66
- *
67
- * JSX content needs one extra render pass because child foreign subtrees may emit
68
- * additional browser assets while also replacing placeholder tokens.
69
- */
70
- async resolveOwnedForeignSubtreeHtml(html, runtimeContext) {
71
- return this.foreignSubtreeExecutionService.resolveQueuedHtml({
72
- currentIntegrationName: this.name,
73
- html,
74
- runtimeContext,
75
- queueLabel: "Ecopages JSX",
76
- getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
77
- applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
78
- dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
79
- renderQueuedChildren: async (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken)
80
- });
81
- }
82
- createForeignChildRuntime(options) {
83
- const runtime = super.createForeignChildRuntime(options);
84
- const interceptForeignChild = runtime.interceptForeignChild;
85
- const interceptForeignChildSync = runtime.interceptForeignChildSync;
86
- const wrapInput = (input) => ({
87
- ...input,
88
- props: input.targetIntegration && input.targetIntegration !== this.name ? this.normalizeForeignChildProps(input.props) : input.props
89
- });
90
- return {
91
- interceptForeignChild: interceptForeignChild ? (input) => interceptForeignChild(wrapInput(input)) : void 0,
92
- interceptForeignChildSync: interceptForeignChildSync ? (input) => interceptForeignChildSync(wrapInput(input)) : void 0
93
- };
94
- }
95
- constructor({
96
- appConfig,
97
- assetProcessingService,
98
- resolvedIntegrationDependencies,
99
- jsxConfig,
100
- runtimeOrigin
101
- }) {
102
- super({
103
- appConfig,
104
- assetProcessingService,
105
- resolvedIntegrationDependencies,
106
- runtimeOrigin
107
- });
108
- this.mdxExtensions = jsxConfig?.mdxExtensions ?? [".mdx"];
109
- this.renderSession = new EcopagesJsxRenderSession(
110
- (assets) => this.htmlTransformer.dedupeProcessedAssets(assets)
111
- );
112
- this.radiantSsrPolicy = jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(jsxConfig?.radiantSsrEnabled ?? false);
113
- }
114
- /** Returns whether the requested page file should be treated as MDX. */
115
- isMdxFile(filePath) {
116
- return isMdxFile(filePath, this.mdxExtensions);
117
- }
118
- async importPageFile(file, options) {
119
- return await this.withPreparedRadiantRuntime(async () => {
120
- const module = await super.importPageFile(file, options);
121
- return this.isMdxFile(file) ? normalizeMdxPageModule(file, module) : module;
122
- });
123
- }
124
- async render(options) {
125
- return await this.withPreparedRadiantRuntime(
126
- () => this.renderSession.withActiveScope(async () => {
127
- try {
128
- return await this.renderPageWithDocumentShell({
129
- page: {
130
- component: options.Page,
131
- props: {
132
- ...options.pageProps,
133
- locals: options.pageLocals
134
- }
135
- },
136
- layout: options.Layout ? {
137
- component: options.Layout,
138
- props: {
139
- ...options.pageProps,
140
- locals: options.locals
141
- }
142
- } : void 0,
143
- htmlTemplate: options.HtmlTemplate,
144
- metadata: options.metadata,
145
- pageProps: options.pageProps ?? {}
146
- });
147
- } catch (error) {
148
- throw this.createRenderError("Error rendering page", error);
72
+ /**
73
+ * Re-renders queued JSX children inside the owning renderer so nested custom
74
+ * elements and queued foreign subtrees contribute assets to the same frame.
75
+ */
76
+ async renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken) {
77
+ if (children === undefined) {
78
+ return { assets: [] };
149
79
  }
150
- })
151
- );
152
- }
153
- async renderComponent(input) {
154
- return await this.withPreparedRadiantRuntime(
155
- () => this.renderSession.withActiveScope(async () => {
156
- const assetFrame = this.renderSession.beginCollectedAssetFrame();
157
- try {
158
- if (typeof input.component !== "function") {
159
- throw new TypeError("JSX renderer expected a callable component.");
160
- }
161
- const component = input.component;
162
- const componentProps = input.children === void 0 ? input.props : {
163
- ...input.props,
164
- children: typeof input.children === "string" ? createMarkupNodeLike(input.children) : input.children
165
- };
166
- const content = await this.withCustomElementRenderHook(() => component(componentProps));
167
- const rendered = await this.renderJsx(content);
168
- const queuedForeignSubtreeResolution = await this.resolveOwnedForeignSubtreeHtml(
169
- rendered.html,
170
- this.getQueuedForeignSubtreeResolutionContext(
171
- input
172
- )
173
- );
174
- const componentAssets = input.component.config?.dependencies && typeof this.assetProcessingService?.processDependencies === "function" ? await this.processComponentDependencies([input.component]) : [];
175
- const assets = this.htmlTransformer.dedupeProcessedAssets([
176
- ...this.renderSession.endCollectedAssetFrame(assetFrame),
177
- ...queuedForeignSubtreeResolution.assets,
178
- ...componentAssets
179
- ]);
180
- return {
181
- html: queuedForeignSubtreeResolution.html,
182
- canAttachAttributes: true,
183
- rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
184
- integrationName: this.name,
185
- assets
186
- };
187
- } catch (error) {
188
- this.renderSession.endCollectedAssetFrame(assetFrame);
189
- throw this.createRenderError("Error rendering component", error);
80
+ let assets = [];
81
+ let html;
82
+ if (typeof children === 'string') {
83
+ html = children;
190
84
  }
191
- })
192
- );
193
- }
194
- async renderToResponse(view, props, ctx) {
195
- return await this.withPreparedRadiantRuntime(
196
- () => this.renderSession.withActiveScope(async () => {
197
- try {
198
- if (typeof view !== "function") {
199
- throw new TypeError("JSX renderer expected a callable view component.");
200
- }
201
- const viewComponent = view;
202
- return await this.renderViewWithDocumentShell({
203
- view: viewComponent,
204
- props,
205
- ctx,
206
- layout: viewComponent.config?.layout
207
- });
208
- } catch (error) {
209
- throw this.createRenderError("Error rendering view", error);
85
+ else {
86
+ const renderedChildren = await this.renderJsx(children);
87
+ html = renderedChildren.html;
88
+ assets = renderedChildren.assets;
210
89
  }
211
- })
212
- );
213
- }
214
- async renderJsx(value) {
215
- const collectedAssets = [];
216
- const html = await this.withCustomElementRenderHook(() => renderToString(value));
217
- const dedupedAssets = this.renderSession.recordCollectedAssets(collectedAssets);
218
- return {
219
- assets: dedupedAssets,
220
- html
221
- };
222
- }
223
- async withCustomElementRenderHook(render) {
224
- return await this.radiantSsrPolicy.withRuntime(
225
- () => withServerCustomElementRenderHook(this.createIntrinsicCustomElementRenderHook(), render)
226
- );
227
- }
228
- async withPreparedRadiantRuntime(render) {
229
- await this.radiantSsrPolicy.prepareRuntime();
230
- return await render();
231
- }
232
- createIntrinsicCustomElementRenderHook() {
233
- return ({ instance }) => {
234
- return instance ? this.radiantSsrPolicy.renderIntrinsicElementMarkup(instance) : void 0;
235
- };
236
- }
90
+ html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken);
91
+ return {
92
+ assets,
93
+ html,
94
+ };
95
+ }
96
+ createForeignChildRuntime(options) {
97
+ const runtime = super.createForeignChildRuntime(options);
98
+ const interceptForeignChild = runtime.interceptForeignChild;
99
+ const interceptForeignChildSync = runtime.interceptForeignChildSync;
100
+ const wrapInput = (input) => ({
101
+ ...input,
102
+ props: input.targetIntegration && input.targetIntegration !== this.name
103
+ ? this.normalizeForeignChildProps(input.props)
104
+ : input.props,
105
+ });
106
+ return {
107
+ interceptForeignChild: interceptForeignChild
108
+ ? (input) => interceptForeignChild(wrapInput(input))
109
+ : undefined,
110
+ interceptForeignChildSync: interceptForeignChildSync
111
+ ? (input) => interceptForeignChildSync(wrapInput(input))
112
+ : undefined,
113
+ };
114
+ }
115
+ constructor({ appConfig, assetProcessingService, resolvedIntegrationDependencies, jsxConfig, runtimeOrigin, }) {
116
+ super({
117
+ appConfig,
118
+ assetProcessingService,
119
+ resolvedIntegrationDependencies,
120
+ runtimeOrigin,
121
+ });
122
+ this.mdxExtensions = jsxConfig?.mdxExtensions ?? ['.mdx'];
123
+ this.renderSession = new EcopagesJsxRenderSession((assets) => this.htmlTransformer.dedupeProcessedAssets(assets));
124
+ this.radiantSsrPolicy =
125
+ jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(jsxConfig?.radiantSsrEnabled ?? false);
126
+ }
127
+ /** Returns whether the requested page file should be treated as MDX. */
128
+ isMdxFile(filePath) {
129
+ return isMdxFile(filePath, this.mdxExtensions);
130
+ }
131
+ async importPageFile(file, options) {
132
+ return await this.withPreparedRadiantRuntime(async () => {
133
+ const module = (await super.importPageFile(file, options));
134
+ return this.isMdxFile(file) ? normalizeMdxPageModule(file, module) : module;
135
+ });
136
+ }
137
+ async render(options) {
138
+ return await this.withPreparedRadiantRuntime(async () => await this.renderSession.withActiveScope(async () => {
139
+ try {
140
+ const result = await this.renderPageWithDocumentShell({
141
+ page: {
142
+ component: options.Page,
143
+ props: {
144
+ ...options.pageProps,
145
+ locals: options.pageLocals,
146
+ },
147
+ },
148
+ layout: options.Layout
149
+ ? {
150
+ component: options.Layout,
151
+ props: {
152
+ ...options.pageProps,
153
+ locals: options.locals,
154
+ },
155
+ }
156
+ : undefined,
157
+ htmlTemplate: options.HtmlTemplate,
158
+ metadata: options.metadata,
159
+ pageProps: options.pageProps ?? {},
160
+ });
161
+ this.recordHmrOwnership([
162
+ options.Page,
163
+ options.Layout,
164
+ options.HtmlTemplate,
165
+ ...(options.resolvedPageDependencyComponents ?? []),
166
+ ]);
167
+ return result;
168
+ }
169
+ catch (error) {
170
+ throw this.createRenderError('Error rendering page', error);
171
+ }
172
+ }));
173
+ }
174
+ async renderComponent(input) {
175
+ return await this.withPreparedRadiantRuntime(async () => await this.renderSession.withActiveScope(async () => {
176
+ const assetFrame = this.renderSession.beginCollectedAssetFrame();
177
+ try {
178
+ if (typeof input.component !== 'function') {
179
+ throw new TypeError('JSX renderer expected a callable component.');
180
+ }
181
+ const component = input.component;
182
+ const componentProps = input.children === undefined
183
+ ? input.props
184
+ : {
185
+ ...input.props,
186
+ children: typeof input.children === 'string'
187
+ ? createMarkupNodeLike(input.children)
188
+ : input.children,
189
+ };
190
+ const content = await this.withCustomElementRenderHook(() => component(componentProps));
191
+ const rendered = await this.renderJsx(content);
192
+ const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
193
+ currentIntegrationName: this.name,
194
+ html: rendered.html,
195
+ runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(input, getForeignSubtreeResolutionContextKey(this.name)),
196
+ queueLabel: 'Ecopages JSX',
197
+ getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
198
+ appConfig: this.appConfig,
199
+ runtimeOrigin: this.runtimeOrigin,
200
+ currentIntegrationName: this.name,
201
+ currentRenderer: this,
202
+ integrationName,
203
+ cache: rendererCache,
204
+ }),
205
+ applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
206
+ dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
207
+ renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken),
208
+ });
209
+ const componentAssets = input.component.config?.dependencies &&
210
+ typeof this.assetProcessingService?.processDependencies === 'function'
211
+ ? await this.processComponentDependencies([input.component])
212
+ : [];
213
+ const assets = this.htmlTransformer.dedupeProcessedAssets([
214
+ ...this.renderSession.endCollectedAssetFrame(assetFrame),
215
+ ...queuedForeignSubtreeResolution.assets,
216
+ ...componentAssets,
217
+ ]);
218
+ this.recordHmrOwnership([input.component]);
219
+ return this.finalizeIslandComponentRender(input, {
220
+ html: queuedForeignSubtreeResolution.html,
221
+ canAttachAttributes: true,
222
+ rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
223
+ integrationName: this.name,
224
+ assets,
225
+ });
226
+ }
227
+ catch (error) {
228
+ this.renderSession.endCollectedAssetFrame(assetFrame);
229
+ throw this.createRenderError('Error rendering component', error);
230
+ }
231
+ }));
232
+ }
233
+ async renderToResponse(view, props, ctx) {
234
+ return await this.withPreparedRadiantRuntime(async () => await this.renderSession.withActiveScope(async () => {
235
+ try {
236
+ if (typeof view !== 'function') {
237
+ throw new TypeError('JSX renderer expected a callable view component.');
238
+ }
239
+ const viewComponent = view;
240
+ const layouts = viewComponent.config?.layouts;
241
+ const response = await this.renderViewWithDocumentShell({
242
+ view: viewComponent,
243
+ props: props,
244
+ ctx,
245
+ layout: layouts?.[layouts.length - 1],
246
+ });
247
+ this.recordHmrOwnership([view]);
248
+ return response;
249
+ }
250
+ catch (error) {
251
+ throw this.createRenderError('Error rendering view', error);
252
+ }
253
+ }));
254
+ }
255
+ async renderJsx(value) {
256
+ const collectedAssets = [];
257
+ const html = await this.withCustomElementRenderHook(() => renderToString(value));
258
+ const dedupedAssets = this.renderSession.recordCollectedAssets(collectedAssets);
259
+ return {
260
+ assets: dedupedAssets,
261
+ html,
262
+ };
263
+ }
264
+ async withCustomElementRenderHook(render) {
265
+ return await this.radiantSsrPolicy.withRuntime(() => withServerCustomElementRenderHook(this.createIntrinsicCustomElementRenderHook(), render));
266
+ }
267
+ async withPreparedRadiantRuntime(render) {
268
+ await this.radiantSsrPolicy.prepareRuntime();
269
+ return await render();
270
+ }
271
+ createIntrinsicCustomElementRenderHook() {
272
+ return ({ instance }) => {
273
+ return instance ? this.radiantSsrPolicy.renderIntrinsicElementMarkup(instance) : undefined;
274
+ };
275
+ }
276
+ /**
277
+ * Records the source files that produced the current render so
278
+ * {@link EcopagesJsxHmrStrategy} can match later watcher events without
279
+ * re-walking the component tree.
280
+ */
281
+ recordHmrOwnership(components) {
282
+ this.renderSession.mergeHmrOwnership(components);
283
+ }
237
284
  }
238
- export {
239
- EcopagesJsxRenderer
240
- };
@@ -1,4 +1,7 @@
1
- const ECOPAGES_JSX_PLUGIN_NAME = "ecopages-jsx";
2
- export {
3
- ECOPAGES_JSX_PLUGIN_NAME
4
- };
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 const ECOPAGES_JSX_PLUGIN_NAME = 'ecopages-jsx';
@@ -1,4 +1,7 @@
1
1
  import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
2
+ import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
3
+ import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
4
+ import type { IHmrManager } from '@ecopages/core';
2
5
  import type { JsxRenderable } from '@ecopages/jsx';
3
6
  import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
4
7
  import type { EcopagesJsxPluginOptions } from './ecopages-jsx.types.js';
@@ -21,6 +24,20 @@ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable>
21
24
  private getDependencies;
22
25
  /** Ensures MDX build hooks are ready before Ecopages collects contributions. */
23
26
  prepareBuildContributions(): Promise<void>;
27
+ /** Assigns grouped-build metadata for page-owned JSX content scripts. */
28
+ prepareAssetDependencies(dependencies: AssetDefinition[]): AssetDefinition[];
29
+ /** Registers JSX HMR strategy, dev-transform plugins, and Radiant SSR invalidation hooks. */
30
+ setHmrManager(hmrManager: IHmrManager): void;
31
+ /**
32
+ * Returns the JSX-integration HMR strategy.
33
+ *
34
+ * @remarks
35
+ * The strategy handles `layout-update` broadcasts for page sources, layout
36
+ * sources, and any component file in the active render tree. It defers
37
+ * registered script entrypoints to the generic JS strategy and includes /
38
+ * explicit server views to `ServerRenderedTemplateHmrStrategy`.
39
+ */
40
+ getHmrStrategy(): HmrStrategy | undefined;
24
41
  /**
25
42
  * Registers MDX tooling and completes the base integration setup.
26
43
  */