@ecopages/ecopages-jsx 0.2.0-beta.3 → 0.2.0-beta.31
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 +1 -0
- package/package.json +6 -5
- package/src/ecopages-jsx-hmr-ownership.d.ts +37 -0
- package/src/ecopages-jsx-hmr-ownership.js +63 -0
- package/src/ecopages-jsx-hmr-strategy.d.ts +36 -0
- package/src/ecopages-jsx-hmr-strategy.js +63 -0
- package/src/ecopages-jsx-mdx.d.ts +2 -4
- package/src/ecopages-jsx-mdx.js +24 -55
- package/src/ecopages-jsx-radiant-ssr-policy.d.ts +6 -5
- package/src/ecopages-jsx-radiant-ssr-policy.js +39 -13
- package/src/ecopages-jsx-render-session.d.ts +0 -7
- package/src/ecopages-jsx-render-session.js +2 -17
- package/src/ecopages-jsx-renderer.d.ts +22 -9
- package/src/ecopages-jsx-renderer.js +75 -32
- package/src/ecopages-jsx.plugin.d.ts +17 -0
- package/src/ecopages-jsx.plugin.js +44 -1
- package/src/page-owned-content-script-grouping.d.ts +12 -0
- package/src/page-owned-content-script-grouping.js +57 -0
- package/src/radiant-registered-script-ssr-invalidation.d.ts +18 -0
- package/src/radiant-registered-script-ssr-invalidation.js +36 -0
- package/src/resolve-radiant-install-hydrator.d.ts +2 -0
- package/src/resolve-radiant-install-hydrator.js +7 -0
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ Ecopages JSX can own the outer page shell or just a nested foreign subtree. When
|
|
|
99
99
|
|
|
100
100
|
Important:
|
|
101
101
|
|
|
102
|
+
- Cross-integration JSX children must use `EcoEmbed`. Opaque plain objects fail fast at core foreign-child queue boundaries; they are not coerced with `String(object)`.
|
|
102
103
|
- Components that may render foreign children must declare those children in `config.dependencies.components`.
|
|
103
104
|
- Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not treat rendered HTML alone as the source of truth.
|
|
104
105
|
- 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-beta.
|
|
3
|
+
"version": "0.2.0-beta.31",
|
|
4
4
|
"description": "JSX integration plugin for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -27,16 +27,17 @@
|
|
|
27
27
|
},
|
|
28
28
|
"repository": {
|
|
29
29
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/ecopages/ecopages.git",
|
|
30
|
+
"url": "git+https://github.com/ecopages/ecopages.git",
|
|
31
31
|
"directory": "packages/integrations/ecopages-jsx"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@ecopages/mdx": "0.2.0-beta.31",
|
|
34
35
|
"@mdx-js/mdx": "^3.1.1",
|
|
35
36
|
"vfile": "^6.0.3"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
|
-
"@ecopages/core": "0.2.0-beta.
|
|
39
|
-
"@ecopages/jsx": "0.3.0-
|
|
40
|
-
"@ecopages/radiant": "0.3.0-
|
|
39
|
+
"@ecopages/core": "0.2.0-beta.31",
|
|
40
|
+
"@ecopages/jsx": "0.3.0-beta.3",
|
|
41
|
+
"@ecopages/radiant": "0.3.0-beta.3"
|
|
41
42
|
}
|
|
42
43
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks the source files that participate in the active SSR render tree.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* The JSX integration renderer calls {@link updateEjsxHmrOwnership} after each
|
|
6
|
+
* page, component, or view render so the HMR strategy can answer
|
|
7
|
+
* "does this changed file affect SSR HTML?" without re-walking the tree on
|
|
8
|
+
* every watcher event.
|
|
9
|
+
*
|
|
10
|
+
* The state is module-scoped because the HMR strategy and renderer live in
|
|
11
|
+
* different call sites (server strategy vs SSR renderer) and share no other
|
|
12
|
+
* lifecycle anchor. A fresh state is installed only when the source-file set
|
|
13
|
+
* changes, so the renderer's per-render hook stays O(1) in the steady state.
|
|
14
|
+
*/
|
|
15
|
+
import type { EcoComponent } from '@ecopages/core';
|
|
16
|
+
export type EjsxHmrOwnershipState = {
|
|
17
|
+
fileOwners: ReadonlySet<string>;
|
|
18
|
+
hash: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Returns the most recently recorded active render tree.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getEjsxHmrOwnership(): EjsxHmrOwnershipState;
|
|
24
|
+
/**
|
|
25
|
+
* Updates the active render tree from one or more root components.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Walks each root's `config.__eco.file`, `config.dependencies.components[*].config`,
|
|
29
|
+
* and `config.layouts[*].config` recursively, collecting every file path. The
|
|
30
|
+
* state is replaced only when the resulting file set has a different hash, so
|
|
31
|
+
* the renderer's per-render hook is free in the steady state.
|
|
32
|
+
*/
|
|
33
|
+
export declare function updateEjsxHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
|
|
34
|
+
/**
|
|
35
|
+
* Resets the state to empty. Used by tests and by HMR manager teardown.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resetEjsxHmrOwnership(): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { rapidhash } from "@ecopages/core/hash";
|
|
3
|
+
const EMPTY_STATE = {
|
|
4
|
+
fileOwners: /* @__PURE__ */ new Set(),
|
|
5
|
+
hash: ""
|
|
6
|
+
};
|
|
7
|
+
let currentState = EMPTY_STATE;
|
|
8
|
+
function getEjsxHmrOwnership() {
|
|
9
|
+
return currentState;
|
|
10
|
+
}
|
|
11
|
+
function updateEjsxHmrOwnership(components) {
|
|
12
|
+
const files = collectFileOwners(components);
|
|
13
|
+
const hash = hashFileSet(files);
|
|
14
|
+
if (hash === currentState.hash) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
currentState = {
|
|
18
|
+
fileOwners: files,
|
|
19
|
+
hash
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function resetEjsxHmrOwnership() {
|
|
23
|
+
currentState = EMPTY_STATE;
|
|
24
|
+
}
|
|
25
|
+
function collectFileOwners(components) {
|
|
26
|
+
const files = /* @__PURE__ */ new Set();
|
|
27
|
+
const visited = /* @__PURE__ */ new Set();
|
|
28
|
+
const visit = (config) => {
|
|
29
|
+
const file = config?.__eco?.file;
|
|
30
|
+
if (!file) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const resolved = path.resolve(file);
|
|
34
|
+
if (visited.has(resolved)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
visited.add(resolved);
|
|
38
|
+
files.add(resolved);
|
|
39
|
+
for (const dependency of config?.dependencies?.components ?? []) {
|
|
40
|
+
visit(dependency?.config);
|
|
41
|
+
}
|
|
42
|
+
for (const layout of config?.layouts ?? []) {
|
|
43
|
+
visit(layout?.config);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
for (const component of components) {
|
|
47
|
+
visit(component?.config);
|
|
48
|
+
}
|
|
49
|
+
return files;
|
|
50
|
+
}
|
|
51
|
+
function hashFileSet(files) {
|
|
52
|
+
if (files.size === 0) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
const sorted = Array.from(files).sort();
|
|
56
|
+
const joined = sorted.join("\n");
|
|
57
|
+
return rapidhash(joined).toString(36);
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
getEjsxHmrOwnership,
|
|
61
|
+
resetEjsxHmrOwnership,
|
|
62
|
+
updateEjsxHmrOwnership
|
|
63
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HMR strategy for JSX integration-owned SSR source modules.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Edits to files that produce SSR HTML (page sources, layout sources, components
|
|
6
|
+
* that the active render tree depends on) need a soft page refetch so the
|
|
7
|
+
* browser picks up the new markup. Plain `update` events re-import modules on
|
|
8
|
+
* the client but leave the server-rendered DOM stale.
|
|
9
|
+
*
|
|
10
|
+
* The strategy matches a file when any of these hold:
|
|
11
|
+
* - the file lives under `pagesDir` or `layoutsDir` and has a JSX `templatesExt`
|
|
12
|
+
* - the file appears in the active render tree's component dependency graph
|
|
13
|
+
*
|
|
14
|
+
* It defers registered HMR entrypoints to {@link JsHmrStrategy} so script-only
|
|
15
|
+
* edits keep their existing `update`-then-hot-accept path. It also defers
|
|
16
|
+
* include templates and explicit server views to {@link ServerRenderedTemplateHmrStrategy}
|
|
17
|
+
* by returning `false` for those paths.
|
|
18
|
+
*/
|
|
19
|
+
import { HmrStrategy, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
|
|
20
|
+
export interface EcopagesJsxHmrStrategyContext {
|
|
21
|
+
getWatchedFiles(): Map<string, string>;
|
|
22
|
+
getSrcDir(): string;
|
|
23
|
+
getPagesDir(): string;
|
|
24
|
+
getLayoutsDir(): string;
|
|
25
|
+
getIncludesDir(): string;
|
|
26
|
+
getTemplateExtensions(): ReadonlyArray<string>;
|
|
27
|
+
}
|
|
28
|
+
export declare class EcopagesJsxHmrStrategy extends HmrStrategy {
|
|
29
|
+
readonly type: 100;
|
|
30
|
+
private readonly context;
|
|
31
|
+
constructor(context: EcopagesJsxHmrStrategyContext);
|
|
32
|
+
matches(filePath: string): boolean;
|
|
33
|
+
process(filePath: string): Promise<HmrAction>;
|
|
34
|
+
private isPageOrLayoutSource;
|
|
35
|
+
private hasJsxTemplateExtension;
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { HmrStrategy, HmrStrategyType } from "@ecopages/core/hmr/hmr-strategy";
|
|
3
|
+
import { getEjsxHmrOwnership } from "./ecopages-jsx-hmr-ownership.js";
|
|
4
|
+
class EcopagesJsxHmrStrategy extends HmrStrategy {
|
|
5
|
+
type = HmrStrategyType.INTEGRATION;
|
|
6
|
+
context;
|
|
7
|
+
constructor(context) {
|
|
8
|
+
super();
|
|
9
|
+
this.context = context;
|
|
10
|
+
}
|
|
11
|
+
matches(filePath) {
|
|
12
|
+
const resolvedPath = path.resolve(filePath);
|
|
13
|
+
const watchedFiles = this.context.getWatchedFiles();
|
|
14
|
+
if (watchedFiles.has(resolvedPath) || [...watchedFiles.keys()].some((key) => path.resolve(key) === resolvedPath)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const srcDir = path.resolve(this.context.getSrcDir());
|
|
18
|
+
if (!resolvedPath.startsWith(`${srcDir}${path.sep}`) && resolvedPath !== srcDir) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const includesDir = path.resolve(this.context.getIncludesDir());
|
|
22
|
+
if (includesDir && (resolvedPath === includesDir || resolvedPath.startsWith(`${includesDir}${path.sep}`))) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (this.isPageOrLayoutSource(resolvedPath)) {
|
|
26
|
+
return this.hasJsxTemplateExtension(resolvedPath);
|
|
27
|
+
}
|
|
28
|
+
const ownership = getEjsxHmrOwnership();
|
|
29
|
+
if (ownership.fileOwners.has(resolvedPath)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
async process(filePath) {
|
|
35
|
+
return {
|
|
36
|
+
type: "broadcast",
|
|
37
|
+
events: [
|
|
38
|
+
{
|
|
39
|
+
type: "layout-update",
|
|
40
|
+
path: filePath,
|
|
41
|
+
timestamp: Date.now()
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
isPageOrLayoutSource(resolvedPath) {
|
|
47
|
+
const pagesDir = path.resolve(this.context.getPagesDir());
|
|
48
|
+
const layoutsDir = path.resolve(this.context.getLayoutsDir());
|
|
49
|
+
if (pagesDir && (resolvedPath === pagesDir || resolvedPath.startsWith(`${pagesDir}${path.sep}`))) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (layoutsDir && (resolvedPath === layoutsDir || resolvedPath.startsWith(`${layoutsDir}${path.sep}`))) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
hasJsxTemplateExtension(resolvedPath) {
|
|
58
|
+
return this.context.getTemplateExtensions().some((extension) => resolvedPath.endsWith(extension));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export {
|
|
62
|
+
EcopagesJsxHmrStrategy
|
|
63
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CompileOptions } from '@mdx-js/mdx';
|
|
2
2
|
import type { EcoComponent, EcoComponentConfig, EcoFunctionComponent, EcoPageFile, GetMetadata } from '@ecopages/core';
|
|
3
3
|
import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
4
|
+
import { appendMdxExtensions, createMdxExtensionFilter } from '@ecopages/mdx/core';
|
|
4
5
|
import type { JsxRenderable } from '@ecopages/jsx';
|
|
5
6
|
import type { EcopagesJsxMdxCompileOptions, EcopagesJsxMdxOptions } from './ecopages-jsx.types.js';
|
|
6
7
|
export type ResolvedMdxCompileOptions = EcopagesJsxMdxCompileOptions & Pick<CompileOptions, 'jsxImportSource' | 'jsxRuntime'>;
|
|
@@ -10,10 +11,7 @@ export type EcopagesJsxMdxPageModule = EcoPageFile<{
|
|
|
10
11
|
layout?: EcoComponent;
|
|
11
12
|
getMetadata?: GetMetadata;
|
|
12
13
|
}>;
|
|
13
|
-
export
|
|
14
|
-
allowQueryString?: boolean;
|
|
15
|
-
}) => RegExp;
|
|
16
|
-
export declare const appendMdxExtensions: (target: string[], mdxExtensions: string[]) => void;
|
|
14
|
+
export { appendMdxExtensions, createMdxExtensionFilter };
|
|
17
15
|
export declare const resolveMdxCompilerOptions: (mdxOptions: EcopagesJsxMdxOptions) => ResolvedMdxCompileOptions;
|
|
18
16
|
export declare const createMdxLoaderPlugin: (compilerOptions: ResolvedMdxCompileOptions, extensions: string[]) => EcoBuildPlugin;
|
|
19
17
|
export declare const registerBunMdxPlugin: (compilerOptions: ResolvedMdxCompileOptions, extensions: string[]) => Promise<void>;
|
package/src/ecopages-jsx-mdx.js
CHANGED
|
@@ -1,62 +1,28 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
import
|
|
2
|
+
import { ensurePageConfigLayouts } from "@ecopages/core/eco/page-layout-normalization";
|
|
3
3
|
import { rapidhash } from "@ecopages/core/hash";
|
|
4
|
+
import {
|
|
5
|
+
appendMdxExtensions,
|
|
6
|
+
createMdxExtensionFilter,
|
|
7
|
+
createMdxLoaderPlugin as createMdxLoaderPluginCore,
|
|
8
|
+
resolveMdxCompilerOptions as resolveMdxCompilerOptionsCore
|
|
9
|
+
} from "@ecopages/mdx/core";
|
|
4
10
|
import { VFile } from "vfile";
|
|
5
11
|
import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return merged.length > 0 ? merged : void 0;
|
|
10
|
-
};
|
|
11
|
-
const createMdxExtensionFilter = (extensions, options) => {
|
|
12
|
-
const escaped = extensions.map(escapeRegex);
|
|
13
|
-
const suffix = options?.allowQueryString ? "(\\?.*)?$" : "$";
|
|
14
|
-
return new RegExp(`(${escaped.join("|")})${suffix}`);
|
|
15
|
-
};
|
|
16
|
-
const appendMdxExtensions = (target, mdxExtensions) => {
|
|
17
|
-
for (const ext of mdxExtensions) {
|
|
18
|
-
if (!target.includes(ext)) {
|
|
19
|
-
target.push(ext);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
const resolveMdxCompilerOptions = (mdxOptions) => {
|
|
24
|
-
const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = mdxOptions;
|
|
25
|
-
const resolved = {
|
|
12
|
+
const resolveMdxCompilerOptions = (mdxOptions) => resolveMdxCompilerOptionsCore(mdxOptions, {
|
|
13
|
+
jsxImportSource: "@ecopages/jsx",
|
|
14
|
+
defaults: {
|
|
26
15
|
format: "detect",
|
|
27
|
-
outputFormat: "program"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (mergedRehype) resolved.rehypePlugins = mergedRehype;
|
|
38
|
-
if (mergedRecma) resolved.recmaPlugins = mergedRecma;
|
|
39
|
-
return resolved;
|
|
40
|
-
};
|
|
41
|
-
const createMdxLoaderPlugin = (compilerOptions, extensions) => {
|
|
42
|
-
const filter = createMdxExtensionFilter(extensions, { allowQueryString: true });
|
|
43
|
-
return {
|
|
44
|
-
name: "ecopages-jsx-mdx-loader",
|
|
45
|
-
setup(build) {
|
|
46
|
-
build.onLoad({ filter }, async (args) => {
|
|
47
|
-
const { compile } = await import("@mdx-js/mdx");
|
|
48
|
-
const filePath = args.path.includes("?") ? args.path.split("?")[0] : args.path;
|
|
49
|
-
const source = await readFile(filePath, "utf-8");
|
|
50
|
-
const compiled = await compile(new VFile({ value: source, path: filePath }), compilerOptions);
|
|
51
|
-
return {
|
|
52
|
-
contents: String(compiled.value),
|
|
53
|
-
loader: "js",
|
|
54
|
-
resolveDir: path.dirname(filePath)
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
};
|
|
16
|
+
outputFormat: "program"
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const createMdxLoaderPlugin = (compilerOptions, extensions) => createMdxLoaderPluginCore({
|
|
20
|
+
name: "ecopages-jsx-mdx-loader",
|
|
21
|
+
compilerOptions,
|
|
22
|
+
extensions,
|
|
23
|
+
includeSourceMap: false,
|
|
24
|
+
loader: "js"
|
|
25
|
+
});
|
|
60
26
|
const registerBunMdxPlugin = async (compilerOptions, extensions) => {
|
|
61
27
|
if (typeof Bun === "undefined") {
|
|
62
28
|
return;
|
|
@@ -84,13 +50,16 @@ const normalizeMdxPageModule = (file, module) => {
|
|
|
84
50
|
const Page = module.default;
|
|
85
51
|
const normalizedConfig = {
|
|
86
52
|
...module.config ?? Page.config ?? {},
|
|
87
|
-
...module.layout ? { layout: module.layout } : {},
|
|
88
53
|
__eco: module.config?.__eco ?? Page.config?.__eco ?? {
|
|
89
54
|
id: String(rapidhash(file)),
|
|
90
55
|
file,
|
|
91
56
|
integration: ECOPAGES_JSX_PLUGIN_NAME
|
|
92
57
|
}
|
|
93
58
|
};
|
|
59
|
+
if (module.layout && !(normalizedConfig.layouts && normalizedConfig.layouts.length > 0)) {
|
|
60
|
+
normalizedConfig.layout = module.layout;
|
|
61
|
+
}
|
|
62
|
+
ensurePageConfigLayouts(normalizedConfig);
|
|
94
63
|
const wrappedPage = async (props) => await Page(props);
|
|
95
64
|
wrappedPage.config = normalizedConfig;
|
|
96
65
|
if (module.getMetadata ?? Page.metadata) {
|
|
@@ -4,13 +4,14 @@ import { createMarkupNodeLike } from '@ecopages/jsx';
|
|
|
4
4
|
*
|
|
5
5
|
* @remarks
|
|
6
6
|
* Radiant's server bridge and light-DOM shim are loaded lazily because most JSX
|
|
7
|
-
* renders do not need them.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* renders do not need them. Resolved runtime modules are cached on the policy
|
|
8
|
+
* instance so repeated renders on the same renderer reuse one load without
|
|
9
|
+
* process-wide mutable class state. Light-DOM constructor installation remains
|
|
10
|
+
* process-global and idempotent via {@link ensureRadiantLightDomGlobals}.
|
|
10
11
|
*/
|
|
11
12
|
export declare class EcopagesJsxRadiantSsrPolicy {
|
|
12
|
-
private
|
|
13
|
-
private
|
|
13
|
+
private runtimeModules;
|
|
14
|
+
private runtimeModulesPromise;
|
|
14
15
|
private readonly enabled;
|
|
15
16
|
constructor(enabled: boolean);
|
|
16
17
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createMarkupNodeLike } from "@ecopages/jsx";
|
|
2
2
|
import { createServerHydrationBindingState, withServerHydrationBindingState } from "@ecopages/jsx/server";
|
|
3
3
|
class EcopagesJsxRadiantSsrPolicy {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
runtimeModules;
|
|
5
|
+
runtimeModulesPromise;
|
|
6
6
|
enabled;
|
|
7
7
|
constructor(enabled) {
|
|
8
8
|
this.enabled = enabled;
|
|
@@ -23,7 +23,7 @@ class EcopagesJsxRadiantSsrPolicy {
|
|
|
23
23
|
if (!this.enabled) {
|
|
24
24
|
return render();
|
|
25
25
|
}
|
|
26
|
-
const runtimeModules = await
|
|
26
|
+
const runtimeModules = await this.runtimeModulesPromise;
|
|
27
27
|
if (!runtimeModules) {
|
|
28
28
|
return render();
|
|
29
29
|
}
|
|
@@ -37,7 +37,7 @@ class EcopagesJsxRadiantSsrPolicy {
|
|
|
37
37
|
* generated host HTML without escaping it back into plain text.
|
|
38
38
|
*/
|
|
39
39
|
renderIntrinsicElementMarkup(instance) {
|
|
40
|
-
const renderBridge =
|
|
40
|
+
const renderBridge = this.runtimeModules?.resolveRadiantElementRenderBridge(instance);
|
|
41
41
|
if (!renderBridge) {
|
|
42
42
|
return void 0;
|
|
43
43
|
}
|
|
@@ -51,28 +51,54 @@ class EcopagesJsxRadiantSsrPolicy {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
async ensureRuntimeInstalled() {
|
|
54
|
-
if (!
|
|
54
|
+
if (!this.runtimeModulesPromise) {
|
|
55
55
|
const radiantLightDomShimEntry = import.meta.resolve("@ecopages/radiant/server/light-dom-shim");
|
|
56
56
|
const radiantElementSsrRuntimeModuleUrl = new URL(
|
|
57
57
|
"./radiant-element-ssr-bridge.js",
|
|
58
58
|
radiantLightDomShimEntry
|
|
59
59
|
).href;
|
|
60
|
-
|
|
61
|
-
import(
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
this.runtimeModulesPromise = (async () => {
|
|
61
|
+
const lightDomShimModule2 = await import(radiantLightDomShimEntry);
|
|
62
|
+
ensureRadiantLightDomGlobals(lightDomShimModule2.installLightDomShim);
|
|
63
|
+
const radiantElementSsrRuntimeModule = await import(radiantElementSsrRuntimeModuleUrl);
|
|
64
64
|
const modules = {
|
|
65
65
|
installLightDomShim: lightDomShimModule2.installLightDomShim,
|
|
66
66
|
resolveRadiantElementRenderBridge: radiantElementSsrRuntimeModule.resolveRadiantElementRenderBridge,
|
|
67
67
|
withServerRadiantElementSsrRuntime: radiantElementSsrRuntimeModule.withServerRadiantElementSsrRuntime
|
|
68
68
|
};
|
|
69
|
-
|
|
69
|
+
this.runtimeModules = modules;
|
|
70
70
|
return modules;
|
|
71
|
-
});
|
|
71
|
+
})();
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
lightDomShimModule.
|
|
73
|
+
await this.runtimeModulesPromise;
|
|
74
|
+
const lightDomShimModule = this.runtimeModules;
|
|
75
|
+
if (lightDomShimModule) {
|
|
76
|
+
ensureRadiantLightDomGlobals(lightDomShimModule.installLightDomShim);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function ensureRadiantLightDomGlobals(installLightDomShim) {
|
|
81
|
+
if (typeof globalThis.HTMLElement !== "undefined") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const window = installLightDomShim();
|
|
85
|
+
if (!window) {
|
|
86
|
+
return;
|
|
75
87
|
}
|
|
88
|
+
Object.assign(globalThis, {
|
|
89
|
+
CSS: window.CSS,
|
|
90
|
+
CustomEvent: window.CustomEvent,
|
|
91
|
+
Document: window.Document,
|
|
92
|
+
Element: window.Element,
|
|
93
|
+
Event: window.Event,
|
|
94
|
+
EventTarget: window.EventTarget,
|
|
95
|
+
HTMLScriptElement: window.HTMLScriptElement,
|
|
96
|
+
HTMLElement: window.HTMLElement,
|
|
97
|
+
Node: window.Node,
|
|
98
|
+
document: window.document,
|
|
99
|
+
customElements: window.customElements,
|
|
100
|
+
window
|
|
101
|
+
});
|
|
76
102
|
}
|
|
77
103
|
export {
|
|
78
104
|
EcopagesJsxRadiantSsrPolicy
|
|
@@ -24,13 +24,6 @@ export declare class EcopagesJsxRenderSession {
|
|
|
24
24
|
* JSX SSR scope bridge.
|
|
25
25
|
*/
|
|
26
26
|
withActiveScope<T>(render: () => T): T;
|
|
27
|
-
/**
|
|
28
|
-
* Runs work with the current page-level hydrate binding namespace attached to JSX SSR.
|
|
29
|
-
*
|
|
30
|
-
* This lets page, layout, and document renders participate in one client-owned
|
|
31
|
-
* hydration root even though Ecopages composes them through multiple sibling render calls.
|
|
32
|
-
*/
|
|
33
|
-
withHydrationBindingScope<T>(render: () => T): T;
|
|
34
27
|
beginCollectedAssetFrame(): ProcessedAsset[];
|
|
35
28
|
endCollectedAssetFrame(frame: ProcessedAsset[]): ProcessedAsset[];
|
|
36
29
|
recordCollectedAssets(collectedAssets: ProcessedAsset[]): ProcessedAsset[];
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import {
|
|
3
|
-
createServerHydrationBindingState,
|
|
4
|
-
getActiveSsrScopeValue,
|
|
5
|
-
withActiveSsrScopeValue,
|
|
6
|
-
withServerHydrationBindingState
|
|
7
|
-
} from "@ecopages/jsx/server";
|
|
2
|
+
import { getActiveSsrScopeValue, withActiveSsrScopeValue } from "@ecopages/jsx/server";
|
|
8
3
|
const ECOPAGES_JSX_SSR_RENDER_STATE_KEY = /* @__PURE__ */ Symbol.for("@ecopages/ecopages-jsx.ssr-render-state");
|
|
9
4
|
const renderStateStorage = new AsyncLocalStorage();
|
|
10
5
|
class EcopagesJsxRenderSession {
|
|
@@ -32,23 +27,13 @@ class EcopagesJsxRenderSession {
|
|
|
32
27
|
return renderStateStorage.run(jsxScopeState, () => render());
|
|
33
28
|
}
|
|
34
29
|
const state = {
|
|
35
|
-
collectedAssetFrames: []
|
|
36
|
-
hydrationBindingState: createServerHydrationBindingState()
|
|
30
|
+
collectedAssetFrames: []
|
|
37
31
|
};
|
|
38
32
|
return renderStateStorage.run(
|
|
39
33
|
state,
|
|
40
34
|
() => withActiveSsrScopeValue(ECOPAGES_JSX_SSR_RENDER_STATE_KEY, state, render)
|
|
41
35
|
);
|
|
42
36
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Runs work with the current page-level hydrate binding namespace attached to JSX SSR.
|
|
45
|
-
*
|
|
46
|
-
* This lets page, layout, and document renders participate in one client-owned
|
|
47
|
-
* hydration root even though Ecopages composes them through multiple sibling render calls.
|
|
48
|
-
*/
|
|
49
|
-
withHydrationBindingScope(render) {
|
|
50
|
-
return withServerHydrationBindingState(this.getState().hydrationBindingState, render);
|
|
51
|
-
}
|
|
52
37
|
beginCollectedAssetFrame() {
|
|
53
38
|
const state = this.getState();
|
|
54
39
|
const frame = [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
|
|
2
|
-
import { IntegrationRenderer, type RenderToResponseContext, type RouteModuleLoadOptions } from '@ecopages/core/route-renderer/integration-renderer';
|
|
3
|
-
import type { ForeignChildRuntime } from '@ecopages/core/route-renderer/component-render-context';
|
|
2
|
+
import { IntegrationRenderer, type RenderToResponseContext, type RouteModuleLoadOptions } from '@ecopages/core/route-renderer/orchestration/integration-renderer';
|
|
3
|
+
import type { ForeignChildRuntime } from '@ecopages/core/route-renderer/orchestration/foreign-child/component-render-context';
|
|
4
4
|
import { type JsxRenderable } from '@ecopages/jsx';
|
|
5
5
|
import { type EcopagesJsxMdxPageModule } from './ecopages-jsx-mdx.js';
|
|
6
6
|
import type { EcopagesJsxRendererOptions } from './ecopages-jsx.types.js';
|
|
@@ -16,19 +16,26 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
16
16
|
private readonly mdxExtensions;
|
|
17
17
|
private readonly renderSession;
|
|
18
18
|
private readonly radiantSsrPolicy;
|
|
19
|
+
/**
|
|
20
|
+
* Serializes foreign-child props for string-first boundaries.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* DOM nodes become `outerHTML`. JSX trees, arrays, and template results are
|
|
24
|
+
* stringified here. Other values are left for core foreign-child interception,
|
|
25
|
+
* which rejects opaque plain objects before queueing. Cross-integration
|
|
26
|
+
* children must use `EcoEmbed`.
|
|
27
|
+
*/
|
|
19
28
|
private normalizeForeignChildProps;
|
|
29
|
+
/**
|
|
30
|
+
* Returns whether children are a JSX/template shape this renderer can
|
|
31
|
+
* serialize before foreign-child queueing.
|
|
32
|
+
*/
|
|
33
|
+
private isJsxSerializableForeignChild;
|
|
20
34
|
/**
|
|
21
35
|
* Re-renders queued JSX children inside the owning renderer so nested custom
|
|
22
36
|
* elements and queued foreign subtrees contribute assets to the same frame.
|
|
23
37
|
*/
|
|
24
38
|
private renderQueuedForeignSubtreeChildren;
|
|
25
|
-
/**
|
|
26
|
-
* Resolves queued foreign subtrees after JSX has been stringified.
|
|
27
|
-
*
|
|
28
|
-
* JSX content needs one extra render pass because child foreign subtrees may emit
|
|
29
|
-
* additional browser assets while also replacing placeholder tokens.
|
|
30
|
-
*/
|
|
31
|
-
private resolveOwnedForeignSubtreeHtml;
|
|
32
39
|
protected createForeignChildRuntime(options: {
|
|
33
40
|
renderInput: ComponentRenderInput;
|
|
34
41
|
rendererCache: Map<string, IntegrationRenderer<any>>;
|
|
@@ -44,4 +51,10 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
44
51
|
private withCustomElementRenderHook;
|
|
45
52
|
private withPreparedRadiantRuntime;
|
|
46
53
|
private createIntrinsicCustomElementRenderHook;
|
|
54
|
+
/**
|
|
55
|
+
* Records the source files that produced the current render so
|
|
56
|
+
* {@link EcopagesJsxHmrStrategy} can match later watcher events without
|
|
57
|
+
* re-walking the component tree.
|
|
58
|
+
*/
|
|
59
|
+
private recordHmrOwnership;
|
|
47
60
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
IntegrationRenderer
|
|
3
|
-
} from "@ecopages/core/route-renderer/integration-renderer";
|
|
3
|
+
} from "@ecopages/core/route-renderer/orchestration/integration-renderer";
|
|
4
|
+
import {
|
|
5
|
+
getForeignSubtreeResolutionContextKey,
|
|
6
|
+
resolveOwningIntegrationRenderer
|
|
7
|
+
} from "@ecopages/core/route-renderer/orchestration/foreign-child/owning-renderer-resolution";
|
|
4
8
|
import { createMarkupNodeLike } from "@ecopages/jsx";
|
|
5
9
|
import { renderToString, withServerCustomElementRenderHook } from "@ecopages/jsx/server";
|
|
6
10
|
import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
|
|
@@ -10,11 +14,21 @@ import {
|
|
|
10
14
|
} from "./ecopages-jsx-mdx.js";
|
|
11
15
|
import { EcopagesJsxRenderSession } from "./ecopages-jsx-render-session.js";
|
|
12
16
|
import { EcopagesJsxRadiantSsrPolicy } from "./ecopages-jsx-radiant-ssr-policy.js";
|
|
17
|
+
import { updateEjsxHmrOwnership } from "./ecopages-jsx-hmr-ownership.js";
|
|
13
18
|
class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
14
19
|
name = ECOPAGES_JSX_PLUGIN_NAME;
|
|
15
20
|
mdxExtensions;
|
|
16
21
|
renderSession;
|
|
17
22
|
radiantSsrPolicy;
|
|
23
|
+
/**
|
|
24
|
+
* Serializes foreign-child props for string-first boundaries.
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* DOM nodes become `outerHTML`. JSX trees, arrays, and template results are
|
|
28
|
+
* stringified here. Other values are left for core foreign-child interception,
|
|
29
|
+
* which rejects opaque plain objects before queueing. Cross-integration
|
|
30
|
+
* children must use `EcoEmbed`.
|
|
31
|
+
*/
|
|
18
32
|
normalizeForeignChildProps(props) {
|
|
19
33
|
if (!("children" in props)) {
|
|
20
34
|
return props;
|
|
@@ -29,10 +43,29 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
29
43
|
children: children.outerHTML
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
if (this.isJsxSerializableForeignChild(children)) {
|
|
47
|
+
return {
|
|
48
|
+
...props,
|
|
49
|
+
children: renderToString(children)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return props;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns whether children are a JSX/template shape this renderer can
|
|
56
|
+
* serialize before foreign-child queueing.
|
|
57
|
+
*/
|
|
58
|
+
isJsxSerializableForeignChild(children) {
|
|
59
|
+
if (Array.isArray(children)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (children === null || typeof children !== "object") {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if ("$$typeof" in children) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return "strings" in children && Array.isArray(children.strings);
|
|
36
69
|
}
|
|
37
70
|
/**
|
|
38
71
|
* Re-renders queued JSX children inside the owning renderer so nested custom
|
|
@@ -61,24 +94,6 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
61
94
|
html
|
|
62
95
|
};
|
|
63
96
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Resolves queued foreign subtrees after JSX has been stringified.
|
|
66
|
-
*
|
|
67
|
-
* JSX content needs one extra render pass because child foreign subtrees may emit
|
|
68
|
-
* additional browser assets while also replacing placeholder tokens.
|
|
69
|
-
*/
|
|
70
|
-
async resolveOwnedForeignSubtreeHtml(html, runtimeContext) {
|
|
71
|
-
return this.foreignSubtreeExecutionService.resolveQueuedHtml({
|
|
72
|
-
currentIntegrationName: this.name,
|
|
73
|
-
html,
|
|
74
|
-
runtimeContext,
|
|
75
|
-
queueLabel: "Ecopages JSX",
|
|
76
|
-
getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
|
|
77
|
-
applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
|
|
78
|
-
dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
|
|
79
|
-
renderQueuedChildren: async (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken)
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
97
|
createForeignChildRuntime(options) {
|
|
83
98
|
const runtime = super.createForeignChildRuntime(options);
|
|
84
99
|
const interceptForeignChild = runtime.interceptForeignChild;
|
|
@@ -125,7 +140,7 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
125
140
|
return await this.withPreparedRadiantRuntime(
|
|
126
141
|
() => this.renderSession.withActiveScope(async () => {
|
|
127
142
|
try {
|
|
128
|
-
|
|
143
|
+
const result = await this.renderPageWithDocumentShell({
|
|
129
144
|
page: {
|
|
130
145
|
component: options.Page,
|
|
131
146
|
props: {
|
|
@@ -144,6 +159,8 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
144
159
|
metadata: options.metadata,
|
|
145
160
|
pageProps: options.pageProps ?? {}
|
|
146
161
|
});
|
|
162
|
+
this.recordHmrOwnership([options.Page, options.Layout, options.HtmlTemplate]);
|
|
163
|
+
return result;
|
|
147
164
|
} catch (error) {
|
|
148
165
|
throw this.createRenderError("Error rendering page", error);
|
|
149
166
|
}
|
|
@@ -165,18 +182,33 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
165
182
|
};
|
|
166
183
|
const content = await this.withCustomElementRenderHook(() => component(componentProps));
|
|
167
184
|
const rendered = await this.renderJsx(content);
|
|
168
|
-
const queuedForeignSubtreeResolution = await this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
const queuedForeignSubtreeResolution = await this.foreignSubtreeExecutionService.resolveQueuedHtml({
|
|
186
|
+
currentIntegrationName: this.name,
|
|
187
|
+
html: rendered.html,
|
|
188
|
+
runtimeContext: this.foreignSubtreeExecutionService.getQueuedRuntimeContext(
|
|
189
|
+
input,
|
|
190
|
+
getForeignSubtreeResolutionContextKey(this.name)
|
|
191
|
+
),
|
|
192
|
+
queueLabel: "Ecopages JSX",
|
|
193
|
+
getOwningRenderer: (integrationName, rendererCache) => resolveOwningIntegrationRenderer({
|
|
194
|
+
appConfig: this.appConfig,
|
|
195
|
+
runtimeOrigin: this.runtimeOrigin,
|
|
196
|
+
currentIntegrationName: this.name,
|
|
197
|
+
currentRenderer: this,
|
|
198
|
+
integrationName,
|
|
199
|
+
cache: rendererCache
|
|
200
|
+
}),
|
|
201
|
+
applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
|
|
202
|
+
dedupeProcessedAssets: (assets2) => this.htmlTransformer.dedupeProcessedAssets(assets2),
|
|
203
|
+
renderQueuedChildren: (children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken)
|
|
204
|
+
});
|
|
174
205
|
const componentAssets = input.component.config?.dependencies && typeof this.assetProcessingService?.processDependencies === "function" ? await this.processComponentDependencies([input.component]) : [];
|
|
175
206
|
const assets = this.htmlTransformer.dedupeProcessedAssets([
|
|
176
207
|
...this.renderSession.endCollectedAssetFrame(assetFrame),
|
|
177
208
|
...queuedForeignSubtreeResolution.assets,
|
|
178
209
|
...componentAssets
|
|
179
210
|
]);
|
|
211
|
+
this.recordHmrOwnership([input.component]);
|
|
180
212
|
return {
|
|
181
213
|
html: queuedForeignSubtreeResolution.html,
|
|
182
214
|
canAttachAttributes: true,
|
|
@@ -199,12 +231,15 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
199
231
|
throw new TypeError("JSX renderer expected a callable view component.");
|
|
200
232
|
}
|
|
201
233
|
const viewComponent = view;
|
|
202
|
-
|
|
234
|
+
const layouts = viewComponent.config?.layouts;
|
|
235
|
+
const response = await this.renderViewWithDocumentShell({
|
|
203
236
|
view: viewComponent,
|
|
204
237
|
props,
|
|
205
238
|
ctx,
|
|
206
|
-
layout:
|
|
239
|
+
layout: layouts?.[layouts.length - 1]
|
|
207
240
|
});
|
|
241
|
+
this.recordHmrOwnership([view]);
|
|
242
|
+
return response;
|
|
208
243
|
} catch (error) {
|
|
209
244
|
throw this.createRenderError("Error rendering view", error);
|
|
210
245
|
}
|
|
@@ -234,6 +269,14 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
234
269
|
return instance ? this.radiantSsrPolicy.renderIntrinsicElementMarkup(instance) : void 0;
|
|
235
270
|
};
|
|
236
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Records the source files that produced the current render so
|
|
274
|
+
* {@link EcopagesJsxHmrStrategy} can match later watcher events without
|
|
275
|
+
* re-walking the component tree.
|
|
276
|
+
*/
|
|
277
|
+
recordHmrOwnership(components) {
|
|
278
|
+
updateEjsxHmrOwnership(components);
|
|
279
|
+
}
|
|
237
280
|
}
|
|
238
281
|
export {
|
|
239
282
|
EcopagesJsxRenderer
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
2
|
+
import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
|
|
3
|
+
import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
|
|
4
|
+
import type { IHmrManager } from '@ecopages/core';
|
|
2
5
|
import type { JsxRenderable } from '@ecopages/jsx';
|
|
3
6
|
import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
|
|
4
7
|
import type { EcopagesJsxPluginOptions } from './ecopages-jsx.types.js';
|
|
@@ -21,6 +24,20 @@ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable>
|
|
|
21
24
|
private getDependencies;
|
|
22
25
|
/** Ensures MDX build hooks are ready before Ecopages collects contributions. */
|
|
23
26
|
prepareBuildContributions(): Promise<void>;
|
|
27
|
+
/** Assigns grouped-build metadata for page-owned JSX content scripts. */
|
|
28
|
+
prepareAssetDependencies(dependencies: AssetDefinition[]): AssetDefinition[];
|
|
29
|
+
/** Registers JSX HMR strategy and Radiant SSR invalidation hooks. */
|
|
30
|
+
setHmrManager(hmrManager: IHmrManager): void;
|
|
31
|
+
/**
|
|
32
|
+
* Returns the JSX-integration HMR strategy.
|
|
33
|
+
*
|
|
34
|
+
* @remarks
|
|
35
|
+
* The strategy handles `layout-update` broadcasts for page sources, layout
|
|
36
|
+
* sources, and any component file in the active render tree. It defers
|
|
37
|
+
* registered script entrypoints to the generic JS strategy and includes /
|
|
38
|
+
* explicit server views to `ServerRenderedTemplateHmrStrategy`.
|
|
39
|
+
*/
|
|
40
|
+
getHmrStrategy(): HmrStrategy | undefined;
|
|
24
41
|
/**
|
|
25
42
|
* Registers MDX tooling and completes the base integration setup.
|
|
26
43
|
*/
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
} from "@ecopages/core/plugins/integration-plugin";
|
|
4
4
|
import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
|
|
5
5
|
import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
|
|
6
|
+
import { RADIANT_INSTALL_HYDRATOR_FILEPATH } from "./resolve-radiant-install-hydrator.js";
|
|
6
7
|
import {
|
|
7
8
|
appendMdxExtensions,
|
|
8
9
|
createMdxLoaderPlugin,
|
|
@@ -10,6 +11,9 @@ import {
|
|
|
10
11
|
resolveMdxCompilerOptions
|
|
11
12
|
} from "./ecopages-jsx-mdx.js";
|
|
12
13
|
import { EcopagesJsxRenderer } from "./ecopages-jsx-renderer.js";
|
|
14
|
+
import { EcopagesJsxHmrStrategy } from "./ecopages-jsx-hmr-strategy.js";
|
|
15
|
+
import { assignPageOwnedContentScriptGroupedBundles } from "./page-owned-content-script-grouping.js";
|
|
16
|
+
import { invalidateRadiantRegisteredScriptSsrRegistration } from "./radiant-registered-script-ssr-invalidation.js";
|
|
13
17
|
const RADIANT_HYDRATOR_SCRIPT_ID = "ecopages-jsx-radiant-hydrator";
|
|
14
18
|
const resolvePluginOptions = (options) => {
|
|
15
19
|
const { extensions: userExtensions, radiant, mdx, ...baseConfig } = options ?? {};
|
|
@@ -72,7 +76,7 @@ class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
72
76
|
return [
|
|
73
77
|
AssetFactory.createNodeModuleScript({
|
|
74
78
|
position: "head",
|
|
75
|
-
importPath:
|
|
79
|
+
importPath: RADIANT_INSTALL_HYDRATOR_FILEPATH,
|
|
76
80
|
bundle: false,
|
|
77
81
|
attributes: {
|
|
78
82
|
"data-eco-script-id": RADIANT_HYDRATOR_SCRIPT_ID
|
|
@@ -84,6 +88,45 @@ class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
84
88
|
async prepareBuildContributions() {
|
|
85
89
|
this.ensureMdxLoaderPlugin();
|
|
86
90
|
}
|
|
91
|
+
/** Assigns grouped-build metadata for page-owned JSX content scripts. */
|
|
92
|
+
prepareAssetDependencies(dependencies) {
|
|
93
|
+
return assignPageOwnedContentScriptGroupedBundles(dependencies);
|
|
94
|
+
}
|
|
95
|
+
/** Registers JSX HMR strategy and Radiant SSR invalidation hooks. */
|
|
96
|
+
setHmrManager(hmrManager) {
|
|
97
|
+
super.setHmrManager(hmrManager);
|
|
98
|
+
if (!this.includeRadiant || !this.appConfig) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const runtime = this.appConfig.runtime ?? {};
|
|
102
|
+
this.appConfig.runtime = runtime;
|
|
103
|
+
runtime.registeredScriptEntrypointChangeHandlers ??= [];
|
|
104
|
+
runtime.registeredScriptEntrypointChangeHandlers.push(invalidateRadiantRegisteredScriptSsrRegistration);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Returns the JSX-integration HMR strategy.
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* The strategy handles `layout-update` broadcasts for page sources, layout
|
|
111
|
+
* sources, and any component file in the active render tree. It defers
|
|
112
|
+
* registered script entrypoints to the generic JS strategy and includes /
|
|
113
|
+
* explicit server views to `ServerRenderedTemplateHmrStrategy`.
|
|
114
|
+
*/
|
|
115
|
+
getHmrStrategy() {
|
|
116
|
+
if (!this.hmrManager || !this.appConfig) {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
const context = this.hmrManager.getDefaultContext();
|
|
120
|
+
const absolutePaths = this.appConfig.absolutePaths;
|
|
121
|
+
return new EcopagesJsxHmrStrategy({
|
|
122
|
+
getWatchedFiles: context.getWatchedFiles,
|
|
123
|
+
getSrcDir: context.getSrcDir,
|
|
124
|
+
getPagesDir: context.getPagesDir,
|
|
125
|
+
getLayoutsDir: context.getLayoutsDir,
|
|
126
|
+
getIncludesDir: () => absolutePaths.includesDir,
|
|
127
|
+
getTemplateExtensions: () => this.extensions
|
|
128
|
+
});
|
|
129
|
+
}
|
|
87
130
|
/**
|
|
88
131
|
* Registers MDX tooling and completes the base integration setup.
|
|
89
132
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
|
|
2
|
+
/** Returns the stable grouped-build id for one integration's page-owned content scripts. */
|
|
3
|
+
export declare function createPageOwnedContentScriptBundleId(integrationName: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Tags page-owned content scripts in one dependency batch with a shared grouped-build id.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Lazy hydration entries (`excludeFromHtml`), component file scripts, and integration runtime
|
|
9
|
+
* assets stay ungrouped. Integrations that already assign a grouped id (for example React
|
|
10
|
+
* Router pages) are left unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export declare function assignPageOwnedContentScriptGroupedBundles(dependencies: AssetDefinition[]): AssetDefinition[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { rapidhash } from "@ecopages/core/utils/hash";
|
|
2
|
+
import { ECOPAGES_JSX_PLUGIN_NAME } from "./ecopages-jsx.constants.js";
|
|
3
|
+
const PAGE_OWNED_CONTENT_SCRIPT_BUNDLE_PREFIX = "ecopages";
|
|
4
|
+
function createPageOwnedContentScriptBundleId(integrationName) {
|
|
5
|
+
return `${PAGE_OWNED_CONTENT_SCRIPT_BUNDLE_PREFIX}-${integrationName}-page-content-scripts`;
|
|
6
|
+
}
|
|
7
|
+
function isPageOwnedGroupableContentScript(dep) {
|
|
8
|
+
if (dep.kind !== "script" || dep.source !== "content" || dep.inline) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (dep.excludeFromHtml) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
if (dep.groupedBundle?.id) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (dep.packageRole === "keep-separate" || dep.packageRole === "runtime" || dep.packageRole === "dynamic-chunk") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
function createPageOwnedGroupedEntryName(dep, usedEntryNames) {
|
|
23
|
+
const baseName = dep.name ?? `script-${rapidhash(dep.content).toString(16)}`;
|
|
24
|
+
if (!usedEntryNames.has(baseName)) {
|
|
25
|
+
usedEntryNames.add(baseName);
|
|
26
|
+
return baseName;
|
|
27
|
+
}
|
|
28
|
+
let suffix = 2;
|
|
29
|
+
let candidate = `${baseName}-${suffix}`;
|
|
30
|
+
while (usedEntryNames.has(candidate)) {
|
|
31
|
+
suffix += 1;
|
|
32
|
+
candidate = `${baseName}-${suffix}`;
|
|
33
|
+
}
|
|
34
|
+
usedEntryNames.add(candidate);
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
function assignPageOwnedContentScriptGroupedBundles(dependencies) {
|
|
38
|
+
const bundleId = createPageOwnedContentScriptBundleId(ECOPAGES_JSX_PLUGIN_NAME);
|
|
39
|
+
const usedEntryNames = /* @__PURE__ */ new Set();
|
|
40
|
+
for (const dependency of dependencies) {
|
|
41
|
+
if (!isPageOwnedGroupableContentScript(dependency)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
dependency.groupedBundle = {
|
|
45
|
+
id: bundleId,
|
|
46
|
+
entryName: createPageOwnedGroupedEntryName(dependency, usedEntryNames)
|
|
47
|
+
};
|
|
48
|
+
if (dependency.bundle === false) {
|
|
49
|
+
dependency.bundle = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return dependencies;
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
assignPageOwnedContentScriptGroupedBundles,
|
|
56
|
+
createPageOwnedContentScriptBundleId
|
|
57
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the custom-element tag name from a registered script module source file.
|
|
3
|
+
*/
|
|
4
|
+
export declare function resolveRadiantCustomElementTag(scriptPath: string): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Removes one custom-element definition from the active runtime registry.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Radiant's `@customElement` decorator and the SSR light-DOM shim both skip
|
|
10
|
+
* re-registration when a tag is already defined. During dev, server requests
|
|
11
|
+
* reuse the same process-global registry, so script edits would otherwise keep
|
|
12
|
+
* serving stale SSR markup until a full dev-server restart.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clearRadiantCustomElementDefinition(tagName: string): void;
|
|
15
|
+
/**
|
|
16
|
+
* Clears the SSR custom-element registry entry for one registered Radiant script module.
|
|
17
|
+
*/
|
|
18
|
+
export declare function invalidateRadiantRegisteredScriptSsrRegistration(scriptPath: string): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
const CUSTOM_ELEMENT_TAG_PATTERN = /@customElement\s*\(\s*['"`]([^'"`]+)['"`]/u;
|
|
3
|
+
function resolveRadiantCustomElementTag(scriptPath) {
|
|
4
|
+
if (!existsSync(scriptPath)) {
|
|
5
|
+
return void 0;
|
|
6
|
+
}
|
|
7
|
+
const content = readFileSync(scriptPath, "utf8");
|
|
8
|
+
const match = content.match(CUSTOM_ELEMENT_TAG_PATTERN);
|
|
9
|
+
return match?.[1];
|
|
10
|
+
}
|
|
11
|
+
function clearRadiantCustomElementDefinition(tagName) {
|
|
12
|
+
const registry = globalThis.customElements;
|
|
13
|
+
if (!registry) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (typeof registry.delete === "function") {
|
|
17
|
+
registry.delete(tagName);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const definitions = registry.definitions;
|
|
21
|
+
if (definitions instanceof Map && definitions.has(tagName)) {
|
|
22
|
+
definitions.delete(tagName);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function invalidateRadiantRegisteredScriptSsrRegistration(scriptPath) {
|
|
26
|
+
const tagName = resolveRadiantCustomElementTag(scriptPath);
|
|
27
|
+
if (!tagName) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
clearRadiantCustomElementDefinition(tagName);
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
clearRadiantCustomElementDefinition,
|
|
34
|
+
invalidateRadiantRegisteredScriptSsrRegistration,
|
|
35
|
+
resolveRadiantCustomElementTag
|
|
36
|
+
};
|