@ecopages/ecopages-jsx 0.2.0-rc.1 → 0.2.0-rc.11
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/README.md +25 -1
- package/package.json +9 -6
- package/src/ecopages-jsx-mdx.d.ts +6 -2
- package/src/ecopages-jsx-mdx.js +14 -37
- package/src/ecopages-jsx-radiant-ssr-policy.js +2 -1
- package/src/ecopages-jsx-render-session.d.ts +2 -8
- package/src/ecopages-jsx-render-session.js +2 -29
- package/src/ecopages-jsx-renderer.d.ts +2 -1
- package/src/ecopages-jsx-renderer.js +152 -123
- package/src/ecopages-jsx.plugin.d.ts +5 -8
- package/src/ecopages-jsx.plugin.js +13 -17
- package/src/page-owned-content-script-grouping.js +1 -1
- package/src/radiant-registered-script-ssr-invalidation.js +2 -0
package/README.md
CHANGED
|
@@ -53,6 +53,12 @@ Radiant support is enabled by default. When `radiant: true`, the plugin keeps th
|
|
|
53
53
|
|
|
54
54
|
That means server-rendered `RadiantElement` hosts hydrate in place only when both the SSR markers and the explicit client hydrator are present. Without the client hydrator, Radiant intentionally falls back to a fresh client render on first connect.
|
|
55
55
|
|
|
56
|
+
### Hydration Semantics: RadiantElement vs. RadiantController
|
|
57
|
+
|
|
58
|
+
- **`RadiantElement` (DOM Preservation)**: Custom elements extending `RadiantElement` support true in-place hydration. When `@ecopages/radiant/client/install-hydrator` executes before elements connect, existing light-DOM SSR nodes are preserved, retaining DOM node identity, form input state, active focus, and media playback.
|
|
59
|
+
- **`RadiantController` (Activation / Template Re-mount)**: Controllers attached via `[data-controller]` activate when `startControllers(document)` is called. Activation executes the controller's `render()` method and mounts its template fresh into the host container, replacing server-rendered child nodes. Reactive properties and signal bindings remain fully interactive, but individual child DOM nodes are reconstructed.
|
|
60
|
+
- **Guidance**: Use `RadiantElement` when in-place preservation of SSR nodes (e.g., focused inputs, media elements, or CSS transitions) is required. Use `RadiantController` for Stimulus-style progressive enhancement on arbitrary markup where child DOM reconstruction is acceptable.
|
|
61
|
+
|
|
56
62
|
```ts
|
|
57
63
|
ecopagesJsxPlugin({
|
|
58
64
|
radiant: true,
|
|
@@ -72,6 +78,23 @@ new MyElement().renderHostToString({ mode: 'hydrate' });
|
|
|
72
78
|
|
|
73
79
|
The legacy `hydrate: true` option remains compatible, but `mode` is the current SSR contract. Registered intrinsic tags that contain a dash are SSR candidates, and framework-specific host rendering should be adapted through the server custom-element render hook instead of hardcoding framework branches into JSX page code.
|
|
74
80
|
|
|
81
|
+
The integration loads Radiant host serialization through the public `@ecopages/radiant/server/radiant-element-ssr` export after installing the light-DOM shim. Applications do not need a separate Radiant SSR side-effect import.
|
|
82
|
+
|
|
83
|
+
### Registered Radiant scripts (`ssr: true`)
|
|
84
|
+
|
|
85
|
+
Declare custom-element registration once in `dependencies.scripts`. Ecopages JSX preloads entries marked `ssr: true` on the server before render so hosts can SSR without a redundant `import './my-element.script.ts'` in the component file.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
export const ThemeToggle = eco.component({
|
|
89
|
+
dependencies: {
|
|
90
|
+
scripts: [{ src: './theme-toggle.script.ts', ssr: true, lazy: { 'on:idle': true } }],
|
|
91
|
+
},
|
|
92
|
+
render: () => <theme-toggle />,
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Keep `import type` when you only need props from the script module. Do not add `ssr: true` to browser-only scripts that touch `window` or `document` without guards.
|
|
97
|
+
|
|
75
98
|
## MDX Support
|
|
76
99
|
|
|
77
100
|
Enable MDX to treat `.mdx` files as JSX routes compiled against the `@ecopages/jsx` runtime.
|
|
@@ -100,7 +123,8 @@ Ecopages JSX can own the outer page shell or just a nested foreign subtree. When
|
|
|
100
123
|
|
|
101
124
|
Important:
|
|
102
125
|
|
|
103
|
-
- Cross-integration
|
|
126
|
+
- Cross-integration shell stacks should use `EcoEmbed` from `@ecopages/ecopages-jsx/eco-embed` (or the matching integration adapter). Plain opaque objects fail fast at core foreign-subtree queue boundaries; they are not coerced with `String(object)`. Pass already-serialized HTML when `EcoEmbed` is not needed.
|
|
104
127
|
- Components that may render foreign children must declare those children in `config.dependencies.components`.
|
|
105
128
|
- Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not treat rendered HTML alone as the source of truth.
|
|
129
|
+
- Route-level dependency resolvers can supply Components at request time; Ecopages JSX includes those resolved roots when deciding whether a Foreign Subtree needs handoff.
|
|
106
130
|
- Ecopages JSX keeps raw-markup preservation and asset collection inside the JSX renderer.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/ecopages-jsx",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.11",
|
|
4
4
|
"description": "JSX integration plugin for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
"main": "./src/ecopages-jsx.plugin.js",
|
|
12
12
|
"types": "./src/ecopages-jsx.plugin.d.ts",
|
|
13
13
|
"type": "module",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
14
17
|
"exports": {
|
|
15
18
|
".": {
|
|
16
19
|
"default": "./src/ecopages-jsx.plugin.js",
|
|
@@ -31,13 +34,13 @@
|
|
|
31
34
|
"directory": "packages/integrations/ecopages-jsx"
|
|
32
35
|
},
|
|
33
36
|
"dependencies": {
|
|
34
|
-
"@ecopages/mdx": "0.2.0-rc.
|
|
37
|
+
"@ecopages/mdx": "0.2.0-rc.11",
|
|
35
38
|
"@mdx-js/mdx": "^3.1.1",
|
|
36
39
|
"vfile": "^6.0.3"
|
|
37
40
|
},
|
|
38
41
|
"peerDependencies": {
|
|
39
|
-
"@ecopages/core": "0.2.0-rc.
|
|
40
|
-
"@ecopages/jsx": "^0.3.0-rc.
|
|
41
|
-
"@ecopages/radiant": "^0.3.0-rc.
|
|
42
|
+
"@ecopages/core": "0.2.0-rc.11",
|
|
43
|
+
"@ecopages/jsx": "^0.3.0-rc.9",
|
|
44
|
+
"@ecopages/radiant": "^0.3.0-rc.9"
|
|
42
45
|
}
|
|
43
|
-
}
|
|
46
|
+
}
|
|
@@ -13,7 +13,11 @@ export type EcopagesJsxMdxPageModule = EcoPageFile<{
|
|
|
13
13
|
}>;
|
|
14
14
|
export { appendMdxExtensions, createMdxExtensionFilter };
|
|
15
15
|
export declare const resolveMdxCompilerOptions: (mdxOptions: EcopagesJsxMdxOptions) => ResolvedMdxCompileOptions;
|
|
16
|
-
export
|
|
17
|
-
|
|
16
|
+
export interface CreateEcopagesJsxMdxLoaderPluginOptions {
|
|
17
|
+
compilerOptions: ResolvedMdxCompileOptions;
|
|
18
|
+
extensions: string[];
|
|
19
|
+
projectRoot: string;
|
|
20
|
+
}
|
|
21
|
+
export declare const createMdxLoaderPlugin: (options: CreateEcopagesJsxMdxLoaderPluginOptions) => EcoBuildPlugin;
|
|
18
22
|
export declare const isMdxFile: (filePath: string, extensions: string[]) => boolean;
|
|
19
23
|
export declare const normalizeMdxPageModule: (file: string, module: EcopagesJsxMdxPageModule) => EcopagesJsxMdxPageModule;
|
package/src/ecopages-jsx-mdx.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
1
|
import { ensurePageConfigLayouts } from '@ecopages/core/eco/page-layout-normalization';
|
|
3
|
-
import { rapidhash } from '@ecopages/core/hash';
|
|
4
2
|
import { appendMdxExtensions, createMdxExtensionFilter, createMdxLoaderPlugin as createMdxLoaderPluginCore, resolveMdxCompilerOptions as resolveMdxCompilerOptionsCore, } from '@ecopages/mdx/core';
|
|
5
|
-
import { VFile } from 'vfile';
|
|
6
3
|
import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
|
|
7
4
|
export { appendMdxExtensions, createMdxExtensionFilter };
|
|
8
5
|
export const resolveMdxCompilerOptions = (mdxOptions) => resolveMdxCompilerOptionsCore(mdxOptions, {
|
|
@@ -12,30 +9,15 @@ export const resolveMdxCompilerOptions = (mdxOptions) => resolveMdxCompilerOptio
|
|
|
12
9
|
outputFormat: 'program',
|
|
13
10
|
},
|
|
14
11
|
});
|
|
15
|
-
export const createMdxLoaderPlugin = (
|
|
12
|
+
export const createMdxLoaderPlugin = (options) => createMdxLoaderPluginCore({
|
|
16
13
|
name: 'ecopages-jsx-mdx-loader',
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
integrationName: ECOPAGES_JSX_PLUGIN_NAME,
|
|
15
|
+
compilerOptions: options.compilerOptions,
|
|
16
|
+
extensions: options.extensions,
|
|
19
17
|
includeSourceMap: false,
|
|
20
18
|
loader: 'js',
|
|
19
|
+
projectRoot: options.projectRoot,
|
|
21
20
|
});
|
|
22
|
-
export const registerBunMdxPlugin = async (compilerOptions, extensions) => {
|
|
23
|
-
if (typeof Bun === 'undefined') {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
const filter = createMdxExtensionFilter(extensions);
|
|
27
|
-
Bun.plugin({
|
|
28
|
-
name: 'ecopages-jsx-mdx',
|
|
29
|
-
setup(build) {
|
|
30
|
-
build.onLoad({ filter }, async (args) => {
|
|
31
|
-
const { compile } = await import('@mdx-js/mdx');
|
|
32
|
-
const source = await readFile(args.path, 'utf-8');
|
|
33
|
-
const compiled = await compile(new VFile({ value: source, path: args.path }), compilerOptions);
|
|
34
|
-
return { contents: String(compiled.value), loader: 'js' };
|
|
35
|
-
});
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
21
|
export const isMdxFile = (filePath, extensions) => {
|
|
40
22
|
return extensions.some((ext) => filePath.endsWith(ext));
|
|
41
23
|
};
|
|
@@ -44,27 +26,22 @@ export const normalizeMdxPageModule = (file, module) => {
|
|
|
44
26
|
throw new TypeError('MDX file must export a callable default component.');
|
|
45
27
|
}
|
|
46
28
|
const Page = module.default;
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
integration: ECOPAGES_JSX_PLUGIN_NAME,
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
if (module.layout && !(normalizedConfig.layouts && normalizedConfig.layouts.length > 0)) {
|
|
57
|
-
normalizedConfig.layout = module.layout;
|
|
29
|
+
const attributedConfig = module.config ?? Page.config;
|
|
30
|
+
if (!attributedConfig?.identity) {
|
|
31
|
+
throw new Error(`[ecopages] MDX page "${file}" is missing component identity; the MDX loader must attribute it.`);
|
|
32
|
+
}
|
|
33
|
+
if (module.layout && !(attributedConfig.layouts && attributedConfig.layouts.length > 0)) {
|
|
34
|
+
attributedConfig.layout = module.layout;
|
|
58
35
|
}
|
|
59
|
-
ensurePageConfigLayouts(
|
|
36
|
+
ensurePageConfigLayouts(attributedConfig);
|
|
60
37
|
const wrappedPage = async (props) => await Page(props);
|
|
61
|
-
wrappedPage.config =
|
|
38
|
+
wrappedPage.config = attributedConfig;
|
|
62
39
|
if (module.getMetadata ?? Page.metadata) {
|
|
63
40
|
wrappedPage.metadata = module.getMetadata ?? Page.metadata;
|
|
64
41
|
}
|
|
65
42
|
return {
|
|
66
43
|
...module,
|
|
67
44
|
default: wrappedPage,
|
|
68
|
-
config:
|
|
45
|
+
config: attributedConfig,
|
|
69
46
|
};
|
|
70
47
|
};
|
|
@@ -59,7 +59,8 @@ export class EcopagesJsxRadiantSsrPolicy {
|
|
|
59
59
|
async ensureRuntimeInstalled() {
|
|
60
60
|
if (!this.runtimeModulesPromise) {
|
|
61
61
|
const radiantLightDomShimEntry = import.meta.resolve('@ecopages/radiant/server/light-dom-shim');
|
|
62
|
-
const radiantElementSsrRuntimeModuleUrl =
|
|
62
|
+
const radiantElementSsrRuntimeModuleUrl = import.meta
|
|
63
|
+
.resolve('@ecopages/radiant/server/radiant-element-ssr');
|
|
63
64
|
this.runtimeModulesPromise = (async () => {
|
|
64
65
|
const lightDomShimModule = await import(radiantLightDomShimEntry);
|
|
65
66
|
ensureRadiantLightDomGlobals(lightDomShimModule.ensureLightDomShim);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { EcoComponent } from '@ecopages/core';
|
|
2
|
-
import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
|
|
3
2
|
export declare const ECOPAGES_JSX_SSR_RENDER_STATE_KEY: unique symbol;
|
|
4
3
|
/**
|
|
5
|
-
* Tracks JSX SSR
|
|
4
|
+
* Tracks JSX SSR HMR-ownership state for one active render flow.
|
|
6
5
|
*
|
|
7
6
|
* @remarks
|
|
8
7
|
* The renderer still mirrors this state into `@ecopages/jsx/server` so nested
|
|
@@ -12,24 +11,19 @@ export declare const ECOPAGES_JSX_SSR_RENDER_STATE_KEY: unique symbol;
|
|
|
12
11
|
* JSX server helpers.
|
|
13
12
|
*/
|
|
14
13
|
export declare class EcopagesJsxRenderSession {
|
|
15
|
-
private readonly dedupeProcessedAssets;
|
|
16
|
-
constructor(dedupeProcessedAssets: (assets: ProcessedAsset[]) => ProcessedAsset[]);
|
|
17
14
|
/**
|
|
18
15
|
* Runs one render inside the active session scope.
|
|
19
16
|
*
|
|
20
17
|
* @remarks
|
|
21
18
|
* When a render is already active, the current session state is reused and
|
|
22
19
|
* mirrored back into the JSX SSR scope so nested `renderToString()` calls see
|
|
23
|
-
* the same
|
|
20
|
+
* the same HMR-ownership set. When no session exists yet, a new state is
|
|
24
21
|
* created and published through both the internal async-local store and the
|
|
25
22
|
* JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
|
|
26
23
|
* published once when that scope completes.
|
|
27
24
|
*/
|
|
28
25
|
withActiveScope<T>(render: () => T | Promise<T>): Promise<T>;
|
|
29
|
-
beginCollectedAssetFrame(): ProcessedAsset[];
|
|
30
|
-
endCollectedAssetFrame(frame: ProcessedAsset[]): ProcessedAsset[];
|
|
31
26
|
mergeHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
|
|
32
|
-
recordCollectedAssets(collectedAssets: ProcessedAsset[]): ProcessedAsset[];
|
|
33
27
|
/**
|
|
34
28
|
* Resolves the current render-session state from the local async scope first.
|
|
35
29
|
*
|
|
@@ -26,7 +26,7 @@ async function runWithCommittedHmrOwnership(state, render) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Tracks JSX SSR
|
|
29
|
+
* Tracks JSX SSR HMR-ownership state for one active render flow.
|
|
30
30
|
*
|
|
31
31
|
* @remarks
|
|
32
32
|
* The renderer still mirrors this state into `@ecopages/jsx/server` so nested
|
|
@@ -36,17 +36,13 @@ async function runWithCommittedHmrOwnership(state, render) {
|
|
|
36
36
|
* JSX server helpers.
|
|
37
37
|
*/
|
|
38
38
|
export class EcopagesJsxRenderSession {
|
|
39
|
-
dedupeProcessedAssets;
|
|
40
|
-
constructor(dedupeProcessedAssets) {
|
|
41
|
-
this.dedupeProcessedAssets = dedupeProcessedAssets;
|
|
42
|
-
}
|
|
43
39
|
/**
|
|
44
40
|
* Runs one render inside the active session scope.
|
|
45
41
|
*
|
|
46
42
|
* @remarks
|
|
47
43
|
* When a render is already active, the current session state is reused and
|
|
48
44
|
* mirrored back into the JSX SSR scope so nested `renderToString()` calls see
|
|
49
|
-
* the same
|
|
45
|
+
* the same HMR-ownership set. When no session exists yet, a new state is
|
|
50
46
|
* created and published through both the internal async-local store and the
|
|
51
47
|
* JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
|
|
52
48
|
* published once when that scope completes.
|
|
@@ -62,36 +58,13 @@ export class EcopagesJsxRenderSession {
|
|
|
62
58
|
return await runWithCommittedHmrOwnership(jsxScopeState, () => renderStateStorage.run(jsxScopeState, () => render()));
|
|
63
59
|
}
|
|
64
60
|
const state = {
|
|
65
|
-
collectedAssetFrames: [],
|
|
66
61
|
pendingHmrFileOwners: new Set(),
|
|
67
62
|
};
|
|
68
63
|
return await runWithCommittedHmrOwnership(state, () => renderStateStorage.run(state, () => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render)));
|
|
69
64
|
}
|
|
70
|
-
beginCollectedAssetFrame() {
|
|
71
|
-
const state = this.getState();
|
|
72
|
-
const frame = [];
|
|
73
|
-
state.collectedAssetFrames.push(frame);
|
|
74
|
-
return frame;
|
|
75
|
-
}
|
|
76
|
-
endCollectedAssetFrame(frame) {
|
|
77
|
-
const activeFrame = this.getState().collectedAssetFrames.pop();
|
|
78
|
-
if (!activeFrame || activeFrame !== frame) {
|
|
79
|
-
return this.dedupeProcessedAssets(frame);
|
|
80
|
-
}
|
|
81
|
-
return this.dedupeProcessedAssets(activeFrame);
|
|
82
|
-
}
|
|
83
65
|
mergeHmrOwnership(components) {
|
|
84
66
|
mergeEjsxHmrOwnership(ensurePendingHmrFileOwners(this.getState()), components);
|
|
85
67
|
}
|
|
86
|
-
recordCollectedAssets(collectedAssets) {
|
|
87
|
-
const dedupedAssets = this.dedupeProcessedAssets(collectedAssets);
|
|
88
|
-
const state = this.getState();
|
|
89
|
-
const activeFrame = state.collectedAssetFrames[state.collectedAssetFrames.length - 1];
|
|
90
|
-
if (activeFrame) {
|
|
91
|
-
activeFrame.push(...dedupedAssets);
|
|
92
|
-
}
|
|
93
|
-
return dedupedAssets;
|
|
94
|
-
}
|
|
95
68
|
/**
|
|
96
69
|
* Resolves the current render-session state from the local async scope first.
|
|
97
70
|
*
|
|
@@ -16,6 +16,7 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
16
16
|
private readonly mdxExtensions;
|
|
17
17
|
private readonly renderSession;
|
|
18
18
|
private readonly radiantSsrPolicy;
|
|
19
|
+
private readonly ssrScriptPreloader;
|
|
19
20
|
/**
|
|
20
21
|
* Serializes foreign-child props for string-first boundaries.
|
|
21
22
|
*
|
|
@@ -33,7 +34,7 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
33
34
|
private isJsxSerializableForeignChild;
|
|
34
35
|
/**
|
|
35
36
|
* Re-renders queued JSX children inside the owning renderer so nested custom
|
|
36
|
-
* elements and queued foreign subtrees
|
|
37
|
+
* elements and queued foreign subtrees resolve here rather than as opaque markup.
|
|
37
38
|
*/
|
|
38
39
|
private renderQueuedForeignSubtreeChildren;
|
|
39
40
|
protected createForeignChildRuntime(options: {
|
|
@@ -7,6 +7,8 @@ import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
|
|
|
7
7
|
import { isMdxFile, normalizeMdxPageModule, } from './ecopages-jsx-mdx.js';
|
|
8
8
|
import { EcopagesJsxRenderSession } from './ecopages-jsx-render-session.js';
|
|
9
9
|
import { EcopagesJsxRadiantSsrPolicy } from './ecopages-jsx-radiant-ssr-policy.js';
|
|
10
|
+
import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, CustomElementScriptPreloader, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
|
|
11
|
+
import { createSharedServerModuleImporter } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-server-module-importer';
|
|
10
12
|
/**
|
|
11
13
|
* Local Ecopages renderer for JSX templates in the docs app.
|
|
12
14
|
*
|
|
@@ -18,6 +20,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
18
20
|
mdxExtensions;
|
|
19
21
|
renderSession;
|
|
20
22
|
radiantSsrPolicy;
|
|
23
|
+
ssrScriptPreloader;
|
|
21
24
|
/**
|
|
22
25
|
* Serializes foreign-child props for string-first boundaries.
|
|
23
26
|
*
|
|
@@ -72,25 +75,22 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
72
75
|
}
|
|
73
76
|
/**
|
|
74
77
|
* Re-renders queued JSX children inside the owning renderer so nested custom
|
|
75
|
-
* elements and queued foreign subtrees
|
|
78
|
+
* elements and queued foreign subtrees resolve here rather than as opaque markup.
|
|
76
79
|
*/
|
|
77
80
|
async renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken) {
|
|
78
81
|
if (children === undefined) {
|
|
79
82
|
return { assets: [] };
|
|
80
83
|
}
|
|
81
|
-
let assets = [];
|
|
82
84
|
let html;
|
|
83
85
|
if (typeof children === 'string') {
|
|
84
86
|
html = children;
|
|
85
87
|
}
|
|
86
88
|
else {
|
|
87
|
-
|
|
88
|
-
html = renderedChildren.html;
|
|
89
|
-
assets = renderedChildren.assets;
|
|
89
|
+
html = await this.renderJsx(children);
|
|
90
90
|
}
|
|
91
91
|
html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken);
|
|
92
92
|
return {
|
|
93
|
-
assets,
|
|
93
|
+
assets: [],
|
|
94
94
|
html,
|
|
95
95
|
};
|
|
96
96
|
}
|
|
@@ -121,9 +121,27 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
121
121
|
runtimeOrigin,
|
|
122
122
|
});
|
|
123
123
|
this.mdxExtensions = jsxConfig?.mdxExtensions ?? ['.mdx'];
|
|
124
|
-
this.renderSession = new EcopagesJsxRenderSession(
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
this.renderSession = new EcopagesJsxRenderSession();
|
|
125
|
+
const radiantSsrEnabled = jsxConfig?.radiantSsrEnabled ?? false;
|
|
126
|
+
this.radiantSsrPolicy = jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(radiantSsrEnabled);
|
|
127
|
+
const preferSourceImports = typeof Bun !== 'undefined';
|
|
128
|
+
this.ssrScriptPreloader = new CustomElementScriptPreloader({
|
|
129
|
+
cacheScope: CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.ecopagesJsx,
|
|
130
|
+
collectForIntegration: ECOPAGES_JSX_PLUGIN_NAME,
|
|
131
|
+
enabled: radiantSsrEnabled,
|
|
132
|
+
logLabel: 'ecopages-jsx',
|
|
133
|
+
preferSourceImports,
|
|
134
|
+
resolveDependencyPath: (componentDir, sourcePath) => this.resolveDependencyPath(componentDir, sourcePath),
|
|
135
|
+
/**
|
|
136
|
+
* @remarks
|
|
137
|
+
* Bun imports the source file through `preferSourceImports`. Node must
|
|
138
|
+
* not: dev asset output is still the `.tsx` source, and a browser bundle
|
|
139
|
+
* is a second Radiant graph. The page server-module loader transpiles
|
|
140
|
+
* the script and externalizes packages, so `customElements.define` hits
|
|
141
|
+
* the same module instances the page already loaded.
|
|
142
|
+
*/
|
|
143
|
+
importServerModule: preferSourceImports ? undefined : createSharedServerModuleImporter(this.appConfig),
|
|
144
|
+
});
|
|
127
145
|
}
|
|
128
146
|
/** Returns whether the requested page file should be treated as MDX. */
|
|
129
147
|
isMdxFile(filePath) {
|
|
@@ -136,129 +154,140 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
136
154
|
});
|
|
137
155
|
}
|
|
138
156
|
async render(options) {
|
|
139
|
-
return await this.withPreparedRadiantRuntime(async () =>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
157
|
+
return await this.withPreparedRadiantRuntime(async () => {
|
|
158
|
+
await this.ssrScriptPreloader.preloadSsrScripts([
|
|
159
|
+
options.Page,
|
|
160
|
+
options.Layout,
|
|
161
|
+
options.HtmlTemplate,
|
|
162
|
+
...(options.resolvedPageDependencyComponents ?? []),
|
|
163
|
+
]);
|
|
164
|
+
return await this.renderSession.withActiveScope(async () => {
|
|
165
|
+
try {
|
|
166
|
+
const result = await this.renderPageWithDocumentShell({
|
|
167
|
+
page: {
|
|
168
|
+
component: options.Page,
|
|
169
|
+
props: {
|
|
170
|
+
...options.pageProps,
|
|
171
|
+
locals: options.pageLocals,
|
|
172
|
+
},
|
|
147
173
|
},
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
/**
|
|
175
|
+
* @remarks Content components supplied by the Page dependency
|
|
176
|
+
* resolver (for example the active MDX entry) carry foreign
|
|
177
|
+
* ownership the static Page config cannot declare.
|
|
178
|
+
*/
|
|
179
|
+
foreignChildRoots: options.resolvedPageDependencyComponents,
|
|
180
|
+
layouts: resolveDocumentShellLayouts({
|
|
181
|
+
layout: options.Layout,
|
|
182
|
+
layoutEntries: options.layoutEntries,
|
|
183
|
+
params: options.params,
|
|
184
|
+
query: options.query,
|
|
185
|
+
locals: options.locals,
|
|
186
|
+
}),
|
|
187
|
+
htmlTemplate: options.HtmlTemplate,
|
|
188
|
+
metadata: options.metadata,
|
|
189
|
+
pageProps: options.pageProps ?? {},
|
|
190
|
+
});
|
|
191
|
+
this.recordHmrOwnership([
|
|
192
|
+
options.Page,
|
|
193
|
+
options.Layout,
|
|
194
|
+
options.HtmlTemplate,
|
|
195
|
+
...(options.resolvedPageDependencyComponents ?? []),
|
|
196
|
+
]);
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
throw this.createRenderError('Error rendering page', error);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
172
204
|
}
|
|
173
205
|
async renderComponent(input) {
|
|
174
|
-
return await this.withPreparedRadiantRuntime(async () =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
html: rendered.html,
|
|
194
|
-
runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(input, getForeignSubtreeResolutionContextKey(this.name)),
|
|
195
|
-
queueLabel: 'Ecopages JSX',
|
|
196
|
-
getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
|
|
197
|
-
appConfig: this.appConfig,
|
|
198
|
-
runtimeOrigin: this.runtimeOrigin,
|
|
206
|
+
return await this.withPreparedRadiantRuntime(async () => {
|
|
207
|
+
await this.ssrScriptPreloader.preloadSsrScripts([input.component]);
|
|
208
|
+
return await this.renderSession.withActiveScope(async () => {
|
|
209
|
+
try {
|
|
210
|
+
if (typeof input.component !== 'function') {
|
|
211
|
+
throw new TypeError('JSX renderer expected a callable component.');
|
|
212
|
+
}
|
|
213
|
+
const component = input.component;
|
|
214
|
+
const componentProps = input.children === undefined
|
|
215
|
+
? input.props
|
|
216
|
+
: {
|
|
217
|
+
...input.props,
|
|
218
|
+
children: typeof input.children === 'string'
|
|
219
|
+
? createMarkupNodeLike(input.children)
|
|
220
|
+
: input.children,
|
|
221
|
+
};
|
|
222
|
+
const content = await this.withCustomElementRenderHook(() => component(componentProps));
|
|
223
|
+
const html = await this.renderJsx(content);
|
|
224
|
+
const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
|
|
199
225
|
currentIntegrationName: this.name,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
html,
|
|
227
|
+
runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(input, getForeignSubtreeResolutionContextKey(this.name)),
|
|
228
|
+
queueLabel: 'Ecopages JSX',
|
|
229
|
+
getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
|
|
230
|
+
appConfig: this.appConfig,
|
|
231
|
+
runtimeOrigin: this.runtimeOrigin,
|
|
232
|
+
currentIntegrationName: this.name,
|
|
233
|
+
currentRenderer: this,
|
|
234
|
+
integrationName,
|
|
235
|
+
cache: rendererCache,
|
|
236
|
+
}),
|
|
237
|
+
applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
|
|
238
|
+
dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
|
|
239
|
+
renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken),
|
|
240
|
+
});
|
|
241
|
+
const componentAssets = input.component.config?.dependencies &&
|
|
242
|
+
typeof this.assetProcessingService?.processDependencies === 'function'
|
|
243
|
+
? await this.processComponentDependencies([input.component])
|
|
244
|
+
: [];
|
|
245
|
+
const assets = this.htmlTransformer.dedupeProcessedAssets([
|
|
246
|
+
...queuedForeignSubtreeResolution.assets,
|
|
247
|
+
...componentAssets,
|
|
248
|
+
]);
|
|
249
|
+
this.recordHmrOwnership([input.component]);
|
|
250
|
+
return this.finalizeIslandComponentRender(input, {
|
|
251
|
+
html: queuedForeignSubtreeResolution.html,
|
|
252
|
+
canAttachAttributes: true,
|
|
253
|
+
rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
|
|
254
|
+
integrationName: this.name,
|
|
255
|
+
assets,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
throw this.createRenderError('Error rendering component', error);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
231
263
|
}
|
|
232
264
|
async renderToResponse(view, props, ctx) {
|
|
233
|
-
return await this.withPreparedRadiantRuntime(async () =>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
265
|
+
return await this.withPreparedRadiantRuntime(async () => {
|
|
266
|
+
const viewComponent = view;
|
|
267
|
+
const layouts = viewComponent.config?.layouts;
|
|
268
|
+
await this.ssrScriptPreloader.preloadSsrScripts([view, ...(layouts ?? [])]);
|
|
269
|
+
return await this.renderSession.withActiveScope(async () => {
|
|
270
|
+
try {
|
|
271
|
+
if (typeof view !== 'function') {
|
|
272
|
+
throw new TypeError('JSX renderer expected a callable view component.');
|
|
273
|
+
}
|
|
274
|
+
const response = await this.renderViewWithDocumentShell({
|
|
275
|
+
view: viewComponent,
|
|
276
|
+
props: props,
|
|
277
|
+
ctx,
|
|
278
|
+
layout: layouts?.[layouts.length - 1],
|
|
279
|
+
});
|
|
280
|
+
this.recordHmrOwnership([view]);
|
|
281
|
+
return response;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
throw this.createRenderError('Error rendering view', error);
|
|
237
285
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const response = await this.renderViewWithDocumentShell({
|
|
241
|
-
view: viewComponent,
|
|
242
|
-
props: props,
|
|
243
|
-
ctx,
|
|
244
|
-
layout: layouts?.[layouts.length - 1],
|
|
245
|
-
});
|
|
246
|
-
this.recordHmrOwnership([view]);
|
|
247
|
-
return response;
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
throw this.createRenderError('Error rendering view', error);
|
|
251
|
-
}
|
|
252
|
-
}));
|
|
286
|
+
});
|
|
287
|
+
});
|
|
253
288
|
}
|
|
254
289
|
async renderJsx(value) {
|
|
255
|
-
|
|
256
|
-
const html = await this.withCustomElementRenderHook(() => renderToString(value));
|
|
257
|
-
const dedupedAssets = this.renderSession.recordCollectedAssets(collectedAssets);
|
|
258
|
-
return {
|
|
259
|
-
assets: dedupedAssets,
|
|
260
|
-
html,
|
|
261
|
-
};
|
|
290
|
+
return await this.withCustomElementRenderHook(() => renderToString(value));
|
|
262
291
|
}
|
|
263
292
|
async withCustomElementRenderHook(render) {
|
|
264
293
|
return await this.radiantSsrPolicy.withRuntime(() => withServerCustomElementRenderHook(this.createIntrinsicCustomElementRenderHook(), render));
|
|
@@ -39,17 +39,14 @@ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable>
|
|
|
39
39
|
*/
|
|
40
40
|
getHmrStrategy(): HmrStrategy | undefined;
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* Ensures the MDX loader exists, then completes the base integration setup.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* Bun registers `integration.plugins` after `setup()` returns. Calling
|
|
46
|
+
* `Bun.plugin` here would install the same MDX loader twice.
|
|
43
47
|
*/
|
|
44
48
|
setup(): Promise<void>;
|
|
45
49
|
private ensureMdxLoaderPlugin;
|
|
46
|
-
/**
|
|
47
|
-
* Registers Bun's MDX loader at runtime setup time.
|
|
48
|
-
*
|
|
49
|
-
* Build-time contribution collection may run where Bun is absent, so
|
|
50
|
-
* this hook stays isolated from manifest preparation.
|
|
51
|
-
*/
|
|
52
|
-
private registerMdxBunPlugin;
|
|
53
50
|
}
|
|
54
51
|
/**
|
|
55
52
|
* Creates the JSX integration plugin with resolved defaults.
|
|
@@ -2,7 +2,7 @@ import { IntegrationPlugin, } from '@ecopages/core/plugins/integration-plugin';
|
|
|
2
2
|
import { AssetFactory } from '@ecopages/core/services/asset-processing-service';
|
|
3
3
|
import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
|
|
4
4
|
import { RADIANT_INSTALL_HYDRATOR_FILEPATH } from './resolve-radiant-install-hydrator.js';
|
|
5
|
-
import { appendMdxExtensions, createMdxLoaderPlugin,
|
|
5
|
+
import { appendMdxExtensions, createMdxLoaderPlugin, resolveMdxCompilerOptions, } from './ecopages-jsx-mdx.js';
|
|
6
6
|
import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
|
|
7
7
|
import { EcopagesJsxHmrStrategy } from './ecopages-jsx-hmr-strategy.js';
|
|
8
8
|
import { EcopagesJsxDevTransformContributor } from './dev-transform/ecopages-jsx-dev-transform-contributor.js';
|
|
@@ -137,32 +137,28 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
140
|
+
* Ensures the MDX loader exists, then completes the base integration setup.
|
|
141
|
+
*
|
|
142
|
+
* @remarks
|
|
143
|
+
* Bun registers `integration.plugins` after `setup()` returns. Calling
|
|
144
|
+
* `Bun.plugin` here would install the same MDX loader twice.
|
|
141
145
|
*/
|
|
142
146
|
async setup() {
|
|
143
147
|
this.ensureMdxLoaderPlugin();
|
|
144
|
-
if (typeof Bun !== 'undefined' && this.mdxEnabled && this.mdxCompilerOptions) {
|
|
145
|
-
await this.registerMdxBunPlugin();
|
|
146
|
-
}
|
|
147
148
|
await super.setup();
|
|
148
149
|
}
|
|
149
150
|
ensureMdxLoaderPlugin() {
|
|
150
151
|
if (!this.mdxEnabled || !this.mdxCompilerOptions || this.mdxLoaderPlugin) {
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Registers Bun's MDX loader at runtime setup time.
|
|
157
|
-
*
|
|
158
|
-
* Build-time contribution collection may run where Bun is absent, so
|
|
159
|
-
* this hook stays isolated from manifest preparation.
|
|
160
|
-
*/
|
|
161
|
-
async registerMdxBunPlugin() {
|
|
162
|
-
if (typeof Bun === 'undefined' || !this.mdxCompilerOptions) {
|
|
163
|
-
return;
|
|
154
|
+
if (!this.appConfig?.rootDir) {
|
|
155
|
+
throw new Error('[EcopagesJsxPlugin] Cannot create MDX loader: appConfig.rootDir is required.');
|
|
164
156
|
}
|
|
165
|
-
|
|
157
|
+
this.mdxLoaderPlugin = createMdxLoaderPlugin({
|
|
158
|
+
compilerOptions: this.mdxCompilerOptions,
|
|
159
|
+
extensions: this.mdxExtensions,
|
|
160
|
+
projectRoot: this.appConfig.rootDir,
|
|
161
|
+
});
|
|
166
162
|
}
|
|
167
163
|
}
|
|
168
164
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rapidhash } from '@ecopages/core/
|
|
1
|
+
import { rapidhash } from '@ecopages/core/hash';
|
|
2
2
|
import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
|
|
3
3
|
/** Shared grouped-build id prefix for page-owned content scripts. */
|
|
4
4
|
const PAGE_OWNED_CONTENT_SCRIPT_BUNDLE_PREFIX = 'ecopages';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, invalidateCustomElementScriptPreload, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
|
|
2
3
|
const CUSTOM_ELEMENT_TAG_PATTERN = /@customElement\s*\(\s*['"`]([^'"`]+)['"`]/u;
|
|
3
4
|
/**
|
|
4
5
|
* Reads the custom-element tag name from a registered script module source file.
|
|
@@ -38,6 +39,7 @@ export function clearRadiantCustomElementDefinition(tagName) {
|
|
|
38
39
|
* Clears the SSR custom-element registry entry for one registered Radiant script module.
|
|
39
40
|
*/
|
|
40
41
|
export function invalidateRadiantRegisteredScriptSsrRegistration(scriptPath) {
|
|
42
|
+
invalidateCustomElementScriptPreload(scriptPath, CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.ecopagesJsx);
|
|
41
43
|
const tagName = resolveRadiantCustomElementTag(scriptPath);
|
|
42
44
|
if (!tagName) {
|
|
43
45
|
return false;
|