@ecopages/ecopages-jsx 0.2.0-beta.2 → 0.2.0-beta.21
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 +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.js +33 -7
- package/src/ecopages-jsx-renderer.d.ts +6 -7
- package/src/ecopages-jsx-renderer.js +22 -25
- package/src/ecopages-jsx.plugin.d.ts +14 -0
- package/src/ecopages-jsx.plugin.js +39 -1
- 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/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.21",
|
|
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.21",
|
|
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.21",
|
|
40
|
+
"@ecopages/jsx": "0.3.0-beta.0",
|
|
41
|
+
"@ecopages/radiant": "0.3.0-beta.0"
|
|
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) {
|
|
@@ -57,10 +57,10 @@ class EcopagesJsxRadiantSsrPolicy {
|
|
|
57
57
|
"./radiant-element-ssr-bridge.js",
|
|
58
58
|
radiantLightDomShimEntry
|
|
59
59
|
).href;
|
|
60
|
-
EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise =
|
|
61
|
-
import(
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
EcopagesJsxRadiantSsrPolicy.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,
|
|
@@ -68,11 +68,37 @@ class EcopagesJsxRadiantSsrPolicy {
|
|
|
68
68
|
};
|
|
69
69
|
EcopagesJsxRadiantSsrPolicy.runtimeModules = modules;
|
|
70
70
|
return modules;
|
|
71
|
-
});
|
|
71
|
+
})();
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
lightDomShimModule.
|
|
73
|
+
await EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise;
|
|
74
|
+
const lightDomShimModule = EcopagesJsxRadiantSsrPolicy.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
|
|
@@ -22,13 +22,6 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
22
22
|
* elements and queued foreign subtrees contribute assets to the same frame.
|
|
23
23
|
*/
|
|
24
24
|
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
25
|
protected createForeignChildRuntime(options: {
|
|
33
26
|
renderInput: ComponentRenderInput;
|
|
34
27
|
rendererCache: Map<string, IntegrationRenderer<any>>;
|
|
@@ -44,4 +37,10 @@ export declare class EcopagesJsxRenderer extends IntegrationRenderer<JsxRenderab
|
|
|
44
37
|
private withCustomElementRenderHook;
|
|
45
38
|
private withPreparedRadiantRuntime;
|
|
46
39
|
private createIntrinsicCustomElementRenderHook;
|
|
40
|
+
/**
|
|
41
|
+
* Records the source files that produced the current render so
|
|
42
|
+
* {@link EcopagesJsxHmrStrategy} can match later watcher events without
|
|
43
|
+
* re-walking the component tree.
|
|
44
|
+
*/
|
|
45
|
+
private recordHmrOwnership;
|
|
47
46
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "./ecopages-jsx-mdx.js";
|
|
11
11
|
import { EcopagesJsxRenderSession } from "./ecopages-jsx-render-session.js";
|
|
12
12
|
import { EcopagesJsxRadiantSsrPolicy } from "./ecopages-jsx-radiant-ssr-policy.js";
|
|
13
|
+
import { updateEjsxHmrOwnership } from "./ecopages-jsx-hmr-ownership.js";
|
|
13
14
|
class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
14
15
|
name = ECOPAGES_JSX_PLUGIN_NAME;
|
|
15
16
|
mdxExtensions;
|
|
@@ -61,24 +62,6 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
61
62
|
html
|
|
62
63
|
};
|
|
63
64
|
}
|
|
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
65
|
createForeignChildRuntime(options) {
|
|
83
66
|
const runtime = super.createForeignChildRuntime(options);
|
|
84
67
|
const interceptForeignChild = runtime.interceptForeignChild;
|
|
@@ -125,7 +108,7 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
125
108
|
return await this.withPreparedRadiantRuntime(
|
|
126
109
|
() => this.renderSession.withActiveScope(async () => {
|
|
127
110
|
try {
|
|
128
|
-
|
|
111
|
+
const result = await this.renderPageWithDocumentShell({
|
|
129
112
|
page: {
|
|
130
113
|
component: options.Page,
|
|
131
114
|
props: {
|
|
@@ -144,6 +127,8 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
144
127
|
metadata: options.metadata,
|
|
145
128
|
pageProps: options.pageProps ?? {}
|
|
146
129
|
});
|
|
130
|
+
this.recordHmrOwnership([options.Page, options.Layout, options.HtmlTemplate]);
|
|
131
|
+
return result;
|
|
147
132
|
} catch (error) {
|
|
148
133
|
throw this.createRenderError("Error rendering page", error);
|
|
149
134
|
}
|
|
@@ -165,11 +150,11 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
165
150
|
};
|
|
166
151
|
const content = await this.withCustomElementRenderHook(() => component(componentProps));
|
|
167
152
|
const rendered = await this.renderJsx(content);
|
|
168
|
-
const queuedForeignSubtreeResolution = await this.
|
|
153
|
+
const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(
|
|
169
154
|
rendered.html,
|
|
170
|
-
this.getQueuedForeignSubtreeResolutionContext(
|
|
171
|
-
|
|
172
|
-
|
|
155
|
+
this.getQueuedForeignSubtreeResolutionContext(input),
|
|
156
|
+
(children, _runtimeContext, queuedResolutionsByToken, resolveToken) => this.renderQueuedForeignSubtreeChildren(children, queuedResolutionsByToken, resolveToken),
|
|
157
|
+
"Ecopages JSX"
|
|
173
158
|
);
|
|
174
159
|
const componentAssets = input.component.config?.dependencies && typeof this.assetProcessingService?.processDependencies === "function" ? await this.processComponentDependencies([input.component]) : [];
|
|
175
160
|
const assets = this.htmlTransformer.dedupeProcessedAssets([
|
|
@@ -177,6 +162,7 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
177
162
|
...queuedForeignSubtreeResolution.assets,
|
|
178
163
|
...componentAssets
|
|
179
164
|
]);
|
|
165
|
+
this.recordHmrOwnership([input.component]);
|
|
180
166
|
return {
|
|
181
167
|
html: queuedForeignSubtreeResolution.html,
|
|
182
168
|
canAttachAttributes: true,
|
|
@@ -199,12 +185,15 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
199
185
|
throw new TypeError("JSX renderer expected a callable view component.");
|
|
200
186
|
}
|
|
201
187
|
const viewComponent = view;
|
|
202
|
-
|
|
188
|
+
const layouts = viewComponent.config?.layouts;
|
|
189
|
+
const response = await this.renderViewWithDocumentShell({
|
|
203
190
|
view: viewComponent,
|
|
204
191
|
props,
|
|
205
192
|
ctx,
|
|
206
|
-
layout:
|
|
193
|
+
layout: layouts?.[layouts.length - 1]
|
|
207
194
|
});
|
|
195
|
+
this.recordHmrOwnership([view]);
|
|
196
|
+
return response;
|
|
208
197
|
} catch (error) {
|
|
209
198
|
throw this.createRenderError("Error rendering view", error);
|
|
210
199
|
}
|
|
@@ -234,6 +223,14 @@ class EcopagesJsxRenderer extends IntegrationRenderer {
|
|
|
234
223
|
return instance ? this.radiantSsrPolicy.renderIntrinsicElementMarkup(instance) : void 0;
|
|
235
224
|
};
|
|
236
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Records the source files that produced the current render so
|
|
228
|
+
* {@link EcopagesJsxHmrStrategy} can match later watcher events without
|
|
229
|
+
* re-walking the component tree.
|
|
230
|
+
*/
|
|
231
|
+
recordHmrOwnership(components) {
|
|
232
|
+
updateEjsxHmrOwnership(components);
|
|
233
|
+
}
|
|
237
234
|
}
|
|
238
235
|
export {
|
|
239
236
|
EcopagesJsxRenderer
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
2
|
+
import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
|
|
3
|
+
import type { IHmrManager } from '@ecopages/core';
|
|
2
4
|
import type { JsxRenderable } from '@ecopages/jsx';
|
|
3
5
|
import { EcopagesJsxRenderer } from './ecopages-jsx-renderer.js';
|
|
4
6
|
import type { EcopagesJsxPluginOptions } from './ecopages-jsx.types.js';
|
|
@@ -21,6 +23,18 @@ export declare class EcopagesJsxPlugin extends IntegrationPlugin<JsxRenderable>
|
|
|
21
23
|
private getDependencies;
|
|
22
24
|
/** Ensures MDX build hooks are ready before Ecopages collects contributions. */
|
|
23
25
|
prepareBuildContributions(): Promise<void>;
|
|
26
|
+
/** Registers JSX HMR strategy and Radiant SSR invalidation hooks. */
|
|
27
|
+
setHmrManager(hmrManager: IHmrManager): void;
|
|
28
|
+
/**
|
|
29
|
+
* Returns the JSX-integration HMR strategy.
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* The strategy handles `layout-update` broadcasts for page sources, layout
|
|
33
|
+
* sources, and any component file in the active render tree. It defers
|
|
34
|
+
* registered script entrypoints to the generic JS strategy and includes /
|
|
35
|
+
* explicit server views to `ServerRenderedTemplateHmrStrategy`.
|
|
36
|
+
*/
|
|
37
|
+
getHmrStrategy(): HmrStrategy | undefined;
|
|
24
38
|
/**
|
|
25
39
|
* Registers MDX tooling and completes the base integration setup.
|
|
26
40
|
*/
|
|
@@ -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,8 @@ 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 { invalidateRadiantRegisteredScriptSsrRegistration } from "./radiant-registered-script-ssr-invalidation.js";
|
|
13
16
|
const RADIANT_HYDRATOR_SCRIPT_ID = "ecopages-jsx-radiant-hydrator";
|
|
14
17
|
const resolvePluginOptions = (options) => {
|
|
15
18
|
const { extensions: userExtensions, radiant, mdx, ...baseConfig } = options ?? {};
|
|
@@ -72,7 +75,7 @@ class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
72
75
|
return [
|
|
73
76
|
AssetFactory.createNodeModuleScript({
|
|
74
77
|
position: "head",
|
|
75
|
-
importPath:
|
|
78
|
+
importPath: RADIANT_INSTALL_HYDRATOR_FILEPATH,
|
|
76
79
|
bundle: false,
|
|
77
80
|
attributes: {
|
|
78
81
|
"data-eco-script-id": RADIANT_HYDRATOR_SCRIPT_ID
|
|
@@ -84,6 +87,41 @@ class EcopagesJsxPlugin extends IntegrationPlugin {
|
|
|
84
87
|
async prepareBuildContributions() {
|
|
85
88
|
this.ensureMdxLoaderPlugin();
|
|
86
89
|
}
|
|
90
|
+
/** Registers JSX HMR strategy and Radiant SSR invalidation hooks. */
|
|
91
|
+
setHmrManager(hmrManager) {
|
|
92
|
+
super.setHmrManager(hmrManager);
|
|
93
|
+
if (!this.includeRadiant || !this.appConfig) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const runtime = this.appConfig.runtime ?? {};
|
|
97
|
+
this.appConfig.runtime = runtime;
|
|
98
|
+
runtime.registeredScriptEntrypointChangeHandlers ??= [];
|
|
99
|
+
runtime.registeredScriptEntrypointChangeHandlers.push(invalidateRadiantRegisteredScriptSsrRegistration);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns the JSX-integration HMR strategy.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* The strategy handles `layout-update` broadcasts for page sources, layout
|
|
106
|
+
* sources, and any component file in the active render tree. It defers
|
|
107
|
+
* registered script entrypoints to the generic JS strategy and includes /
|
|
108
|
+
* explicit server views to `ServerRenderedTemplateHmrStrategy`.
|
|
109
|
+
*/
|
|
110
|
+
getHmrStrategy() {
|
|
111
|
+
if (!this.hmrManager || !this.appConfig) {
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
const context = this.hmrManager.getDefaultContext();
|
|
115
|
+
const absolutePaths = this.appConfig.absolutePaths;
|
|
116
|
+
return new EcopagesJsxHmrStrategy({
|
|
117
|
+
getWatchedFiles: context.getWatchedFiles,
|
|
118
|
+
getSrcDir: context.getSrcDir,
|
|
119
|
+
getPagesDir: context.getPagesDir,
|
|
120
|
+
getLayoutsDir: context.getLayoutsDir,
|
|
121
|
+
getIncludesDir: () => absolutePaths.includesDir,
|
|
122
|
+
getTemplateExtensions: () => this.extensions
|
|
123
|
+
});
|
|
124
|
+
}
|
|
87
125
|
/**
|
|
88
126
|
* Registers MDX tooling and completes the base integration setup.
|
|
89
127
|
*/
|
|
@@ -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
|
+
};
|