@ecopages/ecopages-jsx 0.2.1 → 0.3.1

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.
Files changed (31) hide show
  1. package/README.md +69 -23
  2. package/package.json +25 -6
  3. package/src/dev-transform/ecopages-jsx-dev-transform-contributor.d.ts +17 -0
  4. package/src/dev-transform/ecopages-jsx-dev-transform-contributor.js +21 -0
  5. package/src/eco-embed.d.ts +11 -0
  6. package/src/eco-embed.js +9 -0
  7. package/src/ecopages-jsx-hmr-ownership.d.ts +43 -0
  8. package/src/ecopages-jsx-hmr-ownership.js +91 -0
  9. package/src/ecopages-jsx-hmr-strategy.d.ts +19 -0
  10. package/src/ecopages-jsx-hmr-strategy.js +67 -0
  11. package/src/ecopages-jsx-mdx.d.ts +32 -0
  12. package/src/ecopages-jsx-mdx.js +48 -0
  13. package/src/ecopages-jsx-radiant-ssr-policy.d.ts +39 -0
  14. package/src/ecopages-jsx-radiant-ssr-policy.js +122 -0
  15. package/src/ecopages-jsx-renderer.d.ts +10 -52
  16. package/src/ecopages-jsx-renderer.js +143 -251
  17. package/src/ecopages-jsx.constants.d.ts +7 -0
  18. package/src/ecopages-jsx.constants.js +7 -0
  19. package/src/ecopages-jsx.plugin.d.ts +21 -93
  20. package/src/ecopages-jsx.plugin.js +135 -237
  21. package/src/ecopages-jsx.types.d.ts +32 -0
  22. package/src/ecopages-jsx.types.js +1 -0
  23. package/src/page-owned-content-script-grouping.d.ts +11 -0
  24. package/src/page-owned-content-script-grouping.js +57 -0
  25. package/src/radiant-registered-script-ssr-invalidation.d.ts +19 -0
  26. package/src/radiant-registered-script-ssr-invalidation.js +49 -0
  27. package/src/resolve-radiant-install-hydrator.d.ts +2 -0
  28. package/src/resolve-radiant-install-hydrator.js +3 -0
  29. package/CHANGELOG.md +0 -15
  30. package/src/services/jsx-runtime-bundle.service.d.ts +0 -51
  31. package/src/services/jsx-runtime-bundle.service.js +0 -206
package/README.md CHANGED
@@ -1,35 +1,30 @@
1
1
  # @ecopages/ecopages-jsx
2
2
 
