@ecopages/ecopages-jsx 0.2.1-beta.0 → 0.3.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,155 @@
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;
1
+ import { HttpError } from '@ecopages/core/errors';
2
+ import { createIntegrationRenderer, } from '@ecopages/core/integration';
3
+ import { resolveQueuedTokens } from '@ecopages/core/integration';
4
+ import { createMarkupNodeLike } from '@ecopages/jsx';
5
+ import { renderToString, withServerCustomElementRenderHook } from '@ecopages/jsx/server';
6
+ import { isMdxFile, normalizeMdxPageModule, } from './ecopages-jsx-mdx.js';
7
+ import { recordEjsxHmrOwnership, withEjsxHmrOwnershipScope } from './ecopages-jsx-hmr-ownership.js';
8
+ import { EcopagesJsxRadiantSsrPolicy } from './ecopages-jsx-radiant-ssr-policy.js';
9
+ import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, CustomElementScriptPreloader } from '@ecopages/core/integration';
10
+ import { requireAppRuntime } from '@ecopages/core/integration';
11
+ import { createSharedServerModuleImporter } from '@ecopages/core/integration';
12
+ import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
13
+ /** Creates the hook-based Ecopages JSX renderer for one plugin configuration. */
14
+ export function createEcopagesJsxRenderer(jsxConfig = {}) {
15
+ return createIntegrationRenderer(ECOPAGES_JSX_PLUGIN_NAME, createEcopagesJsxRendererHookFactory(jsxConfig));
16
+ }
17
+ /** Creates JSX renderer hooks that can also configure a custom renderer subclass. */
18
+ export function createEcopagesJsxRendererHookFactory(jsxConfig = {}) {
19
+ const mdxExtensions = jsxConfig.mdxExtensions ?? ['.mdx'];
20
+ return ({ appConfig, resolveComponentDependencies, resolveDependencyPath, }) => {
21
+ const radiantSsrPolicy = new EcopagesJsxRadiantSsrPolicy(jsxConfig.radiantSsrEnabled ?? false);
22
+ const ssrScriptPreloader = new CustomElementScriptPreloader({
23
+ cacheScope: CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.ecopagesJsx,
24
+ preloadScopes: requireAppRuntime(appConfig).customElementSsrPreloadScopes,
25
+ collectForIntegration: ECOPAGES_JSX_PLUGIN_NAME,
26
+ enabled: jsxConfig.radiantSsrEnabled ?? false,
27
+ logLabel: 'ecopages-jsx',
28
+ resolveDependencyPath,
29
+ importServerModule: createSharedServerModuleImporter(appConfig),
30
+ });
31
+ const withCustomElementRenderHook = async (render) => await radiantSsrPolicy.withRuntime(() => withServerCustomElementRenderHook(({ instance, applyChildren, }) => instance ? radiantSsrPolicy.renderIntrinsicElementMarkup(instance, applyChildren) : undefined, render, { ownsChildren: true }));
32
+ const renderJsx = async (value) => await withCustomElementRenderHook(() => renderToString(value));
33
+ const normalizeForeignChildProps = (input) => {
34
+ if (!input.targetIntegration || input.targetIntegration === ECOPAGES_JSX_PLUGIN_NAME) {
35
+ return input.props;
36
+ }
37
+ return normalizeForeignChildren(input.props);
38
+ };
39
+ return {
40
+ beforeImportPageModule: async () => await radiantSsrPolicy.prepareRuntime(),
41
+ normalizePageModule: (file, module) => isMdxFile(file, mdxExtensions)
42
+ ? normalizeMdxPageModule(file, module)
43
+ : module,
44
+ beforeRender: async (options) => {
45
+ await radiantSsrPolicy.prepareRuntime();
46
+ await ssrScriptPreloader.preloadSsrScripts([
47
+ options.Page,
48
+ options.Layout,
49
+ options.HtmlTemplate,
50
+ ...(options.pageDependencyComponents ?? []),
51
+ ]);
52
+ },
53
+ renderComponent: async (input) => {
54
+ await radiantSsrPolicy.prepareRuntime();
55
+ await ssrScriptPreloader.preloadSsrScripts([input.component]);
56
+ return await withEjsxHmrOwnershipScope(async () => {
57
+ try {
58
+ if (typeof input.component !== 'function') {
59
+ throw new TypeError('JSX renderer expected a callable component.');
60
+ }
61
+ const component = input.component;
62
+ const componentProps = input.children === undefined
63
+ ? input.props
64
+ : {
65
+ ...input.props,
66
+ children: typeof input.children === 'string'
67
+ ? createMarkupNodeLike(input.children)
68
+ : input.children,
69
+ };
70
+ const content = await withCustomElementRenderHook(() => component(componentProps));
71
+ const html = await renderJsx(content);
72
+ const assets = input.component.config?.dependencies
73
+ ? await resolveComponentDependencies([input.component])
74
+ : undefined;
75
+ recordEjsxHmrOwnership([input.component]);
76
+ return {
77
+ html,
78
+ canAttachAttributes: true,
79
+ integrationName: ECOPAGES_JSX_PLUGIN_NAME,
80
+ assets,
81
+ };
82
+ }
83
+ catch (error) {
84
+ throw createRenderError('Error rendering component', error);
85
+ }
86
+ });
87
+ },
88
+ renderQueuedChildren: async (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => {
89
+ if (children === undefined) {
90
+ return {};
91
+ }
92
+ const html = typeof children === 'string' ? children : await renderJsx(children);
93
+ return { html: await resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken) };
94
+ },
95
+ normalizeForeignChildProps: (input) => normalizeForeignChildProps(input),
96
+ renderRoute: async (options, render) => await withEjsxHmrOwnershipScope(async () => {
97
+ const body = await render();
98
+ recordEjsxHmrOwnership([
99
+ options.Page,
100
+ options.Layout,
101
+ options.HtmlTemplate,
102
+ ...(options.pageDependencyComponents ?? []),
103
+ ]);
104
+ return body;
105
+ }),
106
+ };
107
+ };
108
+ }
109
+ export const EcopagesJsxRenderer = createEcopagesJsxRenderer();
110
+ function normalizeForeignChildren(props) {
111
+ if (!('children' in props)) {
112
+ return props;
21
113
  }
22
114
  const children = props.children;
23
- if (children === void 0 || typeof children === "string") {
24
- return props;
115
+ if (children === undefined || typeof children === 'string') {
116
+ return props;
25
117
  }
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
- };
118
+ if (typeof children === 'object' &&
119
+ children !== null &&
120
+ 'nodeType' in children &&
121
+ typeof children.nodeType === 'number' &&
122
+ 'outerHTML' in children &&
123
+ typeof children.outerHTML === 'string') {
124
+ return {
125
+ ...props,
126
+ children: children.outerHTML,
127
+ };
31
128
  }
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: [] };
129
+ if (isJsxSerializableForeignChild(children)) {
130
+ return {
131
+ ...props,
132
+ children: renderToString(children),
133
+ };
44
134
  }
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;
135
+ return props;
136
+ }
137
+ function isJsxSerializableForeignChild(children) {
138
+ if (Array.isArray(children)) {
139
+ return true;
53
140
  }
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);
149
- }
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);
190
- }
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);
210
- }
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
- }
141
+ if (children === null || typeof children !== 'object') {
142
+ return false;
143
+ }
144
+ if ('$$typeof' in children) {
145
+ return true;
146
+ }
147
+ return 'strings' in children && Array.isArray(children.strings);
148
+ }
149
+ function createRenderError(message, cause) {
150
+ if (HttpError.isHttpError(cause)) {
151
+ return cause;
152
+ }
153
+ const errorMessage = cause instanceof Error ? `${message}: ${cause.message}` : message;
154
+ return HttpError.InternalServerError(errorMessage);
237
155
  }
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,38 +1,34 @@
1
- import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
1
+ import { IntegrationPlugin, type IntegrationRendererBase, type IntegrationRuntimeContext, type EcoBuildPlugin } from '@ecopages/core/integration';
2
+ import { type AssetDefinition } from '@ecopages/core/integration';
2
3
  import type { JsxRenderable } from '@ecopages/jsx';
