@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 +21 -0
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/package.json +28 -0
- package/src/ecopages-jsx-renderer.d.ts +65 -0
- package/src/ecopages-jsx-renderer.js +298 -0
- package/src/ecopages-jsx.plugin.d.ts +116 -0
- package/src/ecopages-jsx.plugin.js +251 -0
- package/src/services/jsx-runtime-bundle.service.d.ts +51 -0
- package/src/services/jsx-runtime-bundle.service.js +206 -0
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
|
+
};
|