3
- Integration plugin for [@ecopages/jsx](https://jsr.io/@ecopages/jsx) templates in Ecopages. Use it when Ecopages JSX should own `.tsx` routes, Radiant-backed web components, or MDX compiled against the `@ecopages/jsx` runtime.
3
+ Integration plugin for [@ecopages/jsx](https://www.npmjs.com/package/@ecopages/jsx) templates in Ecopages. Use it when Ecopages JSX should own `.tsx` routes, Radiant-backed web components, or MDX compiled against the `@ecopages/jsx` runtime.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- bunx jsr add @ecopages/ecopages-jsx @ecopages/radiant
8
+ bun add @ecopages/ecopages-jsx @ecopages/jsx @ecopages/radiant
9
9
  ```
10
10
 
11
- `@ecopages/radiant` is a required peer dependency for this package.
11
+ `@ecopages/jsx` and `@ecopages/radiant` are required peer dependencies for this package.
12
12
 
13
13
  ## Usage
14
14
 
15
15
  Register the `ecopagesJsxPlugin` in your `eco.config.ts`.
16
16
 
17
17
  ```ts
18
- import { ConfigBuilder } from '@ecopages/core/config-builder';
18
+ import { defineConfig } from '@ecopages/core/config';
19
19
  import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
20
20
 
21
- const config = await new ConfigBuilder()
22
- .setBaseUrl(import.meta.env.ECOPAGES_BASE_URL)
23
- .setIntegrations([ecopagesJsxPlugin()])
24
- .build();
25
-
26
- export default config;
21
+ export default defineConfig({ integrations: [ecopagesJsxPlugin()] });
27
22
  ```
28
23
 
29
24
  ## What This Integration Owns
30
25
 
31
26
  - `.tsx` route files by default. Use `extensions` to change the JSX route suffix list.
32
- - Optional Radiant runtime assets and import-map entries.
27
+ - Optional Radiant SSR/runtime wiring through the public `@ecopages/radiant` entrypoints.
33
28
  - Optional `.mdx` routes compiled against the `@ecopages/jsx` runtime.
34
29
 
35
30
  ## Route Extensions
@@ -42,9 +37,26 @@ ecopagesJsxPlugin({
42
37
  });
43
38
  ```
44
39
 
40
+ ## Development HMR
41
+
42
+ In development, editing a registered Ecopages JSX page refreshes the current page through the navigation runtime. The shared Layout stays mounted where the router can preserve it; elements marked with `data-eco-persist` retain their DOM state. Declared client scripts continue to use script HMR.
43
+
45
44
  ## Radiant Support
46
45
 
47
- Radiant runtime bundles are enabled by default so JSX pages can render and hydrate Radiant custom elements.
46
+ Radiant support is enabled by default. When `radiant: true`, the plugin keeps the ownership split explicit:
47
+
48
+ - Ecopages JSX owns page-level JSX SSR and container hydration.
49
+ - Radiant SSR is activated on the server through `@ecopages/radiant/server/render-component`.
50
+ - The server reuses Radiant's complete light-DOM surface, replacing partial globals before Radiant elements are imported.
51
+ - Radiant host hydration is activated on the client through an explicit head bootstrap that imports `@ecopages/radiant/client/install-hydrator` before intrinsic custom-element modules load.
52
+
53
+ That means server-rendered `RadiantElement` hosts hydrate in place only when both the SSR markers and the explicit client hydrator are present. Without the client hydrator, Radiant intentionally falls back to a fresh client render on first connect.
54
+
55
+ ### Hydration Semantics: RadiantElement vs. RadiantController
56
+
57
+ - **`RadiantElement` (DOM Preservation)**: Custom elements extending `RadiantElement` support true in-place hydration. When `@ecopages/radiant/client/install-hydrator` executes before elements connect, existing light-DOM SSR nodes are preserved, retaining DOM node identity, form input state, active focus, and media playback.
58
+ - **`RadiantController` (Activation / Template Re-mount)**: Controllers attached via `[data-controller]` activate when `startControllers(document)` is called. Activation executes the controller's `render()` method and mounts its template fresh into the host container, replacing server-rendered child nodes. Reactive properties and signal bindings remain fully interactive, but individual child DOM nodes are reconstructed.
59
+ - **Guidance**: Use `RadiantElement` when in-place preservation of SSR nodes (e.g., focused inputs, media elements, or CSS transitions) is required. Use `RadiantController` for Stimulus-style progressive enhancement on arbitrary markup where child DOM reconstruction is acceptable.
48
60
 
49
61
  ```ts
50
62
  ecopagesJsxPlugin({
@@ -52,30 +64,64 @@ ecopagesJsxPlugin({
52
64
  });
53
65
  ```
54
66
 
55
- Set `radiant: false` when your JSX pages do not need the Radiant browser runtime on a given app.
67
+ Set `radiant: false` when your JSX pages do not need Radiant SSR or the Radiant browser runtime on a given app.
68
+
69
+ The plugin bootstrap is intentionally explicit rather than depending on custom-element modules to install the Radiant hydrator opportunistically.
70
+
71
+ For server-side custom-element examples, prefer explicit SSR modes over the older boolean-only contract.
72
+
73
+ ```ts
74
+ new MyElement().renderHostToString({ mode: 'plain' });
75
+ new MyElement().renderHostToString({ mode: 'hydrate' });
76
+ ```
77
+
78
+ The legacy `hydrate: true` option remains compatible, but `mode` is the current SSR contract. Registered intrinsic tags that contain a dash are SSR candidates, and framework-specific host rendering should be adapted through the server custom-element render hook instead of hardcoding framework branches into JSX page code.
79
+
80
+ The integration loads Radiant host serialization through the public `@ecopages/radiant/server/radiant-element-ssr` export after installing the light-DOM shim. Applications do not need a separate Radiant SSR side-effect import.
81
+
82
+ ### Registered Radiant scripts (`ssr: true`)
83
+
84
+ Declare custom-element registration once in `dependencies.scripts`. Ecopages JSX preloads entries marked `ssr: true` on the server before render so hosts can SSR without a redundant `import './my-element.script.ts'` in the component file. Preload deduplication and invalidation state belongs to the app's `AppRuntime`, so separate apps keep separate preload history.
85
+
86
+ ```ts
87
+ export const ThemeToggle = eco.component({
88
+ dependencies: {
89
+ scripts: [{ src: './theme-toggle.script.ts', ssr: true, lazy: { 'on:idle': true } }],
90
+ },
91
+ render: () => <theme-toggle />,
92
+ });
93
+ ```
94
+
95
+ Keep `import type` when you only need props from the script module. Do not add `ssr: true` to browser-only scripts that touch `window` or `document` without guards.
56
96
 
57
97
  ## MDX Support
58
98
 
59
- Enable MDX to treat `.mdx` files as JSX routes compiled against the `@ecopages/jsx` runtime.
99
+ Add an `mdx` object to treat `.mdx` files as JSX routes compiled against the `@ecopages/jsx` runtime. Omit the property to disable MDX. Put `remarkPlugins`, `rehypePlugins`, and `recmaPlugins` next to `compilerOptions`, not inside it.
60
100
 
61
101
  ```ts
62
- import { ConfigBuilder } from '@ecopages/core/config-builder';
102
+ import { defineConfig } from '@ecopages/core/config';
63
103
  import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
64
104
 
65
- const config = await new ConfigBuilder()
66
- .setIntegrations([
105
+ export default defineConfig({
106
+ integrations: [
67
107
  ecopagesJsxPlugin({
68
108
  mdx: {
69
- enabled: true,
70
109
  extensions: ['.mdx', '.md'],
71
110
  },
72
111
  }),
73
- ])
74
- .build();
75
-
76
- export default config;
112
+ ],
113
+ });
77
114
  ```
78
115
 
79
116
  ## Mixed Rendering
80
117
 
81
- Ecopages JSX can own the outer page shell or just the nested boundary. When another integration reaches a JSX-owned boundary, Ecopages hands that boundary back to the JSX renderer so it can serialize the correct output before the outer renderer resumes.
118
+ Ecopages JSX can own the outer page shell or just a nested foreign subtree. When another integration reaches a JSX-owned foreign child, Ecopages hands that foreign subtree back to the JSX renderer so it can serialize the correct output before the outer renderer resumes.
119
+
120
+ Important:
121
+
122
+ - Cross-integration shell stacks should use `EcoEmbed` from `@ecopages/ecopages-jsx/eco-embed` (or the matching integration adapter). Plain opaque objects fail fast at core foreign-subtree queue boundaries; they are not coerced with `String(object)`. Pass already-serialized HTML when `EcoEmbed` is not needed.
123
+ - Components that may render foreign children must declare those children in `config.dependencies.components`.
124
+ - Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not treat rendered HTML alone as the source of truth.
125
+ - Route-level dependency resolvers can supply Components at request time; Ecopages JSX includes those resolved roots when deciding whether a Foreign Subtree needs handoff.
126
+ - Ecopages JSX keeps raw-markup preservation and asset collection inside the JSX renderer.
127
+ - JSX renderers use the required core `AssetProcessingService` to process declared component dependencies.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/ecopages-jsx",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "JSX integration plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -11,18 +11,37 @@
11
11
  "main": "./src/ecopages-jsx.plugin.js",
12
12
  "types": "./src/ecopages-jsx.plugin.d.ts",
13
13
  "type": "module",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "default": "./src/ecopages-jsx.plugin.js",
20
+ "types": "./src/ecopages-jsx.plugin.d.ts"
21
+ },
22
+ "./eco-embed": {
23
+ "default": "./src/eco-embed.js",
24
+ "types": "./src/eco-embed.d.ts"
25
+ },
26
+ "./eco-embed.ts": {
27
+ "default": "./src/eco-embed.js",
28
+ "types": "./src/eco-embed.d.ts"
29
+ }
30
+ },
14
31
  "repository": {
15
32
  "type": "git",
16
- "url": "https://github.com/ecopages/ecopages.git",
33
+ "url": "git+https://github.com/ecopages/ecopages.git",
17
34
  "directory": "packages/integrations/ecopages-jsx"
18
35
  },
