@ecopages/ecopages-jsx 0.2.0-rc.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/ecopages-jsx-hmr-ownership.d.ts +25 -13
- package/src/ecopages-jsx-hmr-ownership.js +52 -20
- package/src/ecopages-jsx-mdx.d.ts +0 -1
- package/src/ecopages-jsx-mdx.js +0 -12
- package/src/ecopages-jsx-renderer.d.ts +1 -8
- package/src/ecopages-jsx-renderer.js +26 -73
- package/src/ecopages-jsx.plugin.d.ts +5 -9
- package/src/ecopages-jsx.plugin.js +8 -29
- package/src/page-owned-content-script-grouping.js +1 -1
- package/src/ecopages-jsx-render-session.d.ts +0 -42
- package/src/ecopages-jsx-render-session.js +0 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/ecopages-jsx",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "JSX integration plugin for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -34,12 +34,12 @@
|
|
|
34
34
|
"directory": "packages/integrations/ecopages-jsx"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@ecopages/mdx": "0.2.0
|
|
37
|
+
"@ecopages/mdx": "0.2.0",
|
|
38
38
|
"@mdx-js/mdx": "^3.1.1",
|
|
39
39
|
"vfile": "^6.0.3"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@ecopages/core": "0.2.0
|
|
42
|
+
"@ecopages/core": "0.2.0",
|
|
43
43
|
"@ecopages/jsx": "^0.3.0-rc.9",
|
|
44
44
|
"@ecopages/radiant": "^0.3.0-rc.9"
|
|
45
45
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tracks the source files that
|
|
2
|
+
* Tracks the source files that produced the most recent JSX render.
|
|
3
3
|
*
|
|
4
4
|
* @remarks
|
|
5
|
-
* The JSX
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The JSX renderer runs each page, component, or view render inside
|
|
6
|
+
* {@link withEjsxHmrOwnershipScope} and records its components with
|
|
7
|
+
* {@link recordEjsxHmrOwnership}. `EcopagesJsxHmrStrategy` reads the result to
|
|
8
|
+
* decide whether a changed file affects SSR HTML without walking the component
|
|
9
|
+
* tree on every watcher event.
|
|
9
10
|
*
|
|
10
|
-
* The state is module-scoped because the HMR strategy
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The state is module-scoped because the renderer and the HMR strategy share no
|
|
12
|
+
* other lifecycle anchor. Publishing replaces it only when the file set changes,
|
|
13
|
+
* so repeated renders of the same page keep the existing state.
|
|
14
|
+
*
|
|
15
|
+
* Only the latest top-level render is kept. With several pages open, a file
|
|
16
|
+
* used only by a page that rendered earlier is not matched until that page
|
|
17
|
+
* renders again.
|
|
14
18
|
*/
|
|
15
19
|
import type { EcoComponent } from '@ecopages/core';
|
|
16
20
|
export type EjsxHmrOwnershipState = {
|
|
@@ -32,13 +36,21 @@ export declare function getEjsxHmrOwnership(): EjsxHmrOwnershipState;
|
|
|
32
36
|
*/
|
|
33
37
|
export declare function updateEjsxHmrOwnership(components: ReadonlyArray<EcoComponent | Partial<EcoComponent> | undefined>): void;
|
|
34
38
|
/**
|
|
35
|
-
*
|
|
39
|
+
* Runs one render and publishes the files it recorded once the outermost scope ends.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Nested renders, such as components and foreign subtrees resolved inside a
|
|
43
|
+
* page, join the outer scope, so the published set covers the whole render tree
|
|
44
|
+
* instead of the last nested render.
|
|
36
45
|
*/
|
|
37
|
-
export declare function
|
|
46
|
+
export declare function withEjsxHmrOwnershipScope<T>(render: () => T | Promise<T>): Promise<T>;
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
48
|
+
* Adds the source files of `components`, their declared dependencies and their
|
|
49
|
+
* layouts to the active render scope.
|
|
50
|
+
*
|
|
51
|
+
* @throws Error when called outside {@link withEjsxHmrOwnershipScope}.
|
|
40
52
|
*/
|
|
41
|
-
export declare function
|
|
53
|
+
export declare function recordEjsxHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
|
|
42
54
|
/**
|
|
43
55
|
* Resets the state to empty. Used by tests and by HMR manager teardown.
|
|
44
56
|
*/
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tracks the source files that
|
|
2
|
+
* Tracks the source files that produced the most recent JSX render.
|
|
3
3
|
*
|
|
4
4
|
* @remarks
|
|
5
|
-
* The JSX
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The JSX renderer runs each page, component, or view render inside
|
|
6
|
+
* {@link withEjsxHmrOwnershipScope} and records its components with
|
|
7
|
+
* {@link recordEjsxHmrOwnership}. `EcopagesJsxHmrStrategy` reads the result to
|
|
8
|
+
* decide whether a changed file affects SSR HTML without walking the component
|
|
9
|
+
* tree on every watcher event.
|
|
9
10
|
*
|
|
10
|
-
* The state is module-scoped because the HMR strategy
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The state is module-scoped because the renderer and the HMR strategy share no
|
|
12
|
+
* other lifecycle anchor. Publishing replaces it only when the file set changes,
|
|
13
|
+
* so repeated renders of the same page keep the existing state.
|
|
14
|
+
*
|
|
15
|
+
* Only the latest top-level render is kept. With several pages open, a file
|
|
16
|
+
* used only by a page that rendered earlier is not matched until that page
|
|
17
|
+
* renders again.
|
|
14
18
|
*/
|
|
19
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
15
20
|
import { rapidhash } from '@ecopages/core/hash';
|
|
16
21
|
import { collectComponentConfigFilePaths } from '@ecopages/core/route-renderer/page-loading/file-scoped-dependency-components';
|
|
17
22
|
const EMPTY_STATE = {
|
|
@@ -19,6 +24,7 @@ const EMPTY_STATE = {
|
|
|
19
24
|
hash: '',
|
|
20
25
|
};
|
|
21
26
|
let currentState = EMPTY_STATE;
|
|
27
|
+
const pendingFileOwners = new AsyncLocalStorage();
|
|
22
28
|
/**
|
|
23
29
|
* Returns the most recently recorded active render tree.
|
|
24
30
|
*/
|
|
@@ -38,17 +44,49 @@ export function updateEjsxHmrOwnership(components) {
|
|
|
38
44
|
publishEjsxHmrOwnership(collectFileOwners(components));
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
|
-
*
|
|
47
|
+
* Runs one render and publishes the files it recorded once the outermost scope ends.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Nested renders, such as components and foreign subtrees resolved inside a
|
|
51
|
+
* page, join the outer scope, so the published set covers the whole render tree
|
|
52
|
+
* instead of the last nested render.
|
|
42
53
|
*/
|
|
43
|
-
export function
|
|
54
|
+
export async function withEjsxHmrOwnershipScope(render) {
|
|
55
|
+
if (pendingFileOwners.getStore()) {
|
|
56
|
+
return await render();
|
|
57
|
+
}
|
|
58
|
+
const fileOwners = new Set();
|
|
59
|
+
try {
|
|
60
|
+
return await pendingFileOwners.run(fileOwners, render);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
if (fileOwners.size > 0) {
|
|
64
|
+
publishEjsxHmrOwnership(fileOwners);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Adds the source files of `components`, their declared dependencies and their
|
|
70
|
+
* layouts to the active render scope.
|
|
71
|
+
*
|
|
72
|
+
* @throws Error when called outside {@link withEjsxHmrOwnershipScope}.
|
|
73
|
+
*/
|
|
74
|
+
export function recordEjsxHmrOwnership(components) {
|
|
75
|
+
const fileOwners = pendingFileOwners.getStore();
|
|
76
|
+
if (!fileOwners) {
|
|
77
|
+
throw new Error('Ecopages JSX HMR ownership can only be recorded inside an active render scope.');
|
|
78
|
+
}
|
|
44
79
|
for (const file of collectFileOwners(components)) {
|
|
45
|
-
|
|
80
|
+
fileOwners.add(file);
|
|
46
81
|
}
|
|
47
82
|
}
|
|
48
83
|
/**
|
|
49
|
-
*
|
|
84
|
+
* Resets the state to empty. Used by tests and by HMR manager teardown.
|
|
50
85
|
*/
|
|
51
|
-
export function
|
|
86
|
+
export function resetEjsxHmrOwnership() {
|
|
87
|
+
currentState = EMPTY_STATE;
|
|
88
|
+
}
|
|
89
|
+
function publishEjsxHmrOwnership(files) {
|
|
52
90
|
const hash = hashFileSet(files);
|
|
53
91
|
if (hash === currentState.hash) {
|
|
54
92
|
return;
|
|
@@ -58,12 +96,6 @@ export function publishEjsxHmrOwnership(files) {
|
|
|
58
96
|
hash,
|
|
59
97
|
};
|
|
60
98
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Resets the state to empty. Used by tests and by HMR manager teardown.
|
|
63
|
-
*/
|
|
64
|
-
export function resetEjsxHmrOwnership() {
|
|
65
|
-
currentState = EMPTY_STATE;
|
|
66
|
-
}
|
|
67
99
|
function collectFileOwners(components) {
|
|
68
100
|
return collectComponentConfigFilePaths(components, { includeLayouts: true });
|
|
69
101
|
}
|
|
@@ -19,6 +19,5 @@ export interface CreateEcopagesJsxMdxLoaderPluginOptions {
|
|
|
19
19
|
projectRoot: string;
|
|
20
20
|
}
|
|
21
21
|
export declare const createMdxLoaderPlugin: (options: CreateEcopagesJsxMdxLoaderPluginOptions) => EcoBuildPlugin;
|
|
22
|
-
export declare const registerBunMdxPlugin: (options: CreateEcopagesJsxMdxLoaderPluginOptions) => Promise<void>;
|
|
23
22
|
export declare const isMdxFile: (filePath: string, extensions: string[]) => boolean;
|
|
24
23
|
export declare const normalizeMdxPageModule: (file: string, module: EcopagesJsxMdxPageModule) => EcopagesJsxMdxPageModule;
|
package/src/ecopages-jsx-mdx.js
CHANGED
|
@@ -18,18 +18,6 @@ export const createMdxLoaderPlugin = (options) => createMdxLoaderPluginCore({
|
|
|
18
18
|
loader: 'js',
|
|
19
19
|
projectRoot: options.projectRoot,
|
|
20
20
|
});
|
|
21
|
-
export const registerBunMdxPlugin = async (options) => {
|
|
22
|
-
if (typeof Bun === 'undefined') {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
const plugin = createMdxLoaderPlugin(options);
|
|
26
|
-
Bun.plugin({
|
|
27
|
-
name: plugin.name,
|
|
28
|
-
setup(build) {
|
|
29
|
-
return plugin.setup(build);
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
};
|
|
33
21
|
export const isMdxFile = (filePath, extensions) => {
|
|
34
22
|
return extensions.some((ext) => filePath.endsWith(ext));
|
|
35
23
|
};
|
|
@@ -14,7 +14,6 @@ export type { EcopagesJsxRendererConfig, EcopagesJsxRendererOptions } from './ec
|
|
|
14
14
|
export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderable> {
|
|
15
15
|
name: string;
|
|
16
16
|
private readonly mdxExtensions;
|
|
17
|
-
private readonly renderSession;
|
|
18
17
|
private readonly radiantSsrPolicy;
|
|
19
18
|
private readonly ssrScriptPreloader;
|
|
20
19
|
/**
|
|
@@ -34,7 +33,7 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
34
33
|
private isJsxSerializableForeignChild;
|
|
35
34
|
/**
|
|
36
35
|
* Re-renders queued JSX children inside the owning renderer so nested custom
|
|
37
|
-
* elements and queued foreign subtrees
|
|
36
|
+
* elements and queued foreign subtrees resolve here rather than as opaque markup.
|
|
38
37
|
*/
|
|
39
38
|
private renderQueuedForeignSubtreeChildren;
|
|
40
39
|
protected createForeignChildRuntime(options: {
|
|
@@ -52,10 +51,4 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
52
51
|
private withCustomElementRenderHook;
|
|
53
52
|
private withPreparedRadiantRuntime;
|
|
54
53
|
private createIntrinsicCustomElementRenderHook;
|
|
55
|
-
/**
|
|
56
|
-
* Records the source files that produced the current render so
|
|
57
|
-
* {@link EcopagesJsxHmrStrategy} can match later watcher events without
|
|
58
|
-
* re-walking the component tree.
|
|
59
|
-
*/
|
|
60
|
-
private recordHmrOwnership;
|
|
61
54
|
}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { IntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/integration-renderer';
|
|
2
2
|
import { resolveDocumentShellLayouts } from '@ecopages/core/route-renderer/orchestration/document-shell/layout-shell-props.service';
|
|
3
|
-
import { getForeignSubtreeResolutionContextKey, resolveOwningIntegrationRenderer, } from '@ecopages/core/route-renderer/orchestration/foreign-child/owning-renderer-resolution';
|
|
4
3
|
import { createMarkupNodeLike } from '@ecopages/jsx';
|
|
5
4
|
import { renderToString, withServerCustomElementRenderHook } from '@ecopages/jsx/server';
|
|
6
5
|
import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
|
|
7
6
|
import { isMdxFile, normalizeMdxPageModule, } from './ecopages-jsx-mdx.js';
|
|
8
|
-
import {
|
|
7
|
+
import { recordEjsxHmrOwnership, withEjsxHmrOwnershipScope } from './ecopages-jsx-hmr-ownership.js';
|
|
9
8
|
import { EcopagesJsxRadiantSsrPolicy } from './ecopages-jsx-radiant-ssr-policy.js';
|
|
10
9
|
import { CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES, CustomElementScriptPreloader, } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-script-preloader';
|
|
11
|
-
import {
|
|
10
|
+
import { createSharedServerModuleImporter } from '@ecopages/core/route-renderer/orchestration/custom-element-scripts/custom-element-server-module-importer';
|
|
12
11
|
/**
|
|
13
12
|
* Local Ecopages renderer for JSX templates in the docs app.
|
|
14
13
|
*
|
|
@@ -18,7 +17,6 @@ import { createCustomElementSsrPreloadEntrypointResolver } from '@ecopages/core/
|
|
|
18
17
|
export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
19
18
|
name = ECOPAGES_JSX_PLUGIN_NAME;
|
|
20
19
|
mdxExtensions;
|
|
21
|
-
renderSession;
|
|
22
20
|
radiantSsrPolicy;
|
|
23
21
|
ssrScriptPreloader;
|
|
24
22
|
/**
|
|
@@ -75,26 +73,15 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
75
73
|
}
|
|
76
74
|
/**
|
|
77
75
|
* Re-renders queued JSX children inside the owning renderer so nested custom
|
|
78
|
-
* elements and queued foreign subtrees
|
|
76
|
+
* elements and queued foreign subtrees resolve here rather than as opaque markup.
|
|
79
77
|
*/
|
|
80
78
|
async renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken) {
|
|
81
79
|
if (children === undefined) {
|
|
82
|
-
return {
|
|
80
|
+
return {};
|
|
83
81
|
}
|
|
84
|
-
|
|
85
|
-
let html;
|
|
86
|
-
if (typeof children === 'string') {
|
|
87
|
-
html = children;
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
const renderedChildren = await this.renderJsx(children);
|
|
91
|
-
html = renderedChildren.html;
|
|
92
|
-
assets = renderedChildren.assets;
|
|
93
|
-
}
|
|
94
|
-
html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken);
|
|
82
|
+
const html = typeof children === 'string' ? children : await this.renderJsx(children);
|
|
95
83
|
return {
|
|
96
|
-
|
|
97
|
-
html,
|
|
84
|
+
html: await this.foreignSubtreeExecutionService.resolveQueuedTokens(html, queuedResolutionsByToken, resolveToken),
|
|
98
85
|
};
|
|
99
86
|
}
|
|
100
87
|
createForeignChildRuntime(options) {
|
|
@@ -124,26 +111,25 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
124
111
|
runtimeOrigin,
|
|
125
112
|
});
|
|
126
113
|
this.mdxExtensions = jsxConfig?.mdxExtensions ?? ['.mdx'];
|
|
127
|
-
this.renderSession = new EcopagesJsxRenderSession((assets) => this.htmlTransformer.dedupeProcessedAssets(assets));
|
|
128
114
|
const radiantSsrEnabled = jsxConfig?.radiantSsrEnabled ?? false;
|
|
129
115
|
this.radiantSsrPolicy = jsxConfig?.radiantSsrPolicy ?? new EcopagesJsxRadiantSsrPolicy(radiantSsrEnabled);
|
|
130
116
|
const preferSourceImports = typeof Bun !== 'undefined';
|
|
131
117
|
this.ssrScriptPreloader = new CustomElementScriptPreloader({
|
|
132
118
|
cacheScope: CUSTOM_ELEMENT_SSR_PRELOAD_CACHE_SCOPES.ecopagesJsx,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
* Node production builds evaluate registration scripts through the
|
|
136
|
-
* asset pipeline during render. Preloading through the app module loader
|
|
137
|
-
* registers Radiant hosts in an isolated graph and breaks SSR.
|
|
138
|
-
*/
|
|
139
|
-
enabled: radiantSsrEnabled && preferSourceImports,
|
|
119
|
+
collectForIntegration: ECOPAGES_JSX_PLUGIN_NAME,
|
|
120
|
+
enabled: radiantSsrEnabled,
|
|
140
121
|
logLabel: 'ecopages-jsx',
|
|
141
122
|
preferSourceImports,
|
|
142
123
|
resolveDependencyPath: (componentDir, sourcePath) => this.resolveDependencyPath(componentDir, sourcePath),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
124
|
+
/**
|
|
125
|
+
* @remarks
|
|
126
|
+
* Bun imports the source file through `preferSourceImports`. Node must
|
|
127
|
+
* not: dev asset output is still the `.tsx` source, and a browser bundle
|
|
128
|
+
* is a second Radiant graph. The page server-module loader transpiles
|
|
129
|
+
* the script and externalizes packages, so `customElements.define` hits
|
|
130
|
+
* the same module instances the page already loaded.
|
|
131
|
+
*/
|
|
132
|
+
importServerModule: preferSourceImports ? undefined : createSharedServerModuleImporter(this.appConfig),
|
|
147
133
|
});
|
|
148
134
|
}
|
|
149
135
|
/** Returns whether the requested page file should be treated as MDX. */
|
|
@@ -164,7 +150,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
164
150
|
options.HtmlTemplate,
|
|
165
151
|
...(options.resolvedPageDependencyComponents ?? []),
|
|
166
152
|
]);
|
|
167
|
-
return await
|
|
153
|
+
return await withEjsxHmrOwnershipScope(async () => {
|
|
168
154
|
try {
|
|
169
155
|
const result = await this.renderPageWithDocumentShell({
|
|
170
156
|
page: {
|
|
@@ -191,7 +177,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
191
177
|
metadata: options.metadata,
|
|
192
178
|
pageProps: options.pageProps ?? {},
|
|
193
179
|
});
|
|
194
|
-
|
|
180
|
+
recordEjsxHmrOwnership([
|
|
195
181
|
options.Page,
|
|
196
182
|
options.Layout,
|
|
197
183
|
options.HtmlTemplate,
|
|
@@ -208,8 +194,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
208
194
|
async renderComponent(input) {
|
|
209
195
|
return await this.withPreparedRadiantRuntime(async () => {
|
|
210
196
|
await this.ssrScriptPreloader.preloadSsrScripts([input.component]);
|
|
211
|
-
return await
|
|
212
|
-
const assetFrame = this.renderSession.beginCollectedAssetFrame();
|
|
197
|
+
return await withEjsxHmrOwnershipScope(async () => {
|
|
213
198
|
try {
|
|
214
199
|
if (typeof input.component !== 'function') {
|
|
215
200
|
throw new TypeError('JSX renderer expected a callable component.');
|
|
@@ -224,34 +209,17 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
224
209
|
: input.children,
|
|
225
210
|
};
|
|
226
211
|
const content = await this.withCustomElementRenderHook(() => component(componentProps));
|
|
227
|
-
const
|
|
228
|
-
const queuedForeignSubtreeResolution = await this.
|
|
229
|
-
currentIntegrationName: this.name,
|
|
230
|
-
html: rendered.html,
|
|
231
|
-
runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(input, getForeignSubtreeResolutionContextKey(this.name)),
|
|
232
|
-
queueLabel: 'Ecopages JSX',
|
|
233
|
-
getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
|
|
234
|
-
appConfig: this.appConfig,
|
|
235
|
-
runtimeOrigin: this.runtimeOrigin,
|
|
236
|
-
currentIntegrationName: this.name,
|
|
237
|
-
currentRenderer: this,
|
|
238
|
-
integrationName,
|
|
239
|
-
cache: rendererCache,
|
|
240
|
-
}),
|
|
241
|
-
applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
|
|
242
|
-
dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
|
|
243
|
-
renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken),
|
|
244
|
-
});
|
|
212
|
+
const html = await this.renderJsx(content);
|
|
213
|
+
const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtrees(html, this.getQueuedForeignSubtreeContext(input), (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken));
|
|
245
214
|
const componentAssets = input.component.config?.dependencies &&
|
|
246
215
|
typeof this.assetProcessingService?.processDependencies === 'function'
|
|
247
216
|
? await this.processComponentDependencies([input.component])
|
|
248
217
|
: [];
|
|
249
218
|
const assets = this.htmlTransformer.dedupeProcessedAssets([
|
|
250
|
-
...this.renderSession.endCollectedAssetFrame(assetFrame),
|
|
251
219
|
...queuedForeignSubtreeResolution.assets,
|
|
252
220
|
...componentAssets,
|
|
253
221
|
]);
|
|
254
|
-
|
|
222
|
+
recordEjsxHmrOwnership([input.component]);
|
|
255
223
|
return this.finalizeIslandComponentRender(input, {
|
|
256
224
|
html: queuedForeignSubtreeResolution.html,
|
|
257
225
|
canAttachAttributes: true,
|
|
@@ -261,7 +229,6 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
261
229
|
});
|
|
262
230
|
}
|
|
263
231
|
catch (error) {
|
|
264
|
-
this.renderSession.endCollectedAssetFrame(assetFrame);
|
|
265
232
|
throw this.createRenderError('Error rendering component', error);
|
|
266
233
|
}
|
|
267
234
|
});
|
|
@@ -272,7 +239,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
272
239
|
const viewComponent = view;
|
|
273
240
|
const layouts = viewComponent.config?.layouts;
|
|
274
241
|
await this.ssrScriptPreloader.preloadSsrScripts([view, ...(layouts ?? [])]);
|
|
275
|
-
return await
|
|
242
|
+
return await withEjsxHmrOwnershipScope(async () => {
|
|
276
243
|
try {
|
|
277
244
|
if (typeof view !== 'function') {
|
|
278
245
|
throw new TypeError('JSX renderer expected a callable view component.');
|
|
@@ -283,7 +250,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
283
250
|
ctx,
|
|
284
251
|
layout: layouts?.[layouts.length - 1],
|
|
285
252
|
});
|
|
286
|
-
|
|
253
|
+
recordEjsxHmrOwnership([view]);
|
|
287
254
|
return response;
|
|
288
255
|
}
|
|
289
256
|
catch (error) {
|
|
@@ -293,13 +260,7 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
293
260
|
});
|
|
294
261
|
}
|
|
295
262
|
async renderJsx(value) {
|
|
296
|
-
|
|
297
|
-
const html = await this.withCustomElementRenderHook(() => renderToString(value));
|
|
298
|
-
const dedupedAssets = this.renderSession.recordCollectedAssets(collectedAssets);
|
|
299
|
-
return {
|
|
300
|
-
assets: dedupedAssets,
|
|
301
|
-
html,
|
|
302
|
-
};
|
|
263
|
+
return await this.withCustomElementRenderHook(() => renderToString(value));
|
|
303
264
|
}
|
|
304
265
|
async withCustomElementRenderHook(render) {
|
|
305
266
|
return await this.radiantSsrPolicy.withRuntime(() => withServerCustomElementRenderHook(this.createIntrinsicCustomElementRenderHook(), render));
|
|
@@ -313,12 +274,4 @@ export class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
313
274
|
return instance ? this.radiantSsrPolicy.renderIntrinsicElementMarkup(instance) : undefined;
|
|
314
275
|
};
|
|
315
276
|
}
|
|
316
|
-
/**
|
|
317
|
-
* Records the source files that produced the current render so
|
|
318
|
-
* {@link EcopagesJsxHmrStrategy} can match later watcher events without
|
|
319
|
-
* re-walking the component tree.
|
|
320
|
-
*/
|
|
321
|
-
recordHmrOwnership(components) {
|
|
322
|
-
this.renderSession.mergeHmrOwnership(components);
|
|
323
|
-
}
|
|
324
277
|
}
|
|
@@ -10,7 +10,6 @@ export type { EcopagesJsxMdxOptions, EcopagesJsxPluginOptions, EcopagesJsxRender
|
|
|
10
10
|
export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable> {
|
|
11
11
|
renderer: typeof EcopagesJsxRenderer;
|
|
12
12
|
private includeRadiant;
|
|
13
|
-
private mdxEnabled;
|
|
14
13
|
private mdxCompilerOptions?;
|
|
15
14
|
private mdxExtensions;
|
|
16
15
|
private mdxLoaderPlugin?;
|
|
@@ -39,17 +38,14 @@ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable>
|
|
|
39
38
|
*/
|
|
40
39
|
getHmrStrategy(): HmrStrategy | undefined;
|
|
41
40
|
/**
|
|
42
|
-
*
|
|
41
|
+
* Ensures the MDX loader exists, then completes the base integration setup.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* Bun registers `integration.plugins` after `setup()` returns. Calling
|
|
45
|
+
* `Bun.plugin` here would install the same MDX loader twice.
|
|
43
46
|
*/
|
|
44
47
|
setup(): Promise<void>;
|
|
45
48
|
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
49
|
}
|
|
54
50
|
/**
|
|
55
51
|
* 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';
|
|
@@ -30,7 +30,6 @@ const resolvePluginOptions = (options) => {
|
|
|
30
30
|
...baseConfig,
|
|
31
31
|
extensions,
|
|
32
32
|
includeRadiant: radiant ?? true,
|
|
33
|
-
mdxEnabled,
|
|
34
33
|
mdxExtensions,
|
|
35
34
|
mdxCompilerOptions: mdxEnabled && mdx ? resolveMdxCompilerOptions(mdx) : undefined,
|
|
36
35
|
};
|
|
@@ -39,7 +38,6 @@ const resolvePluginOptions = (options) => {
|
|
|
39
38
|
export class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
40
39
|
renderer = EcopagesJsxRenderer;
|
|
41
40
|
includeRadiant;
|
|
42
|
-
mdxEnabled;
|
|
43
41
|
mdxCompilerOptions;
|
|
44
42
|
mdxExtensions;
|
|
45
43
|
mdxLoaderPlugin;
|
|
@@ -59,7 +57,7 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
59
57
|
}
|
|
60
58
|
constructor(options) {
|
|
61
59
|
const config = resolvePluginOptions(options);
|
|
62
|
-
const { extensions, includeRadiant,
|
|
60
|
+
const { extensions, includeRadiant, mdxExtensions, mdxCompilerOptions, ...baseConfig } = config;
|
|
63
61
|
super({
|
|
64
62
|
name: ECOPAGES_JSX_PLUGIN_NAME,
|
|
65
63
|
extensions,
|
|
@@ -67,7 +65,6 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
67
65
|
...baseConfig,
|
|
68
66
|
});
|
|
69
67
|
this.includeRadiant = includeRadiant;
|
|
70
|
-
this.mdxEnabled = mdxEnabled;
|
|
71
68
|
this.mdxExtensions = mdxExtensions;
|
|
72
69
|
this.mdxCompilerOptions = mdxCompilerOptions;
|
|
73
70
|
if (this.includeRadiant) {
|
|
@@ -137,17 +134,18 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
137
134
|
});
|
|
138
135
|
}
|
|
139
136
|
/**
|
|
140
|
-
*
|
|
137
|
+
* Ensures the MDX loader exists, then completes the base integration setup.
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Bun registers `integration.plugins` after `setup()` returns. Calling
|
|
141
|
+
* `Bun.plugin` here would install the same MDX loader twice.
|
|
141
142
|
*/
|
|
142
143
|
async setup() {
|
|
143
144
|
this.ensureMdxLoaderPlugin();
|
|
144
|
-
if (typeof Bun !== 'undefined' && this.mdxEnabled && this.mdxCompilerOptions) {
|
|
145
|
-
await this.registerMdxBunPlugin();
|
|
146
|
-
}
|
|
147
145
|
await super.setup();
|
|
148
146
|
}
|
|
149
147
|
ensureMdxLoaderPlugin() {
|
|
150
|
-
if (!this.
|
|
148
|
+
if (!this.mdxCompilerOptions || this.mdxLoaderPlugin) {
|
|
151
149
|
return;
|
|
152
150
|
}
|
|
153
151
|
if (!this.appConfig?.rootDir) {
|
|
@@ -159,25 +157,6 @@ export class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
159
157
|
projectRoot: this.appConfig.rootDir,
|
|
160
158
|
});
|
|
161
159
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Registers Bun's MDX loader at runtime setup time.
|
|
164
|
-
*
|
|
165
|
-
* Build-time contribution collection may run where Bun is absent, so
|
|
166
|
-
* this hook stays isolated from manifest preparation.
|
|
167
|
-
*/
|
|
168
|
-
async registerMdxBunPlugin() {
|
|
169
|
-
if (typeof Bun === 'undefined' || !this.mdxCompilerOptions) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (!this.appConfig) {
|
|
173
|
-
throw new Error('[EcopagesJsxPlugin] Cannot register Bun MDX plugin: appConfig is not set');
|
|
174
|
-
}
|
|
175
|
-
await registerBunMdxPlugin({
|
|
176
|
-
compilerOptions: this.mdxCompilerOptions,
|
|
177
|
-
extensions: this.mdxExtensions,
|
|
178
|
-
projectRoot: this.appConfig.rootDir,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
160
|
}
|
|
182
161
|
/**
|
|
183
162
|
* Creates the JSX integration plugin with resolved defaults.
|
|
@@ -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,42 +0,0 @@
|
|
|
1
|
-
import type { EcoComponent } from '@ecopages/core';
|
|
2
|
-
import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
|
|
3
|
-
export declare const ECOPAGES_JSX_SSR_RENDER_STATE_KEY: unique symbol;
|
|
4
|
-
/**
|
|
5
|
-
* Tracks JSX SSR asset-collection state for one active render flow.
|
|
6
|
-
*
|
|
7
|
-
* @remarks
|
|
8
|
-
* The renderer still mirrors this state into `@ecopages/jsx/server` so nested
|
|
9
|
-
* JSX renders and existing SSR-scope probes can observe it, but it also keeps
|
|
10
|
-
* an internal async-local copy. That fallback avoids losing renderer state when
|
|
11
|
-
* host runtimes such as Vite end up loading separate module identities for the
|
|
12
|
-
* JSX server helpers.
|
|
13
|
-
*/
|
|
14
|
-
export declare class EcopagesJsxRenderSession {
|
|
15
|
-
private readonly dedupeProcessedAssets;
|
|
16
|
-
constructor(dedupeProcessedAssets: (assets: ProcessedAsset[]) => ProcessedAsset[]);
|
|
17
|
-
/**
|
|
18
|
-
* Runs one render inside the active session scope.
|
|
19
|
-
*
|
|
20
|
-
* @remarks
|
|
21
|
-
* When a render is already active, the current session state is reused and
|
|
22
|
-
* mirrored back into the JSX SSR scope so nested `renderToString()` calls see
|
|
23
|
-
* the same asset frame stack. When no session exists yet, a new state is
|
|
24
|
-
* created and published through both the internal async-local store and the
|
|
25
|
-
* JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
|
|
26
|
-
* published once when that scope completes.
|
|
27
|
-
*/
|
|
28
|
-
withActiveScope<T>(render: () => T | Promise<T>): Promise<T>;
|
|
29
|
-
beginCollectedAssetFrame(): ProcessedAsset[];
|
|
30
|
-
endCollectedAssetFrame(frame: ProcessedAsset[]): ProcessedAsset[];
|
|
31
|
-
mergeHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
|
|
32
|
-
recordCollectedAssets(collectedAssets: ProcessedAsset[]): ProcessedAsset[];
|
|
33
|
-
/**
|
|
34
|
-
* Resolves the current render-session state from the local async scope first.
|
|
35
|
-
*
|
|
36
|
-
* @remarks
|
|
37
|
-
* The fallback read from `@ecopages/jsx/server` keeps compatibility with code
|
|
38
|
-
* that still inspects the public SSR scope directly, while the async-local
|
|
39
|
-
* store remains the authoritative source for renderer-owned bookkeeping.
|
|
40
|
-
*/
|
|
41
|
-
private getState;
|
|
42
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
import { getActiveSsrScopeValue, withActiveSsrScopeValue } from '@ecopages/jsx/server';
|
|
3
|
-
import { mergeEjsxHmrOwnership, publishEjsxHmrOwnership } from './ecopages-jsx-hmr-ownership.js';
|
|
4
|
-
export const ECOPAGES_JSX_SSR_RENDER_STATE_KEY = Symbol.for('@ecopages/ecopages-jsx.ssr-render-state');
|
|
5
|
-
const renderStateStorage = new AsyncLocalStorage();
|
|
6
|
-
function ensurePendingHmrFileOwners(state) {
|
|
7
|
-
if (!state.pendingHmrFileOwners) {
|
|
8
|
-
state.pendingHmrFileOwners = new Set();
|
|
9
|
-
}
|
|
10
|
-
return state.pendingHmrFileOwners;
|
|
11
|
-
}
|
|
12
|
-
function commitPendingHmrOwnership(state) {
|
|
13
|
-
const pending = state.pendingHmrFileOwners;
|
|
14
|
-
if (!pending || pending.size === 0) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
publishEjsxHmrOwnership(pending);
|
|
18
|
-
pending.clear();
|
|
19
|
-
}
|
|
20
|
-
async function runWithCommittedHmrOwnership(state, render) {
|
|
21
|
-
try {
|
|
22
|
-
return await render();
|
|
23
|
-
}
|
|
24
|
-
finally {
|
|
25
|
-
commitPendingHmrOwnership(state);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Tracks JSX SSR asset-collection state for one active render flow.
|
|
30
|
-
*
|
|
31
|
-
* @remarks
|
|
32
|
-
* The renderer still mirrors this state into `@ecopages/jsx/server` so nested
|
|
33
|
-
* JSX renders and existing SSR-scope probes can observe it, but it also keeps
|
|
34
|
-
* an internal async-local copy. That fallback avoids losing renderer state when
|
|
35
|
-
* host runtimes such as Vite end up loading separate module identities for the
|
|
36
|
-
* JSX server helpers.
|
|
37
|
-
*/
|
|
38
|
-
export class EcopagesJsxRenderSession {
|
|
39
|
-
dedupeProcessedAssets;
|
|
40
|
-
constructor(dedupeProcessedAssets) {
|
|
41
|
-
this.dedupeProcessedAssets = dedupeProcessedAssets;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Runs one render inside the active session scope.
|
|
45
|
-
*
|
|
46
|
-
* @remarks
|
|
47
|
-
* When a render is already active, the current session state is reused and
|
|
48
|
-
* mirrored back into the JSX SSR scope so nested `renderToString()` calls see
|
|
49
|
-
* the same asset frame stack. When no session exists yet, a new state is
|
|
50
|
-
* created and published through both the internal async-local store and the
|
|
51
|
-
* JSX SSR scope bridge. HMR ownership accumulated during the outer scope is
|
|
52
|
-
* published once when that scope completes.
|
|
53
|
-
*/
|
|
54
|
-
async withActiveScope(render) {
|
|
55
|
-
const activeState = renderStateStorage.getStore();
|
|
56
|
-
if (activeState) {
|
|
57
|
-
return await withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, activeState, render);
|
|
58
|
-
}
|
|
59
|
-
const jsxScopeState = getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
|
|
60
|
-
if (jsxScopeState) {
|
|
61
|
-
ensurePendingHmrFileOwners(jsxScopeState);
|
|
62
|
-
return await runWithCommittedHmrOwnership(jsxScopeState, () => renderStateStorage.run(jsxScopeState, () => render()));
|
|
63
|
-
}
|
|
64
|
-
const state = {
|
|
65
|
-
collectedAssetFrames: [],
|
|
66
|
-
pendingHmrFileOwners: new Set(),
|
|
67
|
-
};
|
|
68
|
-
return await runWithCommittedHmrOwnership(state, () => renderStateStorage.run(state, () => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render)));
|
|
69
|
-
}
|
|
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
|
-
mergeHmrOwnership(components) {
|
|
84
|
-
mergeEjsxHmrOwnership(ensurePendingHmrFileOwners(this.getState()), components);
|
|
85
|
-
}
|
|
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
|
-
/**
|
|
96
|
-
* Resolves the current render-session state from the local async scope first.
|
|
97
|
-
*
|
|
98
|
-
* @remarks
|
|
99
|
-
* The fallback read from `@ecopages/jsx/server` keeps compatibility with code
|
|
100
|
-
* that still inspects the public SSR scope directly, while the async-local
|
|
101
|
-
* store remains the authoritative source for renderer-owned bookkeeping.
|
|
102
|
-
*/
|
|
103
|
-
getState() {
|
|
104
|
-
const state = renderStateStorage.getStore() ??
|
|
105
|
-
getActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY);
|
|
106
|
-
if (!state) {
|
|
107
|
-
throw new Error('Ecopages JSX SSR render state is unavailable outside the active render scope.');
|
|
108
|
-
}
|
|
109
|
-
return state;
|
|
110
|
-
}
|
|
111
|
-
}
|