@ecopages/ecopages-jsx 0.2.0-beta.3 → 0.2.0-beta.30

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 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",
3
+ "version": "0.2.0-beta.30",
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.30",
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.3",
39
- "@ecopages/jsx": "0.3.0-alpha.25",
40
- "@ecopages/radiant": "0.3.0-alpha.25"
39
+ "@ecopages/core": "0.2.0-beta.30",
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 declare const createMdxExtensionFilter: (extensions: string[], options?: {
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>;
@@ -1,62 +1,28 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import path from "node:path";
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 escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7
- const mergePluginLists = (...lists) => {
8
- const merged = lists.flatMap((list) => list ? [...list] : []);
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
- ...compilerOptions,
29
- jsxImportSource: "@ecopages/jsx",
30
- jsxRuntime: "automatic",
31
- development: process.env.NODE_ENV === "development"
32
- };
33
- const mergedRemark = mergePluginLists(compilerOptions?.remarkPlugins, remarkPlugins);
34
- const mergedRehype = mergePluginLists(compilerOptions?.rehypePlugins, rehypePlugins);
35
- const mergedRecma = mergePluginLists(compilerOptions?.recmaPlugins, recmaPlugins);
36
- if (mergedRemark) resolved.remarkPlugins = mergedRemark;
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. Once enabled, the resolved runtime modules are
8
- * cached statically so repeated page and component renders do not keep paying
9
- * module-resolution or shim-installation costs.
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 static runtimeModules;
13
- private static runtimeModulesPromise;
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
- static runtimeModules;
5
- static runtimeModulesPromise;
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 EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise;
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 = EcopagesJsxRadiantSsrPolicy.runtimeModules?.resolveRadiantElementRenderBridge(instance);
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 (!EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise) {
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
- EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise = Promise.all([
61
- import(radiantElementSsrRuntimeModuleUrl),
62
- import(radiantLightDomShimEntry)
63
- ]).then(([radiantElementSsrRuntimeModule, lightDomShimModule2]) => {
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
- EcopagesJsxRadiantSsrPolicy.runtimeModules = modules;
69
+ this.runtimeModules = modules;
70
70
  return modules;
71
- });
71
+ })();
72
72
  }
73
- const lightDomShimModule = await EcopagesJsxRadiantSsrPolicy.runtimeModulesPromise;
74
- lightDomShimModule.installLightDomShim();
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
- return {
33
- ...props,
34
- children: renderToString(children)
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
- return await this.renderPageWithDocumentShell({
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.resolveOwnedForeignSubtreeHtml(
169
- rendered.html,
170
- this.getQueuedForeignSubtreeResolutionContext(
171
- input
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
- return await this.renderViewWithDocumentShell({
234
+ const layouts = viewComponent.config?.layouts;
235
+ const response = await this.renderViewWithDocumentShell({
203
236
  view: viewComponent,
204
237
  props,
205
238
  ctx,
206
- layout: viewComponent.config?.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: "@ecopages/radiant/client/install-hydrator",
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
+ };
@@ -0,0 +1,2 @@
1
+ /** Absolute path to `@ecopages/radiant/client/install-hydrator` from this integration package. */
2
+ export declare const RADIANT_INSTALL_HYDRATOR_FILEPATH: string;
@@ -0,0 +1,7 @@
1
+ import { fileURLToPath } from "node:url";
2
+ const RADIANT_INSTALL_HYDRATOR_FILEPATH = fileURLToPath(
3
+ import.meta.resolve("@ecopages/radiant/client/install-hydrator")
4
+ );
5
+ export {
6
+ RADIANT_INSTALL_HYDRATOR_FILEPATH
7
+ };