3
4
  import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
4
5
  import type { EcopagesJsxPluginOptions } from './ecopages-jsx.types.js';
5
6
  export type { EcopagesJsxMdxOptions, EcopagesJsxPluginOptions, EcopagesJsxRendererConfig, } from './ecopages-jsx.types.js';
6
7
  /** JSX integration plugin for Ecopages, supporting `.tsx` templates and optional Radiant web components. */
7
8
  export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable> {
8
- renderer: typeof EcopagesJsxRenderer;
9
+ renderer: new (options: import("@ecopages/core/integration").IntegrationRendererOptions) => import("@ecopages/core/integration").IntegrationRenderer<JsxRenderable> & {
10
+ name: string;
11
+ };
12
+ readonly bundlesPageDependencies = true;
9
13
  private includeRadiant;
10
- private mdxEnabled;
11
14
  private mdxCompilerOptions?;
12
15
  private mdxExtensions;
13
- private mdxLoaderPlugin?;
14
- /** Returns the build plugins required by the JSX integration. */
15
- get plugins(): EcoBuildPlugin[];
16
- /** Creates the renderer instance with the resolved JSX integration runtime options. */
17
- initializeRenderer(options?: {
18
- rendererModules?: unknown;
19
- }): EcopagesJsxRenderer;
20
- constructor(options?: EcopagesJsxPluginOptions);
21
- private getDependencies;
22
- /** Ensures MDX build hooks are ready before Ecopages collects contributions. */
23
- prepareBuildContributions(): Promise<void>;
24
- /**
25
- * Registers MDX tooling and completes the base integration setup.
26
- */
27
- setup(): Promise<void>;
28
- private ensureMdxLoaderPlugin;
29
16
  /**
30
- * Registers Bun's MDX loader at runtime setup time.
17
+ * Returns the build plugins required by the JSX integration.
31
18
  *
32
- * Build-time contribution collection may run where Bun is absent, so
33
- * this hook stays isolated from manifest preparation.
19
+ * @remarks
20
+ * Returns a new loader declaration without registering or initializing it. The build manifest owns the
21
+ * resulting instance for the app runtime.
34
22
  */
35
- private registerMdxBunPlugin;
23
+ plugins(app: IntegrationRuntimeContext['appConfig']): EcoBuildPlugin[];
24
+ /** Creates the renderer instance with the resolved JSX integration runtime options. */
25
+ createRenderer(base: IntegrationRendererBase): EcopagesJsxRenderer;
26
+ constructor(options?: EcopagesJsxPluginOptions);
27
+ private getDependencies;
28
+ /** Assigns grouped-build metadata for page-owned JSX content scripts. */
29
+ prepareAssetDependencies(dependencies: AssetDefinition[]): AssetDefinition[];
30
+ /** Registers JSX HMR strategy, dev-transform plugins, and Radiant SSR invalidation hooks. */
31
+ hmr(context: IntegrationRuntimeContext): void;
36
32
  }
37
33
  /**
38
34
  * Creates the JSX integration plugin with resolved defaults.