19
36
  "dependencies": {
37
+ "@ecopages/file-system": "0.3.1",
38
+ "@ecopages/mdx": "0.3.1",
20
39
  "@mdx-js/mdx": "^3.1.1",
21
40
  "vfile": "^6.0.3"
22
41
  },
23
42
  "peerDependencies": {
24
- "@ecopages/core": "0.2.1",
25
- "@ecopages/jsx": "^0.3.0-alpha.0",
26
- "@ecopages/radiant": "^0.3.0-alpha.0"
43
+ "@ecopages/core": "0.3.1",
44
+ "@ecopages/jsx": "^0.4.0",
45
+ "@ecopages/radiant": "^0.4.0"
27
46
  }
28
- }
47
+ }
@@ -0,0 +1,17 @@
1
+ import type { EcoBuildPlugin } from '@ecopages/core/integration';
2
+ import type { DevTransformBundleContributor } from '@ecopages/core/integration';
3
+ import type { EcopagesJsxHmrStrategy } from '../ecopages-jsx-hmr-strategy.js';
4
+ export type EcopagesJsxDevTransformContributorOptions = {
5
+ strategy: EcopagesJsxHmrStrategy;
6
+ getMdxLoaderPlugin: () => EcoBuildPlugin | undefined;
7
+ };
8
+ /**
9
+ * Supplies Ecopages JSX module plugins for the core dev transform bundler.
10
+ */
11
+ export declare class EcopagesJsxDevTransformContributor implements DevTransformBundleContributor {
12
+ private readonly strategy;
13
+ private readonly getMdxLoaderPlugin;
14
+ constructor(options: EcopagesJsxDevTransformContributorOptions);
15
+ ownsModule(sourcePath: string): boolean;
16
+ getModulePlugins(sourcePath: string): Promise<readonly EcoBuildPlugin[]>;
17
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Supplies Ecopages JSX module plugins for the core dev transform bundler.
3
+ */
4
+ export class EcopagesJsxDevTransformContributor {
5
+ strategy;
6
+ getMdxLoaderPlugin;
7
+ constructor(options) {
8
+ this.strategy = options.strategy;
9
+ this.getMdxLoaderPlugin = options.getMdxLoaderPlugin;
10
+ }
11
+ ownsModule(sourcePath) {
12
+ return this.strategy.ownsDevTransformEntrypoint(sourcePath);
13
+ }
14
+ async getModulePlugins(sourcePath) {
15
+ if (!sourcePath.endsWith('.mdx')) {
16
+ return [];
17
+ }
18
+ const mdxLoader = this.getMdxLoaderPlugin();
19
+ return mdxLoader ? [mdxLoader] : [];
20
+ }
21
+ }
@@ -0,0 +1,11 @@
1
+ import type { EcoComponent, EcoEmbedProps as CoreEcoEmbedProps } from '@ecopages/core';
2
+ import type { JsxRenderable } from '@ecopages/jsx';
3
+ /**
4
+ * Props for the Ecopages JSX-owned `EcoEmbed` adapter.
5
+ */
6
+ export type EcoEmbedProps<TComponent extends EcoComponent> = CoreEcoEmbedProps<TComponent>;
7
+ /**
8
+ * Renders a foreign or same-integration eco component from an Ecopages JSX
9
+ * file without forcing inline mixed-JSX authoring.
10
+ */
11
+ export declare function EcoEmbed<TComponent extends EcoComponent>({ component, props, children, }: EcoEmbedProps<TComponent>): JsxRenderable;
@@ -0,0 +1,9 @@
1
+ /** @jsxImportSource @ecopages/jsx */
2
+ import { eco } from '@ecopages/core';
3
+ /**
4
+ * Renders a foreign or same-integration eco component from an Ecopages JSX
5
+ * file without forcing inline mixed-JSX authoring.
6
+ */
7
+ export function EcoEmbed({ component, props, children, }) {
8
+ return eco.embed(component, props, children);
9
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Tracks the source files that produced the most recent JSX render.
3
+ *
4
+ * @remarks
5
+ * The JSX renderer runs each page, component, or view render inside
6
+ * {@link withEjsxHmrOwnershipScope} and records its components with
7
+ * {@link recordEjsxHmrOwnership}. `EcopagesJsxHmrStrategy` reads the result to
8
+ * decide whether a changed file affects SSR HTML without walking the component
9
+ * tree on every watcher event.
10
+ *
11
+ * The state is module-scoped because the renderer and the HMR strategy share no
12
+ * other lifecycle anchor. Publishing replaces it only when the file set changes,
13
+ * so repeated renders of the same page keep the existing state.
14
+ *
15
+ * Only the latest top-level render is kept. With several pages open, a file
16
+ * used only by a page that rendered earlier is not matched until that page
17
+ * renders again.
18
+ */
19
+ import type { EcoComponent } from '@ecopages/core';
20
+ export type EjsxHmrOwnershipState = {
21
+ fileOwners: ReadonlySet<string>;
22
+ hash: string;
23
+ };
24
+ /**
25
+ * Returns the most recently recorded active render tree.
26
+ */
27
+ export declare function getEjsxHmrOwnership(): EjsxHmrOwnershipState;
28
+ /**
29
+ * Runs one render and publishes the files it recorded once the outermost scope ends.
30
+ *
31
+ * @remarks
32
+ * Nested renders, such as components and foreign subtrees resolved inside a
33
+ * page, join the outer scope, so the published set covers the whole render tree
34
+ * instead of the last nested render.
35
+ */
36
+ export declare function withEjsxHmrOwnershipScope<T>(render: () => T | Promise<T>): Promise<T>;
37
+ /**
38
+ * Adds the source files of `components`, their declared dependencies and their
39
+ * layouts to the active render scope.
40
+ *
41
+ * @throws Error when called outside {@link withEjsxHmrOwnershipScope}.
42
+ */
43
+ export declare function recordEjsxHmrOwnership(components: ReadonlyArray<EcoComponent | undefined>): void;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Tracks the source files that produced the most recent JSX render.
3
+ *
4
+ * @remarks
5
+ * The JSX renderer runs each page, component, or view render inside
6
+ * {@link withEjsxHmrOwnershipScope} and records its components with
7
+ * {@link recordEjsxHmrOwnership}. `EcopagesJsxHmrStrategy` reads the result to
8
+ * decide whether a changed file affects SSR HTML without walking the component
9
+ * tree on every watcher event.
10
+ *
11
+ * The state is module-scoped because the renderer and the HMR strategy share no
12
+ * other lifecycle anchor. Publishing replaces it only when the file set changes,
13
+ * so repeated renders of the same page keep the existing state.
14
+ *
15
+ * Only the latest top-level render is kept. With several pages open, a file
16
+ * used only by a page that rendered earlier is not matched until that page
17
+ * renders again.
18
+ */
19
+ import { AsyncLocalStorage } from 'node:async_hooks';
20
+ import { contentHash } from '@ecopages/core/build';
21
+ import { collectComponentConfigFilePaths } from '@ecopages/core/integration';
22
+ const EMPTY_STATE = {
23
+ fileOwners: new Set(),
24
+ hash: '',
25
+ };
26
+ let currentState = EMPTY_STATE;
27
+ const pendingFileOwners = new AsyncLocalStorage();
28
+ /**
29
+ * Returns the most recently recorded active render tree.
30
+ */
31
+ export function getEjsxHmrOwnership() {
32
+ return currentState;
33
+ }
34
+ /**
35
+ * Runs one render and publishes the files it recorded once the outermost scope ends.
36
+ *
37
+ * @remarks
38
+ * Nested renders, such as components and foreign subtrees resolved inside a
39
+ * page, join the outer scope, so the published set covers the whole render tree
40
+ * instead of the last nested render.
41
+ */
42
+ export async function withEjsxHmrOwnershipScope(render) {
43
+ if (pendingFileOwners.getStore()) {
44
+ return await render();
45
+ }
46
+ const fileOwners = new Set();
47
+ try {
48
+ return await pendingFileOwners.run(fileOwners, render);
49
+ }
50
+ finally {
51
+ if (fileOwners.size > 0) {
52
+ publishEjsxHmrOwnership(fileOwners);
53
+ }
54
+ }
55
+ }
56
+ /**
57
+ * Adds the source files of `components`, their declared dependencies and their
58
+ * layouts to the active render scope.
59
+ *
60
+ * @throws Error when called outside {@link withEjsxHmrOwnershipScope}.
61
+ */
62
+ export function recordEjsxHmrOwnership(components) {
63
+ const fileOwners = pendingFileOwners.getStore();
64
+ if (!fileOwners) {
65
+ throw new Error('Ecopages JSX HMR ownership can only be recorded inside an active render scope.');
66
+ }
67
+ for (const file of collectFileOwners(components)) {
68
+ fileOwners.add(file);
69
+ }
70
+ }
71
+ function publishEjsxHmrOwnership(files) {
72
+ const hash = hashFileSet(files);
73
+ if (hash === currentState.hash) {
74
+ return;
75
+ }
76
+ currentState = {
77
+ fileOwners: new Set(files),
78
+ hash,
79
+ };
80
+ }
81
+ function collectFileOwners(components) {
82
+ return collectComponentConfigFilePaths(components, { includeLayouts: true });
83
+ }
84
+ function hashFileSet(files) {
85
+ if (files.size === 0) {
86
+ return '';
87
+ }
88
+ const sorted = Array.from(files).sort();
89
+ const joined = sorted.join('\n');
90
+ return BigInt(`0x${contentHash(joined, 16)}`).toString(36);
91
+ }
@@ -0,0 +1,19 @@
1
+ import { HmrStrategy, type HmrAction } from '@ecopages/core/integration';
2
+ import type { HmrRegisteredEntrypointsContext } from '@ecopages/core/integration';
3
+ export type EcopagesJsxHmrStrategyContext = HmrRegisteredEntrypointsContext & {
4
+ getSrcDir(): string;
5
+ getPagesDir(): string;
6
+ getLayoutsDir(): string;
7
+ getIncludesDir(): string;
8
+ getTemplateExtensions(): ReadonlyArray<string>;
9
+ };
10
+ export declare class EcopagesJsxHmrStrategy extends HmrStrategy {
11
+ readonly type: 100;
12
+ private readonly context;
13
+ constructor(context: EcopagesJsxHmrStrategyContext);
14
+ matches(filePath: string): boolean;
15
+ ownsDevTransformEntrypoint(filePath: string): boolean;
16
+ process(filePath: string): Promise<HmrAction>;
17
+ private isPageOrLayoutSource;
18
+ private hasJsxTemplateExtension;
19
+ }
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import { HmrStrategy, HmrStrategyType } from '@ecopages/core/integration';
3
+ import { isRegisteredDevTransformEntrypoint, isRegisteredScriptEntrypoint } from '@ecopages/core/integration';
4
+ import { getEjsxHmrOwnership } from './ecopages-jsx-hmr-ownership.js';
5
+ export class EcopagesJsxHmrStrategy extends HmrStrategy {
6
+ type = HmrStrategyType.INTEGRATION;
7
+ context;
8
+ constructor(context) {
9
+ super();
10
+ this.context = context;
11
+ }
12
+ matches(filePath) {
13
+ const resolvedPath = path.resolve(filePath);
14
+ if (isRegisteredScriptEntrypoint(this.context.getRegisteredEntrypoints(), 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
+ ownsDevTransformEntrypoint(filePath) {
35
+ const resolvedPath = path.resolve(filePath);
36
+ if (!isRegisteredDevTransformEntrypoint(this.context.getRegisteredEntrypoints(), resolvedPath)) {
37
+ return false;
38
+ }
39
+ return this.isPageOrLayoutSource(resolvedPath) && this.hasJsxTemplateExtension(resolvedPath);
40
+ }
41
+ async process(filePath) {
42
+ return {
43
+ type: 'broadcast',
44
+ events: [
45
+ {
46
+ type: 'layout-update',
47
+ path: filePath,
48
+ timestamp: Date.now(),
49
+ },
50
+ ],
51
+ };
52
+ }
53
+ isPageOrLayoutSource(resolvedPath) {
54
+ const pagesDir = path.resolve(this.context.getPagesDir());
55
+ const layoutsDir = path.resolve(this.context.getLayoutsDir());
56
+ if (pagesDir && (resolvedPath === pagesDir || resolvedPath.startsWith(`${pagesDir}${path.sep}`))) {
57
+ return true;
58
+ }
59
+ if (layoutsDir && (resolvedPath === layoutsDir || resolvedPath.startsWith(`${layoutsDir}${path.sep}`))) {
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ hasJsxTemplateExtension(resolvedPath) {
65
+ return this.context.getTemplateExtensions().some((extension) => resolvedPath.endsWith(extension));
66
+ }
67
+ }
@@ -0,0 +1,32 @@
1
+ import type { CompileOptions } from '@mdx-js/mdx';
2
+ import type { EcoComponent, EcoComponentConfig, EcoFunctionComponent, EcoPageFile, GetMetadata } from '@ecopages/core';
3
+ import type { EcoBuildPlugin } from '@ecopages/core/integration';
4
+ import { appendMdxExtensions, createMdxExtensionFilter } from '@ecopages/mdx/core';
5
+ import type { JsxRenderable } from '@ecopages/jsx';
6
+ import type { EcopagesJsxMdxOptions } from './ecopages-jsx.types.js';
7
+ import type { RequestPipelineMetricsState } from '@ecopages/core/build';
8
+ export type ResolvedMdxCompileOptions = CompileOptions & {
9
+ jsxImportSource: '@ecopages/jsx';
10
+ };
11
+ export type AsyncEcoComponent<P = Record<string, unknown>, R = JsxRenderable> = EcoFunctionComponent<P, R | Promise<R>>;
12
+ type AsyncEcoPageComponent = AsyncEcoComponent<Record<string, unknown>> & {
13
+ metadata?: GetMetadata;
14
+ };
15
+ export type EcopagesJsxMdxPageModule = Omit<EcoPageFile<{
16
+ config?: EcoComponentConfig;
17
+ layout?: EcoComponent;
18
+ getMetadata?: GetMetadata;
19
+ }>, 'default'> & {
20
+ default: AsyncEcoPageComponent;
21
+ };
22
+ export { appendMdxExtensions, createMdxExtensionFilter };
23
+ export declare const resolveMdxCompilerOptions: (mdxOptions: EcopagesJsxMdxOptions) => ResolvedMdxCompileOptions;
24
+ export interface CreateEcopagesJsxMdxLoaderPluginOptions {
25
+ compilerOptions: ResolvedMdxCompileOptions;
26
+ extensions: string[];
27
+ projectRoot: string;
28
+ requestPipelineMetrics: RequestPipelineMetricsState;
29
+ }
30
+ export declare const createMdxLoaderPlugin: (options: CreateEcopagesJsxMdxLoaderPluginOptions) => EcoBuildPlugin;
31
+ export declare const isMdxFile: (filePath: string, extensions: string[]) => boolean;
32
+ export declare const normalizeMdxPageModule: (file: string, module: EcopagesJsxMdxPageModule) => EcopagesJsxMdxPageModule;
@@ -0,0 +1,48 @@
1
+ import { ensurePageConfigLayouts } from '@ecopages/core/integration';
2
+ import { appendMdxExtensions, createMdxExtensionFilter, createMdxLoaderPlugin as createMdxLoaderPluginCore, resolveMdxCompilerOptions as resolveMdxCompilerOptionsCore, } from '@ecopages/mdx/core';
3
+ import { ECOPAGES_JSX_PLUGIN_NAME } from './ecopages-jsx.constants.js';
4
+ export { appendMdxExtensions, createMdxExtensionFilter };
5
+ export const resolveMdxCompilerOptions = (mdxOptions) => resolveMdxCompilerOptionsCore(mdxOptions, {
6
+ jsxImportSource: '@ecopages/jsx',
7
+ defaults: {
8
+ format: 'detect',
9
+ outputFormat: 'program',
10
+ },
11
+ });
12
+ export const createMdxLoaderPlugin = (options) => createMdxLoaderPluginCore({
13
+ name: 'ecopages-jsx-mdx-loader',
14
+ integrationName: ECOPAGES_JSX_PLUGIN_NAME,
15
+ compilerOptions: options.compilerOptions,
16
+ extensions: options.extensions,
17
+ includeSourceMap: false,
18
+ loader: 'js',
19
+ projectRoot: options.projectRoot,
20
+ requestPipelineMetrics: options.requestPipelineMetrics,
21
+ });
22
+ export const isMdxFile = (filePath, extensions) => {
23
+ return extensions.some((ext) => filePath.endsWith(ext));
24
+ };
25
+ export const normalizeMdxPageModule = (file, module) => {
26
+ if (typeof module.default !== 'function') {
27
+ throw new TypeError('MDX file must export a callable default component.');
28
+ }
29
+ const Page = module.default;
30
+ const attributedConfig = module.config ?? Page.config;
31
+ if (!attributedConfig?.identity) {
32
+ throw new Error(`[ecopages] MDX page "${file}" is missing component identity; the MDX loader must attribute it.`);
33
+ }
34
+ if (module.layout && !(attributedConfig.layouts && attributedConfig.layouts.length > 0)) {
35
+ attributedConfig.layout = module.layout;
36
+ }
37
+ ensurePageConfigLayouts(attributedConfig);
38
+ const wrappedPage = async (props) => await Page(props);
39
+ wrappedPage.config = attributedConfig;
40
+ if (module.getMetadata ?? Page.metadata) {
41
+ wrappedPage.metadata = module.getMetadata ?? Page.metadata;
42
+ }
43
+ return {
44
+ ...module,
45
+ default: wrappedPage,
46
+ config: attributedConfig,
47
+ };
48
+ };
@@ -0,0 +1,39 @@
1
+ import { createMarkupNodeLike } from '@ecopages/jsx';
2
+ /**
3
+ * Owns optional Radiant SSR runtime installation for the JSX integration.
4
+ *
5
+ * @remarks
6
+ * Radiant's server bridge and light-DOM shim are loaded lazily because most JSX
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}; the
11
+ * Radiant installer also repairs incomplete DOM globals left by another runtime.
12
+ */
13
+ export declare class EcopagesJsxRadiantSsrPolicy {
14
+ private runtimeModules;
15
+ private runtimeModulesPromise;
16
+ private readonly enabled;
17
+ constructor(enabled: boolean);
18
+ /**
19
+ * Ensures the Radiant SSR runtime is installed before a render needs it.
20
+ */
21
+ prepareRuntime(): Promise<void>;
22
+ /**
23
+ * Runs one render inside Radiant's server runtime when the policy is enabled.
24
+ */
25
+ withRuntime<T>(render: () => T): Promise<T>;
26
+ /**
27
+ * Converts one Radiant custom-element instance into trusted SSR markup.
28
+ *
29
+ * @remarks
30
+ * The returned node-like wrapper lets the JSX server renderer preserve the
31
+ * generated host HTML without escaping it back into plain text.
32
+ *
33
+ * The render hook owns the host's JSX children, so `applyChildren` runs inside the host's context providers.
34
+ * A consumer passed as a child of a provider host then resolves that provider in the server HTML instead of
35
+ * rendering its fallback state.
36
+ */
37
+ renderIntrinsicElementMarkup(instance: unknown, applyChildren?: () => void): ReturnType<typeof createMarkupNodeLike> | undefined;
38
+ private ensureRuntimeInstalled;
39
+ }