@ecopages/ecopages-jsx 0.2.0-alpha.12

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 ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## [UNRELEASED] -- TBD
4
+
5
+ ### Features
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
11
+
12
+ ### Bug Fixes
13
+
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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Andrea Zanenghi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,60 @@
1
+ # @ecopages/ecopages-jsx
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.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bunx jsr add @ecopages/ecopages-jsx @ecopages/radiant
9
+ ```
10
+
11
+ `@ecopages/radiant` is a required peer dependency for this package.
12
+
13
+ ## Usage
14
+
15
+ Register the `ecopagesJsxPlugin` in your `eco.config.ts`.
16
+
17
+ ```ts
18
+ import { ConfigBuilder } from '@ecopages/core/config-builder';
19
+ import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
20
+
21
+ const config = await new ConfigBuilder()
22
+ .setBaseUrl(import.meta.env.ECOPAGES_BASE_URL)
23
+ .setIntegrations([ecopagesJsxPlugin()])
24
+ .build();
25
+
26
+ export default config;
27
+ ```
28
+
29
+ ## Radiant Support
30
+
31
+ Radiant runtime bundles are enabled by default so JSX pages can render and hydrate Radiant custom elements.
32
+
33
+ ```ts
34
+ ecopagesJsxPlugin({
35
+ radiant: true,
36
+ });
37
+ ```
38
+
39
+ Set `radiant: false` when your JSX pages do not need the Radiant browser runtime on a given app.
40
+
41
+ ## MDX Support
42
+
43
+ Enable MDX to treat `.mdx` files as JSX routes compiled against the `@ecopages/jsx` runtime.
44
+
45
+ ```ts
46
+ import { ConfigBuilder } from '@ecopages/core/config-builder';
47
+ import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
48
+
49
+ const config = await new ConfigBuilder()
50
+ .setIntegrations([
51
+ ecopagesJsxPlugin({
52
+ mdx: {
53
+ enabled: true,
54
+ },
55
+ }),
56
+ ])
57
+ .build();
58
+
59
+ export default config;
60
+ ```
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@ecopages/ecopages-jsx",
3
+ "version": "0.2.0-alpha.12",
4
+ "description": "JSX integration plugin for Ecopages",
5
+ "keywords": [
6
+ "ecopages",
7
+ "jsx",
8
+ "plugin"
9
+ ],
10
+ "license": "MIT",
11
+ "main": "./src/ecopages-jsx.plugin.js",
12
+ "types": "./src/ecopages-jsx.plugin.d.ts",
13
+ "type": "module",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/ecopages/ecopages.git",
17
+ "directory": "packages/integrations/ecopages-jsx"
18
+ },
19
+ "dependencies": {
20
+ "@mdx-js/mdx": "^3.1.1",
21
+ "vfile": "^6.0.3"
22
+ },
23
+ "peerDependencies": {
24
+ "@ecopages/core": "0.2.0-alpha.12",
25
+ "@ecopages/jsx": "^0.3.0-alpha.0",
26
+ "@ecopages/radiant": "^0.3.0-alpha.0"
27
+ }
28
+ }
@@ -0,0 +1,65 @@
1
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComponentConfig, EcoPageFile, GetMetadata, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
2
+ import type { EcoPagesAppConfig } from '@ecopages/core/internal-types';
3
+ import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
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
+ };
10
+ type MdxPageModule = EcoPageFile<{
11
+ config?: EcoComponentConfig;
12
+ layout?: EcoComponent;
13
+ getMetadata?: GetMetadata;
14
+ }>;
15
+ /**
16
+ * Local Ecopages renderer for JSX templates in the docs app.
17
+ *
18
+ * This keeps the integration scoped to the docs package while supporting
19
+ * async page, layout, and html template components on the server.
20
+ */
21
+ 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
+ name: string;
33
+ static mdxExtensions: string[];
34
+ private intrinsicCustomElementAssets;
35
+ private collectedAssetFrames;
36
+ private radiantSsrEnabled;
37
+ constructor({ appConfig, assetProcessingService, resolvedIntegrationDependencies, runtimeOrigin, }: {
38
+ appConfig: EcoPagesAppConfig;
39
+ assetProcessingService: AssetProcessingService;
40
+ resolvedIntegrationDependencies: ProcessedAsset[];
41
+ runtimeOrigin: string;
42
+ });
43
+ /** Returns whether the requested page file should be treated as MDX. */
44
+ isMdxFile(filePath: string): boolean;
45
+ /**
46
+ * Supplies the intrinsic custom-element assets discovered by the plugin so
47
+ * component renders can attach the correct client scripts.
48
+ */
49
+ setIntrinsicCustomElementAssets(assetsByTagName: Map<string, readonly ProcessedAsset[]>): void;
50
+ /** Enables the Radiant SSR light-DOM shim for this renderer instance. */
51
+ setRadiantSsrEnabled(enabled: boolean): void;
52
+ protected importPageFile(file: string): Promise<MdxPageModule>;
53
+ render(options: IntegrationRendererRenderOptions<JsxRenderable>): Promise<RouteRendererBody>;
54
+ renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
55
+ renderToResponse<P = any>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
56
+ private resolveViewMetadata;
57
+ private renderDocument;
58
+ private beginCollectedAssetFrame;
59
+ private endCollectedAssetFrame;
60
+ private mergeCollectedAssets;
61
+ private renderJsx;
62
+ private renderEcoComponent;
63
+ private createIntrinsicCustomElementRenderHook;
64
+ }
65
+ export {};
@@ -0,0 +1,298 @@
1
+ import { rapidhash } from "@ecopages/core/hash";
2
+ import { IntegrationRenderer } from "@ecopages/core/route-renderer/integration-renderer";
3
+ import {
4
+ serializeTemplateShape
5
+ } from "@ecopages/core/route-renderer/template-serialization";
6
+ import { renderToString, withServerCustomElementRenderHook } from "@ecopages/jsx/server";
7
+ import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.plugin.js";
8
+ 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
+ async function ensureRadiantLightDomShimInstalled() {
18
+ if (!radiantLightDomShimInstallPromise) {
19
+ radiantLightDomShimInstallPromise = import("@ecopages/radiant/server/light-dom-shim").then((module) => {
20
+ module.installLightDomShim();
21
+ }).then(() => void 0);
22
+ }
23
+ await radiantLightDomShimInstallPromise;
24
+ }
25
+ const isEcoFunctionComponent = (component) => {
26
+ return typeof component === "function";
27
+ };
28
+ const renderComponent = async (component, props) => {
29
+ return await component(props);
30
+ };
31
+ const createEcoMeta = (file) => ({
32
+ id: String(rapidhash(file)),
33
+ file,
34
+ integration: ECOPAGES_JSX_PLUGIN_NAME
35
+ });
36
+ const wrapMdxPage = (page, {
37
+ config,
38
+ metadata
39
+ }) => {
40
+ const wrappedPage = (async (props) => renderComponent(page, props));
41
+ wrappedPage.config = config;
42
+ if (metadata) {
43
+ wrappedPage.metadata = metadata;
44
+ }
45
+ return wrappedPage;
46
+ };
47
+ 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
+ name = ECOPAGES_JSX_PLUGIN_NAME;
59
+ static mdxExtensions = [".mdx"];
60
+ intrinsicCustomElementAssets = /* @__PURE__ */ new Map();
61
+ collectedAssetFrames = [];
62
+ radiantSsrEnabled = false;
63
+ constructor({
64
+ appConfig,
65
+ assetProcessingService,
66
+ resolvedIntegrationDependencies,
67
+ runtimeOrigin
68
+ }) {
69
+ super({
70
+ appConfig,
71
+ assetProcessingService,
72
+ resolvedIntegrationDependencies,
73
+ runtimeOrigin
74
+ });
75
+ }
76
+ /** Returns whether the requested page file should be treated as MDX. */
77
+ isMdxFile(filePath) {
78
+ return EcopagesJsxRenderer.mdxExtensions.some((ext) => filePath.endsWith(ext));
79
+ }
80
+ /**
81
+ * Supplies the intrinsic custom-element assets discovered by the plugin so
82
+ * component renders can attach the correct client scripts.
83
+ */
84
+ setIntrinsicCustomElementAssets(assetsByTagName) {
85
+ this.intrinsicCustomElementAssets = assetsByTagName;
86
+ }
87
+ /** Enables the Radiant SSR light-DOM shim for this renderer instance. */
88
+ setRadiantSsrEnabled(enabled) {
89
+ this.radiantSsrEnabled = enabled;
90
+ }
91
+ async importPageFile(file) {
92
+ const module = await super.importPageFile(file);
93
+ if (!this.isMdxFile(file)) {
94
+ return module;
95
+ }
96
+ const Page = module.default;
97
+ const normalizedConfig = {
98
+ ...module.config ?? Page.config ?? {},
99
+ ...module.layout ? { layout: module.layout } : {},
100
+ __eco: module.config?.__eco ?? Page.config?.__eco ?? createEcoMeta(file)
101
+ };
102
+ const wrappedPage = wrapMdxPage(Page, {
103
+ config: normalizedConfig,
104
+ metadata: module.getMetadata ?? Page.metadata
105
+ });
106
+ return {
107
+ ...module,
108
+ default: wrappedPage,
109
+ config: normalizedConfig
110
+ };
111
+ }
112
+ async render(options) {
113
+ const assetFrame = this.beginCollectedAssetFrame();
114
+ try {
115
+ const page = await this.renderEcoComponent(options.Page, {
116
+ ...options.pageProps,
117
+ locals: options.pageLocals
118
+ });
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
+ } catch (error) {
136
+ this.endCollectedAssetFrame(assetFrame);
137
+ throw this.createRenderError("Error rendering page", error);
138
+ }
139
+ }
140
+ async renderComponent(input) {
141
+ const assetFrame = this.beginCollectedAssetFrame();
142
+ try {
143
+ if (!isEcoFunctionComponent(input.component)) {
144
+ throw new TypeError("JSX renderer expected a callable component.");
145
+ }
146
+ const props = input.children === void 0 ? input.props : { ...input.props, children: input.children };
147
+ const content = await this.renderEcoComponent(
148
+ input.component,
149
+ props
150
+ );
151
+ const rendered = await this.renderJsx(content);
152
+ const componentAssets = input.component.config?.dependencies && typeof this.assetProcessingService?.processDependencies === "function" ? await this.processComponentDependencies([input.component]) : [];
153
+ const html = rendered.html;
154
+ const assets = this.htmlTransformer.dedupeProcessedAssets([
155
+ ...this.endCollectedAssetFrame(assetFrame),
156
+ ...componentAssets
157
+ ]);
158
+ return {
159
+ html,
160
+ canAttachAttributes: true,
161
+ rootTag: this.getRootTagName(html),
162
+ integrationName: this.name,
163
+ assets
164
+ };
165
+ } catch (error) {
166
+ this.endCollectedAssetFrame(assetFrame);
167
+ throw this.createRenderError("Error rendering component", error);
168
+ }
169
+ }
170
+ async renderToResponse(view, props, ctx) {
171
+ try {
172
+ if (!isEcoFunctionComponent(view)) {
173
+ throw new TypeError("JSX renderer expected a callable view component.");
174
+ }
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;
190
+ });
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
+ } catch (error) {
200
+ throw this.createRenderError("Error rendering view", error);
201
+ }
202
+ }
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
+ beginCollectedAssetFrame() {
233
+ const frame = [];
234
+ this.collectedAssetFrames.push(frame);
235
+ return frame;
236
+ }
237
+ endCollectedAssetFrame(frame) {
238
+ const activeFrame = this.collectedAssetFrames.pop();
239
+ if (!activeFrame || activeFrame !== frame) {
240
+ return this.htmlTransformer.dedupeProcessedAssets(frame);
241
+ }
242
+ return this.htmlTransformer.dedupeProcessedAssets(activeFrame);
243
+ }
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
+ async renderJsx(value) {
253
+ if (this.radiantSsrEnabled) {
254
+ await ensureRadiantLightDomShimInstalled();
255
+ }
256
+ const collectedAssets = [];
257
+ const html = withServerCustomElementRenderHook(
258
+ this.createIntrinsicCustomElementRenderHook(collectedAssets),
259
+ () => renderToString(value)
260
+ );
261
+ const dedupedAssets = this.htmlTransformer.dedupeProcessedAssets(collectedAssets);
262
+ const activeFrame = this.collectedAssetFrames[this.collectedAssetFrames.length - 1];
263
+ if (activeFrame) {
264
+ activeFrame.push(...dedupedAssets);
265
+ }
266
+ return {
267
+ assets: dedupedAssets,
268
+ html
269
+ };
270
+ }
271
+ async renderEcoComponent(component, props) {
272
+ if (this.radiantSsrEnabled) {
273
+ await ensureRadiantLightDomShimInstalled();
274
+ }
275
+ const collectedAssets = [];
276
+ const rendered = await withServerCustomElementRenderHook(
277
+ this.createIntrinsicCustomElementRenderHook(collectedAssets),
278
+ () => renderComponent(component, props)
279
+ );
280
+ const activeFrame = this.collectedAssetFrames[this.collectedAssetFrames.length - 1];
281
+ if (activeFrame) {
282
+ activeFrame.push(...this.htmlTransformer.dedupeProcessedAssets(collectedAssets));
283
+ }
284
+ return rendered;
285
+ }
286
+ createIntrinsicCustomElementRenderHook(target) {
287
+ return ({ tagName }) => {
288
+ const assets = this.intrinsicCustomElementAssets.get(tagName);
289
+ if (assets) {
290
+ target.push(...assets);
291
+ }
292
+ return void 0;
293
+ };
294
+ }
295
+ }
296
+ export {
297
+ EcopagesJsxRenderer
298
+ };
@@ -0,0 +1,116 @@
1
+ import type { CompileOptions } from '@mdx-js/mdx';
2
+ import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
3
+ import { IntegrationPlugin, type IntegrationPluginConfig } from '@ecopages/core/plugins/integration-plugin';
4
+ import type { JsxRenderable } from '@ecopages/jsx';
5
+ import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
6
+ type MdxPluginList = NonNullable<CompileOptions['remarkPlugins']>;
7
+ type MdxCompileOptions = Omit<CompileOptions, 'jsxImportSource' | 'jsxRuntime' | 'remarkPlugins' | 'rehypePlugins' | 'recmaPlugins'> & {
8
+ remarkPlugins?: MdxPluginList;
9
+ rehypePlugins?: MdxPluginList;
10
+ recmaPlugins?: MdxPluginList;
11
+ };
12
+ /**
13
+ * Stable integration name shared by the JSX plugin and renderer.
14
+ *
15
+ * Ecopages uses this identifier to match route files, renderer instances, and
16
+ * cross-integration component boundaries.
17
+ */
18
+ export declare const ECOPAGES_JSX_PLUGIN_NAME = "ecopages-jsx";
19
+ /**
20
+ * MDX configuration for the JSX integration.
21
+ *
22
+ * Mirrors Ecopages' built-in combined integration pattern where a single
23
+ * JSX-capable plugin can own both `.tsx` and `.mdx` route files.
24
+ */
25
+ export type EcopagesJsxMdxOptions = {
26
+ /** Enables MDX file handling inside the JSX integration. */
27
+ enabled: boolean;
28
+ /** Additional MDX compiler options. JSX runtime fields are managed by the integration. */
29
+ compilerOptions?: MdxCompileOptions;
30
+ /** Extra remark plugins appended to `compilerOptions.remarkPlugins`. */
31
+ remarkPlugins?: MdxPluginList;
32
+ /** Extra rehype plugins appended to `compilerOptions.rehypePlugins`. */
33
+ rehypePlugins?: MdxPluginList;
34
+ /** Extra recma plugins appended to `compilerOptions.recmaPlugins`. */
35
+ recmaPlugins?: MdxPluginList;
36
+ /** Custom file extensions to treat as MDX. */
37
+ extensions?: string[];
38
+ };
39
+ /** Options for the JSX integration plugin. */
40
+ export type EcopagesJsxPluginOptions = Omit<IntegrationPluginConfig, 'name' | 'extensions'> & {
41
+ /** Optional JSX route extensions. Defaults to `.tsx`. */
42
+ extensions?: string[];
43
+ /**
44
+ * Whether to include the `@ecopages/radiant` and `@ecopages/signals` vendor
45
+ * bundles and bare-specifier mappings.
46
+ *
47
+ * Set to `false` when pages do not use Radiant web components.
48
+ * @default true
49
+ */
50
+ radiant?: boolean;
51
+ /** Optional MDX integration configuration. */
52
+ mdx?: EcopagesJsxMdxOptions;
53
+ };
54
+ /** JSX integration plugin for Ecopages, supporting `.tsx` templates and optional Radiant web components. */
55
+ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable> {
56
+ /** Renderer implementation used for JSX and MDX routes. */
57
+ renderer: IntegrationPlugin<JsxRenderable>["renderer"];
58
+ private intrinsicCustomElementAssets;
59
+ private includeRadiant;
60
+ private mdxEnabled;
61
+ private mdxCompilerOptions?;
62
+ private mdxExtensions;
63
+ private mdxLoaderPlugin?;
64
+ private runtimeBundleService;
65
+ private runtimeSpecifierMap;
66
+ private runtimeDepsInitialized;
67
+ /** Returns the build plugins required by the JSX integration. */
68
+ get plugins(): EcoBuildPlugin[];
69
+ /** Returns the browser-only build plugins required by the JSX integration. */
70
+ get browserBuildPlugins(): EcoBuildPlugin[];
71
+ /**
72
+ * Exposes the bare-module specifier map used by the import map.
73
+ *
74
+ * Client bundles keep these imports external so the browser can load the
75
+ * shared runtime packages from the generated vendor assets.
76
+ */
77
+ getRuntimeSpecifierMap(): Record<string, string>;
78
+ /**
79
+ * Creates the renderer instance and attaches the discovered intrinsic custom
80
+ * element assets before the renderer handles any requests.
81
+ */
82
+ initializeRenderer(): EcopagesJsxRenderer;
83
+ /**
84
+ * Creates a JSX integration plugin.
85
+ *
86
+ * When MDX is enabled, the plugin also claims the configured MDX extensions
87
+ * and prepares compiler options around the shared `@ecopages/jsx` runtime.
88
+ */
89
+ constructor(options?: EcopagesJsxPluginOptions);
90
+ /** Ensures MDX build hooks are ready before Ecopages collects contributions. */
91
+ prepareBuildContributions(): Promise<void>;
92
+ /**
93
+ * Registers MDX tooling, discovers intrinsic custom-element assets, and then
94
+ * completes the base integration setup.
95
+ */
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
+ private ensureMdxLoaderPlugin;
106
+ private setupMdxBunPlugin;
107
+ private buildIntrinsicCustomElementAssetRegistry;
108
+ private collectScriptEntryFiles;
109
+ private resolveIntrinsicCustomElementAsset;
110
+ private extractIntrinsicCustomElementTagNames;
111
+ }
112
+ /**
113
+ * Creates the JSX integration plugin.
114
+ */
115
+ export declare const ecopagesJsxPlugin: (options?: EcopagesJsxPluginOptions) => EcopagesJsxPlugin;
116
+ export {};
@@ -0,0 +1,251 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { IntegrationPlugin } from "@ecopages/core/plugins/integration-plugin";
4
+ import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
5
+ import { VFile } from "vfile";
6
+ import { EcopagesJsxRenderer } from "./ecopages-jsx-renderer.js";
7
+ import { JsxRuntimeBundleService } from "./services/jsx-runtime-bundle.service.js";
8
+ const mergePluginLists = (...lists) => {
9
+ const merged = lists.flatMap((list) => list ? [...list] : []);
10
+ return merged.length > 0 ? merged : void 0;
11
+ };
12
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ const createMdxLoaderPlugin = (compilerOptions, extensions) => {
14
+ const escapedExts = extensions.map(escapeRegex);
15
+ const filter = new RegExp(`(${escapedExts.join("|")})(\\?.*)?$`);
16
+ return {
17
+ name: "ecopages-jsx-mdx-loader",
18
+ setup(build) {
19
+ build.onLoad({ filter }, async (args) => {
20
+ const { compile } = await import("@mdx-js/mdx");
21
+ const filePath = args.path.includes("?") ? args.path.split("?")[0] : args.path;
22
+ const source = await readFile(filePath, "utf-8");
23
+ const compiled = await compile(new VFile({ value: source, path: filePath }), compilerOptions);
24
+ return {
25
+ contents: String(compiled.value),
26
+ loader: "js",
27
+ resolveDir: path.dirname(filePath)
28
+ };
29
+ });
30
+ }
31
+ };
32
+ };
33
+ const ECOPAGES_JSX_PLUGIN_NAME = "ecopages-jsx";
34
+ class EcopagesJsxPlugin extends IntegrationPlugin {
35
+ /** Renderer implementation used for JSX and MDX routes. */
36
+ renderer = EcopagesJsxRenderer;
37
+ intrinsicCustomElementAssets = /* @__PURE__ */ new Map();
38
+ includeRadiant;
39
+ mdxEnabled;
40
+ mdxCompilerOptions;
41
+ mdxExtensions;
42
+ mdxLoaderPlugin;
43
+ runtimeBundleService;
44
+ runtimeSpecifierMap = {};
45
+ runtimeDepsInitialized = false;
46
+ /** Returns the build plugins required by the JSX integration. */
47
+ get plugins() {
48
+ return [this.mdxLoaderPlugin].filter((plugin) => plugin !== void 0);
49
+ }
50
+ /** Returns the browser-only build plugins required by the JSX integration. */
51
+ get browserBuildPlugins() {
52
+ return [this.runtimeBundleService.getBuildPlugin()].filter(
53
+ (plugin) => plugin !== void 0
54
+ );
55
+ }
56
+ /**
57
+ * Exposes the bare-module specifier map used by the import map.
58
+ *
59
+ * Client bundles keep these imports external so the browser can load the
60
+ * shared runtime packages from the generated vendor assets.
61
+ */
62
+ getRuntimeSpecifierMap() {
63
+ return this.runtimeSpecifierMap;
64
+ }
65
+ /**
66
+ * Creates the renderer instance and attaches the discovered intrinsic custom
67
+ * element assets before the renderer handles any requests.
68
+ */
69
+ initializeRenderer() {
70
+ const renderer = super.initializeRenderer();
71
+ renderer.setIntrinsicCustomElementAssets(this.intrinsicCustomElementAssets);
72
+ renderer.setRadiantSsrEnabled(this.includeRadiant);
73
+ return renderer;
74
+ }
75
+ /**
76
+ * Creates a JSX integration plugin.
77
+ *
78
+ * When MDX is enabled, the plugin also claims the configured MDX extensions
79
+ * and prepares compiler options around the shared `@ecopages/jsx` runtime.
80
+ */
81
+ constructor(options) {
82
+ const { extensions: _ignoredExtensions, ...restOptions } = options ?? {};
83
+ const extensions = [...options?.extensions ?? [".tsx"]];
84
+ const mdxExtensions = options?.mdx?.extensions ?? [".mdx"];
85
+ const includeRadiant = options?.radiant ?? true;
86
+ if (options?.mdx?.enabled) {
87
+ for (const extension of mdxExtensions) {
88
+ if (!extensions.includes(extension)) {
89
+ extensions.push(extension);
90
+ }
91
+ }
92
+ }
93
+ super({
94
+ name: ECOPAGES_JSX_PLUGIN_NAME,
95
+ extensions,
96
+ jsxImportSource: "@ecopages/jsx",
97
+ ...restOptions
98
+ });
99
+ this.includeRadiant = includeRadiant;
100
+ this.runtimeBundleService = new JsxRuntimeBundleService({ radiant: includeRadiant });
101
+ this.mdxEnabled = options?.mdx?.enabled ?? false;
102
+ this.mdxExtensions = mdxExtensions;
103
+ EcopagesJsxRenderer.mdxExtensions = this.mdxExtensions;
104
+ if (this.mdxEnabled) {
105
+ const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = options?.mdx ?? {};
106
+ const resolvedCompilerOptions = {
107
+ format: "detect",
108
+ outputFormat: "program",
109
+ ...compilerOptions,
110
+ jsxImportSource: "@ecopages/jsx",
111
+ jsxRuntime: "automatic",
112
+ development: process.env.NODE_ENV === "development"
113
+ };
114
+ const mergedRemarkPlugins = mergePluginLists(compilerOptions?.remarkPlugins, remarkPlugins);
115
+ const mergedRehypePlugins = mergePluginLists(compilerOptions?.rehypePlugins, rehypePlugins);
116
+ const mergedRecmaPlugins = mergePluginLists(compilerOptions?.recmaPlugins, recmaPlugins);
117
+ if (mergedRemarkPlugins) {
118
+ resolvedCompilerOptions.remarkPlugins = mergedRemarkPlugins;
119
+ }
120
+ if (mergedRehypePlugins) {
121
+ resolvedCompilerOptions.rehypePlugins = mergedRehypePlugins;
122
+ }
123
+ if (mergedRecmaPlugins) {
124
+ resolvedCompilerOptions.recmaPlugins = mergedRecmaPlugins;
125
+ }
126
+ this.mdxCompilerOptions = resolvedCompilerOptions;
127
+ }
128
+ }
129
+ /** Ensures MDX build hooks are ready before Ecopages collects contributions. */
130
+ async prepareBuildContributions() {
131
+ if (!this.runtimeDepsInitialized) {
132
+ this.runtimeDepsInitialized = true;
133
+ this.runtimeBundleService.setRootDir(this.appConfig?.rootDir);
134
+ this.runtimeSpecifierMap = await this.runtimeBundleService.getSpecifierMap();
135
+ const vendorDeps = await this.runtimeBundleService.getDependencies();
136
+ this.integrationDependencies.unshift(...vendorDeps);
137
+ }
138
+ this.ensureMdxLoaderPlugin();
139
+ }
140
+ /**
141
+ * Registers MDX tooling, discovers intrinsic custom-element assets, and then
142
+ * completes the base integration setup.
143
+ */
144
+ async setup() {
145
+ this.ensureMdxLoaderPlugin();
146
+ if (typeof Bun !== "undefined" && this.mdxEnabled && this.mdxCompilerOptions) {
147
+ await this.setupMdxBunPlugin();
148
+ }
149
+ await this.buildIntrinsicCustomElementAssetRegistry();
150
+ await super.setup();
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
+ ensureMdxLoaderPlugin() {
160
+ if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
161
+ return;
162
+ }
163
+ this.mdxLoaderPlugin = createMdxLoaderPlugin(this.mdxCompilerOptions, this.mdxExtensions);
164
+ }
165
+ async setupMdxBunPlugin() {
166
+ if (typeof Bun === "undefined" || !this.mdxEnabled || !this.mdxCompilerOptions) {
167
+ return;
168
+ }
169
+ const compilerOptions = this.mdxCompilerOptions;
170
+ const escapedExts = this.mdxExtensions.map(escapeRegex);
171
+ const filter = new RegExp(`(${escapedExts.join("|")})$`);
172
+ Bun.plugin({
173
+ name: "ecopages-jsx-mdx",
174
+ setup(build) {
175
+ build.onLoad({ filter }, async (args) => {
176
+ const { compile } = await import("@mdx-js/mdx");
177
+ const source = await readFile(args.path, "utf-8");
178
+ const compiled = await compile(new VFile({ value: source, path: args.path }), compilerOptions);
179
+ return { contents: String(compiled.value), loader: "js" };
180
+ });
181
+ }
182
+ });
183
+ }
184
+ async buildIntrinsicCustomElementAssetRegistry() {
185
+ if (!this.appConfig || !this.assetProcessingService) {
186
+ return;
187
+ }
188
+ this.intrinsicCustomElementAssets.clear();
189
+ const scriptFiles = await this.collectScriptEntryFiles(this.appConfig.absolutePaths.srcDir);
190
+ for (const scriptFile of scriptFiles) {
191
+ const tagNames = await this.extractIntrinsicCustomElementTagNames(scriptFile);
192
+ if (tagNames.length === 0) {
193
+ continue;
194
+ }
195
+ const processedAsset = await this.resolveIntrinsicCustomElementAsset(scriptFile);
196
+ if (!processedAsset) {
197
+ continue;
198
+ }
199
+ for (const tagName of tagNames) {
200
+ this.intrinsicCustomElementAssets.set(tagName, [processedAsset]);
201
+ }
202
+ }
203
+ }
204
+ async collectScriptEntryFiles(directory) {
205
+ const directoryEntries = await readdir(directory, { withFileTypes: true });
206
+ const scriptFiles = [];
207
+ for (const directoryEntry of directoryEntries) {
208
+ const entryPath = path.join(directory, directoryEntry.name);
209
+ if (directoryEntry.isDirectory()) {
210
+ scriptFiles.push(...await this.collectScriptEntryFiles(entryPath));
211
+ continue;
212
+ }
213
+ if (/\.script\.(?:ts|tsx)$/.test(directoryEntry.name)) {
214
+ scriptFiles.push(entryPath);
215
+ }
216
+ }
217
+ return scriptFiles;
218
+ }
219
+ async resolveIntrinsicCustomElementAsset(scriptFile) {
220
+ if (!this.assetProcessingService) {
221
+ return void 0;
222
+ }
223
+ const [processedAsset] = await this.assetProcessingService.processDependencies(
224
+ [
225
+ AssetFactory.createFileScript({
226
+ filepath: scriptFile,
227
+ position: "head"
228
+ })
229
+ ],
230
+ `${this.name}:intrinsic-custom-elements:${scriptFile}`
231
+ );
232
+ return processedAsset;
233
+ }
234
+ async extractIntrinsicCustomElementTagNames(scriptFile) {
235
+ const source = await readFile(scriptFile, "utf8");
236
+ const tagNames = /* @__PURE__ */ new Set();
237
+ for (const match of source.matchAll(/@customElement\(\s*['"]([^'"]+)['"]/g)) {
238
+ const tagName = match[1];
239
+ if (tagName) {
240
+ tagNames.add(tagName);
241
+ }
242
+ }
243
+ return [...tagNames];
244
+ }
245
+ }
246
+ const ecopagesJsxPlugin = (options) => new EcopagesJsxPlugin(options);
247
+ export {
248
+ ECOPAGES_JSX_PLUGIN_NAME,
249
+ EcopagesJsxPlugin,
250
+ ecopagesJsxPlugin
251
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Runtime bundle service for the JSX integration.
3
+ *
4
+ * Owns creation of browser runtime vendor assets, the import map specifier
5
+ * mapping, and the build external plugin. Radiant sub-path specifiers are
6
+ * derived at runtime from `@ecopages/radiant/package.json` exports so the
7
+ * list stays in sync with whatever version is installed.
8
+ *
9
+ * @module
10
+ */
11
+ import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
12
+ import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
13
+ export interface JsxRuntimeBundleServiceConfig {
14
+ radiant: boolean;
15
+ rootDir?: string;
16
+ }
17
+ export declare class JsxRuntimeBundleService {
18
+ private readonly config;
19
+ private cachedSpecifierMap;
20
+ private cachedRadiantEntryModulePath;
21
+ constructor(config: JsxRuntimeBundleServiceConfig);
22
+ setRootDir(rootDir: string | undefined): void;
23
+ /**
24
+ * Returns the build plugin that aliases JSX and Radiant runtime specifiers to
25
+ * their emitted browser vendor URLs.
26
+ *
27
+ * @remarks
28
+ * The returned plugin both externalizes the mapped specifiers during bundle
29
+ * resolution and exposes alias metadata so Ecopages can rewrite any emitted JS
30
+ * imports that still reference bare runtime specifiers.
31
+ */
32
+ getBuildPlugin(): EcoBuildPlugin;
33
+ /**
34
+ * Builds the bare-specifier-to-vendor-URL map for the browser import map.
35
+ *
36
+ * JSX sub-paths are always included. When `radiant` is enabled, radiant
37
+ * sub-paths are derived from `@ecopages/radiant/package.json` exports and
38
+ * the result is cached for the lifetime of this service instance.
39
+ */
40
+ getSpecifierMap(): Promise<Record<string, string>>;
41
+ /**
42
+ * Builds the full list of vendor asset definitions: the import map inline
43
+ * script plus one `createBrowserRuntimeScriptAsset` per vendor bundle.
44
+ */
45
+ getDependencies(): Promise<AssetDefinition[]>;
46
+ private getOrCreateSpecifierMap;
47
+ private getRadiantBrowserRuntimeSpecifiers;
48
+ private getRadiantBrowserRuntimeModules;
49
+ private resolveRadiantExportModulePath;
50
+ private getOrCreateRadiantEntryModulePath;
51
+ }
@@ -0,0 +1,206 @@
1
+ import path from "node:path";
2
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
4
+ import {
5
+ buildBrowserRuntimeAssetUrl,
6
+ createBrowserRuntimeScriptAsset,
7
+ AssetFactory
8
+ } from "@ecopages/core/services/asset-processing-service";
9
+ const VENDOR_FILE_NAMES = {
10
+ jsx: "ecopages-jsx-esm.js",
11
+ radiant: "ecopages-radiant-esm.js"
12
+ };
13
+ function getNamedExportNamesFromModuleSource(source) {
14
+ const exportNames = /* @__PURE__ */ new Set();
15
+ for (const match of source.matchAll(/export\s*\{([^}]+)\}/g)) {
16
+ for (const specifier of match[1].split(",")) {
17
+ const trimmedSpecifier = specifier.trim();
18
+ if (!trimmedSpecifier) {
19
+ continue;
20
+ }
21
+ const aliasMatch = trimmedSpecifier.match(/(?:.+\s+as\s+)?([A-Z_a-z$][\w$]*)$/);
22
+ if (aliasMatch?.[1] && aliasMatch[1] !== "default") {
23
+ exportNames.add(aliasMatch[1]);
24
+ }
25
+ }
26
+ }
27
+ for (const match of source.matchAll(
28
+ /export\s+(?:async\s+)?(?:const|function|class|let|var)\s+([A-Z_a-z$][\w$]*)/g
29
+ )) {
30
+ if (match[1] !== "default") {
31
+ exportNames.add(match[1]);
32
+ }
33
+ }
34
+ return [...exportNames].sort();
35
+ }
36
+ function isBrowserRuntimeRadiantSpecifier(exportKey) {
37
+ if (exportKey === "." || exportKey.startsWith("./context/")) {
38
+ return true;
39
+ }
40
+ if (exportKey.startsWith("./decorators/") || exportKey.startsWith("./helpers/")) {
41
+ return true;
42
+ }
43
+ return exportKey === "./core/radiant-component" || exportKey === "./core/radiant-element";
44
+ }
45
+ function findPackageManifestPath(packageName) {
46
+ let currentDir = path.dirname(new URL(import.meta.url).pathname);
47
+ while (true) {
48
+ const candidatePath = path.join(currentDir, "node_modules", packageName, "package.json");
49
+ if (existsSync(candidatePath)) {
50
+ return candidatePath;
51
+ }
52
+ const parentDir = path.dirname(currentDir);
53
+ if (parentDir === currentDir) {
54
+ break;
55
+ }
56
+ currentDir = parentDir;
57
+ }
58
+ throw new Error(`Could not locate ${packageName}/package.json from ${import.meta.url}`);
59
+ }
60
+ class JsxRuntimeBundleService {
61
+ config;
62
+ cachedSpecifierMap;
63
+ cachedRadiantEntryModulePath;
64
+ constructor(config) {
65
+ this.config = config;
66
+ }
67
+ setRootDir(rootDir) {
68
+ this.config.rootDir = rootDir;
69
+ }
70
+ /**
71
+ * Returns the build plugin that aliases JSX and Radiant runtime specifiers to
72
+ * their emitted browser vendor URLs.
73
+ *
74
+ * @remarks
75
+ * The returned plugin both externalizes the mapped specifiers during bundle
76
+ * resolution and exposes alias metadata so Ecopages can rewrite any emitted JS
77
+ * imports that still reference bare runtime specifiers.
78
+ */
79
+ getBuildPlugin() {
80
+ return createRuntimeSpecifierAliasPlugin(this.getOrCreateSpecifierMap(), {
81
+ name: "ecopages-jsx-runtime-alias"
82
+ });
83
+ }
84
+ /**
85
+ * Builds the bare-specifier-to-vendor-URL map for the browser import map.
86
+ *
87
+ * JSX sub-paths are always included. When `radiant` is enabled, radiant
88
+ * sub-paths are derived from `@ecopages/radiant/package.json` exports and
89
+ * the result is cached for the lifetime of this service instance.
90
+ */
91
+ async getSpecifierMap() {
92
+ return this.getOrCreateSpecifierMap();
93
+ }
94
+ /**
95
+ * Builds the full list of vendor asset definitions: the import map inline
96
+ * script plus one `createBrowserRuntimeScriptAsset` per vendor bundle.
97
+ */
98
+ async getDependencies() {
99
+ const specifierMap = await this.getSpecifierMap();
100
+ const deps = [
101
+ AssetFactory.createInlineContentScript({
102
+ position: "head",
103
+ bundle: false,
104
+ content: JSON.stringify({ imports: specifierMap }, null, 2),
105
+ attributes: { type: "importmap" }
106
+ }),
107
+ createBrowserRuntimeScriptAsset({
108
+ importPath: "@ecopages/jsx",
109
+ name: "ecopages-jsx-esm",
110
+ fileName: VENDOR_FILE_NAMES.jsx
111
+ })
112
+ ];
113
+ if (this.config.radiant) {
114
+ const radiantEntryModulePath = await this.getOrCreateRadiantEntryModulePath();
115
+ deps.push(
116
+ createBrowserRuntimeScriptAsset({
117
+ importPath: radiantEntryModulePath,
118
+ name: "ecopages-radiant-esm",
119
+ fileName: VENDOR_FILE_NAMES.radiant
120
+ })
121
+ );
122
+ }
123
+ return deps;
124
+ }
125
+ getOrCreateSpecifierMap() {
126
+ if (this.cachedSpecifierMap) {
127
+ return this.cachedSpecifierMap;
128
+ }
129
+ const jsxVendorUrl = buildBrowserRuntimeAssetUrl(VENDOR_FILE_NAMES.jsx);
130
+ const specifierMap = {
131
+ "@ecopages/jsx": jsxVendorUrl,
132
+ "@ecopages/jsx/server": jsxVendorUrl,
133
+ "@ecopages/jsx/client": jsxVendorUrl,
134
+ "@ecopages/jsx/jsx-runtime": jsxVendorUrl,
135
+ "@ecopages/jsx/jsx-dev-runtime": jsxVendorUrl
136
+ };
137
+ if (this.config.radiant) {
138
+ const radiantVendorUrl = buildBrowserRuntimeAssetUrl(VENDOR_FILE_NAMES.radiant);
139
+ const radiantPkg = JSON.parse(
140
+ readFileSync(findPackageManifestPath("@ecopages/radiant"), "utf8")
141
+ );
142
+ for (const key of Object.keys(radiantPkg.exports ?? {})) {
143
+ if (!isBrowserRuntimeRadiantSpecifier(key)) {
144
+ continue;
145
+ }
146
+ const specifier = key === "." ? "@ecopages/radiant" : `@ecopages/radiant${key.slice(1)}`;
147
+ specifierMap[specifier] = radiantVendorUrl;
148
+ }
149
+ }
150
+ this.cachedSpecifierMap = specifierMap;
151
+ return specifierMap;
152
+ }
153
+ getRadiantBrowserRuntimeSpecifiers() {
154
+ return this.getRadiantBrowserRuntimeModules().map(({ exportKey }) => `@ecopages/radiant${exportKey.slice(1)}`);
155
+ }
156
+ getRadiantBrowserRuntimeModules() {
157
+ const manifestPath = findPackageManifestPath("@ecopages/radiant");
158
+ const packageDir = path.dirname(realpathSync(manifestPath));
159
+ const radiantPkg = JSON.parse(readFileSync(manifestPath, "utf8"));
160
+ return Object.entries(radiantPkg.exports ?? {}).filter(([key]) => isBrowserRuntimeRadiantSpecifier(key) && key !== ".").sort(([left], [right]) => left.localeCompare(right)).map(([exportKey, exportTarget]) => ({
161
+ exportKey,
162
+ modulePath: this.resolveRadiantExportModulePath(packageDir, exportKey, exportTarget)
163
+ })).filter((module) => existsSync(module.modulePath));
164
+ }
165
+ resolveRadiantExportModulePath(packageDir, exportKey, exportTarget) {
166
+ if (typeof exportTarget === "string") {
167
+ return path.resolve(packageDir, exportTarget);
168
+ }
169
+ if (exportTarget && typeof exportTarget === "object" && "import" in exportTarget) {
170
+ const importTarget = exportTarget.import;
171
+ if (typeof importTarget === "string") {
172
+ return path.resolve(packageDir, importTarget);
173
+ }
174
+ }
175
+ throw new Error(`Missing import target for @ecopages/radiant export ${exportKey}`);
176
+ }
177
+ async getOrCreateRadiantEntryModulePath() {
178
+ if (this.cachedRadiantEntryModulePath) {
179
+ return this.cachedRadiantEntryModulePath;
180
+ }
181
+ const rootDir = this.config.rootDir ?? process.cwd();
182
+ const artifactsDir = path.join(rootDir, "node_modules", ".cache", "ecopages-browser-runtime");
183
+ const filePath = path.join(artifactsDir, "ecopages-radiant-esm-entry.mjs");
184
+ const seenExports = /* @__PURE__ */ new Set();
185
+ const statements = [];
186
+ mkdirSync(artifactsDir, { recursive: true });
187
+ for (const module of this.getRadiantBrowserRuntimeModules()) {
188
+ const exportNames = getNamedExportNamesFromModuleSource(readFileSync(module.modulePath, "utf8")).filter((name) => !seenExports.has(name)).filter((name) => /^[$A-Z_a-z][$\w]*$/.test(name)).sort();
189
+ if (exportNames.length === 0) {
190
+ continue;
191
+ }
192
+ const relativeModulePath = path.relative(artifactsDir, module.modulePath).split(path.sep).join("/");
193
+ const entryImportPath = relativeModulePath.startsWith(".") ? relativeModulePath : `./${relativeModulePath}`;
194
+ statements.push(`export { ${exportNames.join(", ")} } from '${entryImportPath}';`);
195
+ for (const exportName of exportNames) {
196
+ seenExports.add(exportName);
197
+ }
198
+ }
199
+ writeFileSync(filePath, statements.join("\n"), "utf8");
200
+ this.cachedRadiantEntryModulePath = filePath;
201
+ return filePath;
202
+ }
203
+ }
204
+ export {
205
+ JsxRuntimeBundleService
206
+ };