@ecopages/ecopages-jsx 0.2.1-beta.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -16
- package/package.json +10 -6
- package/src/dev-transform/ecopages-jsx-dev-transform-contributor.d.ts +17 -0
- package/src/dev-transform/ecopages-jsx-dev-transform-contributor.js +21 -0
- package/src/eco-embed.js +8 -10
- package/src/ecopages-jsx-hmr-ownership.d.ts +43 -0
- package/src/ecopages-jsx-hmr-ownership.js +91 -0
- package/src/ecopages-jsx-hmr-strategy.d.ts +19 -0
- package/src/ecopages-jsx-hmr-strategy.js +67 -0
- package/src/ecopages-jsx-mdx.d.ts +22 -11
- package/src/ecopages-jsx-mdx.js +43 -108
- package/src/ecopages-jsx-radiant-ssr-policy.d.ts +12 -6
- package/src/ecopages-jsx-radiant-ssr-policy.js +116 -73
- package/src/ecopages-jsx-renderer.d.ts +10 -45
- package/src/ecopages-jsx-renderer.js +148 -233
- package/src/ecopages-jsx.constants.js +7 -4
- package/src/ecopages-jsx.plugin.d.ts +19 -23
- package/src/ecopages-jsx.plugin.js +137 -111
- package/src/ecopages-jsx.types.d.ts +4 -34
- package/src/ecopages-jsx.types.js +1 -0
- package/src/page-owned-content-script-grouping.d.ts +11 -0
- package/src/page-owned-content-script-grouping.js +57 -0
- package/src/radiant-registered-script-ssr-invalidation.d.ts +19 -0
- package/src/radiant-registered-script-ssr-invalidation.js +49 -0
- package/src/resolve-radiant-install-hydrator.d.ts +2 -0
- package/src/resolve-radiant-install-hydrator.js +3 -0
- package/src/ecopages-jsx-render-session.d.ts +0 -46
- package/src/ecopages-jsx-render-session.js +0 -93
package/README.md
CHANGED
|
@@ -15,15 +15,10 @@ bun add @ecopages/ecopages-jsx @ecopages/jsx @ecopages/radiant
|
|
|
15
15
|
Register the `ecopagesJsxPlugin` in your `eco.config.ts`.
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
import {
|
|
18
|
+
import { defineConfig } from '@ecopages/core/config';
|
|
19
19
|
import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
|
|
20
20
|
|
|
21
|
-
|
|
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
|
|
@@ -42,16 +37,27 @@ 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
46
|
Radiant support is enabled by default. When `radiant: true`, the plugin keeps the ownership split explicit:
|
|
48
47
|
|
|
49
48
|
- Ecopages JSX owns page-level JSX SSR and container hydration.
|
|
50
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
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
52
|
|
|
53
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
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.
|
|
60
|
+
|
|
55
61
|
```ts
|
|
56
62
|
ecopagesJsxPlugin({
|
|
57
63
|
radiant: true,
|
|
@@ -71,26 +77,40 @@ new MyElement().renderHostToString({ mode: 'hydrate' });
|
|
|
71
77
|
|
|
72
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.
|
|
73
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.
|
|
96
|
+
|
|
74
97
|
## MDX Support
|
|
75
98
|
|
|
76
|
-
|
|
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.
|
|
77
100
|
|
|
78
101
|
```ts
|
|
79
|
-
import {
|
|
102
|
+
import { defineConfig } from '@ecopages/core/config';
|
|
80
103
|
import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
export default defineConfig({
|
|
106
|
+
integrations: [
|
|
84
107
|
ecopagesJsxPlugin({
|
|
85
108
|
mdx: {
|
|
86
|
-
enabled: true,
|
|
87
109
|
extensions: ['.mdx', '.md'],
|
|
88
110
|
},
|
|
89
111
|
}),
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
export default config;
|
|
112
|
+
],
|
|
113
|
+
});
|
|
94
114
|
```
|
|
95
115
|
|
|
96
116
|
## Mixed Rendering
|
|
@@ -99,6 +119,9 @@ Ecopages JSX can own the outer page shell or just a nested foreign subtree. When
|
|
|
99
119
|
|
|
100
120
|
Important:
|
|
101
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.
|
|
102
123
|
- Components that may render foreign children must declare those children in `config.dependencies.components`.
|
|
103
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.
|
|
104
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "JSX integration plugin for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -11,6 +11,9 @@
|
|
|
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
|
+
},
|
|
14
17
|
"exports": {
|
|
15
18
|
".": {
|
|
16
19
|
"default": "./src/ecopages-jsx.plugin.js",
|
|
@@ -27,16 +30,17 @@
|
|
|
27
30
|
},
|
|
28
31
|
"repository": {
|
|
29
32
|
"type": "git",
|
|
30
|
-
"url": "https://github.com/ecopages/ecopages.git",
|
|
33
|
+
"url": "git+https://github.com/ecopages/ecopages.git",
|
|
31
34
|
"directory": "packages/integrations/ecopages-jsx"
|
|
32
35
|
},
|
|
33
36
|
"dependencies": {
|
|
37
|
+
"@ecopages/mdx": "0.3.0",
|
|
34
38
|
"@mdx-js/mdx": "^3.1.1",
|
|
35
39
|
"vfile": "^6.0.3"
|
|
36
40
|
},
|
|
37
41
|
"peerDependencies": {
|
|
38
|
-
"@ecopages/core": "0.
|
|
39
|
-
"@ecopages/jsx": "0.
|
|
40
|
-
"@ecopages/radiant": "0.
|
|
42
|
+
"@ecopages/core": "0.3.0",
|
|
43
|
+
"@ecopages/jsx": "^0.4.0",
|
|
44
|
+
"@ecopages/radiant": "^0.4.0"
|
|
41
45
|
}
|
|
42
|
-
}
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|
package/src/eco-embed.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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);
|
|
8
9
|
}
|
|
9
|
-
export {
|
|
10
|
-
EcoEmbed
|
|
11
|
-
};
|
|
@@ -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
|
+
}
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import type { CompileOptions } from '@mdx-js/mdx';
|
|
2
2
|
import type { EcoComponent, EcoComponentConfig, EcoFunctionComponent, EcoPageFile, GetMetadata } from '@ecopages/core';
|
|
3
|
-
import type { EcoBuildPlugin } from '@ecopages/core/
|
|
3
|
+
import type { EcoBuildPlugin } from '@ecopages/core/integration';
|
|
4
|
+
import { appendMdxExtensions, createMdxExtensionFilter } from '@ecopages/mdx/core';
|
|
4
5
|
import type { JsxRenderable } from '@ecopages/jsx';
|
|
5
|
-
import type {
|
|
6
|
-
|
|
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
|
+
};
|
|
7
11
|
export type AsyncEcoComponent<P = Record<string, unknown>, R = JsxRenderable> = EcoFunctionComponent<P, R | Promise<R>>;
|
|
8
|
-
|
|
12
|
+
type AsyncEcoPageComponent = AsyncEcoComponent<Record<string, unknown>> & {
|
|
13
|
+
metadata?: GetMetadata;
|
|
14
|
+
};
|
|
15
|
+
export type EcopagesJsxMdxPageModule = Omit<EcoPageFile<{
|
|
9
16
|
config?: EcoComponentConfig;
|
|
10
17
|
layout?: EcoComponent;
|
|
11
18
|
getMetadata?: GetMetadata;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export declare const appendMdxExtensions: (target: string[], mdxExtensions: string[]) => void;
|
|
19
|
+
}>, 'default'> & {
|
|
20
|
+
default: AsyncEcoPageComponent;
|
|
21
|
+
};
|
|
22
|
+
export { appendMdxExtensions, createMdxExtensionFilter };
|
|
17
23
|
export declare const resolveMdxCompilerOptions: (mdxOptions: EcopagesJsxMdxOptions) => ResolvedMdxCompileOptions;
|
|
18
|
-
export
|
|
19
|
-
|
|
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;
|
|
20
31
|
export declare const isMdxFile: (filePath: string, extensions: string[]) => boolean;
|
|
21
32
|
export declare const normalizeMdxPageModule: (file: string, module: EcopagesJsxMdxPageModule) => EcopagesJsxMdxPageModule;
|
package/src/ecopages-jsx-mdx.js
CHANGED
|
@@ -1,113 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.');
|
|
20
28
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const resolved = {
|
|
26
|
-
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
|
-
});
|
|
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.`);
|
|
57
33
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const registerBunMdxPlugin = async (compilerOptions, extensions) => {
|
|
61
|
-
if (typeof Bun === "undefined") {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const filter = createMdxExtensionFilter(extensions);
|
|
65
|
-
Bun.plugin({
|
|
66
|
-
name: "ecopages-jsx-mdx",
|
|
67
|
-
setup(build) {
|
|
68
|
-
build.onLoad({ filter }, async (args) => {
|
|
69
|
-
const { compile } = await import("@mdx-js/mdx");
|
|
70
|
-
const source = await readFile(args.path, "utf-8");
|
|
71
|
-
const compiled = await compile(new VFile({ value: source, path: args.path }), compilerOptions);
|
|
72
|
-
return { contents: String(compiled.value), loader: "js" };
|
|
73
|
-
});
|
|
34
|
+
if (module.layout && !(attributedConfig.layouts && attributedConfig.layouts.length > 0)) {
|
|
35
|
+
attributedConfig.layout = module.layout;
|
|
74
36
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const normalizeMdxPageModule = (file, module) => {
|
|
81
|
-
if (typeof module.default !== "function") {
|
|
82
|
-
throw new TypeError("MDX file must export a callable default component.");
|
|
83
|
-
}
|
|
84
|
-
const Page = module.default;
|
|
85
|
-
const normalizedConfig = {
|
|
86
|
-
...module.config ?? Page.config ?? {},
|
|
87
|
-
...module.layout ? { layout: module.layout } : {},
|
|
88
|
-
__eco: module.config?.__eco ?? Page.config?.__eco ?? {
|
|
89
|
-
id: String(rapidhash(file)),
|
|
90
|
-
file,
|
|
91
|
-
integration: ECOPAGES_JSX_PLUGIN_NAME
|
|
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;
|
|
92
42
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
...module,
|
|
101
|
-
default: wrappedPage,
|
|
102
|
-
config: normalizedConfig
|
|
103
|
-
};
|
|
104
|
-
};
|
|
105
|
-
export {
|
|
106
|
-
appendMdxExtensions,
|
|
107
|
-
createMdxExtensionFilter,
|
|
108
|
-
createMdxLoaderPlugin,
|
|
109
|
-
isMdxFile,
|
|
110
|
-
normalizeMdxPageModule,
|
|
111
|
-
registerBunMdxPlugin,
|
|
112
|
-
resolveMdxCompilerOptions
|
|
43
|
+
return {
|
|
44
|
+
...module,
|
|
45
|
+
default: wrappedPage,
|
|
46
|
+
config: attributedConfig,
|
|
47
|
+
};
|
|
113
48
|
};
|