@ecopages/mdx 0.2.0-alpha.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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/package.json +49 -0
- package/src/declarations.d.ts +8 -0
- package/src/mdx-loader-plugin.d.ts +3 -0
- package/src/mdx-loader-plugin.js +37 -0
- package/src/mdx-loader-plugin.ts +40 -0
- package/src/mdx-renderer.d.ts +51 -0
- package/src/mdx-renderer.js +125 -0
- package/src/mdx-renderer.ts +221 -0
- package/src/mdx.plugin.d.ts +34 -0
- package/src/mdx.plugin.js +54 -0
- package/src/mdx.plugin.ts +85 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@ecopages/mdx` are documented here.
|
|
4
|
+
|
|
5
|
+
> **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
|
|
6
|
+
|
|
7
|
+
## [UNRELEASED] — TBD
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- **Decoupled from React** — The MDX integration now works as a standalone, runtime-agnostic integration. React dependencies have been removed; MDX routes are server-rendered without requiring the React integration (`4d5474a4`).
|
|
12
|
+
- **Async MDX compilation** — The MDX loader plugin now compiles MDX files asynchronously, improving compatibility with async remark/rehype plugins (`9e879dbe`).
|
|
13
|
+
- **Updated type definitions** — Plugin options are more precisely typed to clarify server-rendered MDX route configuration.
|
|
14
|
+
|
|
15
|
+
### Refactoring
|
|
16
|
+
|
|
17
|
+
- Removed unused HMR strategy and renderer files that were React-specific.
|
|
18
|
+
- README updated to document standalone MDX usage without React.
|
|
19
|
+
- Ambient module declarations cleaned up (`5f46ecc5`).
|
|
20
|
+
- Aligned with full orchestration mode (`fc07bdb0`).
|
|
21
|
+
|
|
22
|
+
### Documentation
|
|
23
|
+
|
|
24
|
+
- README updated to clarify the integration is now usable without the React integration.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Migration Notes
|
|
29
|
+
|
|
30
|
+
- If you were using `@ecopages/mdx` together with `@ecopages/react` for MDX routes, the two integrations must now be registered separately. MDX handles server rendering; React handles client hydration.
|
|
31
|
+
- The `useReact` option and React-specific HMR hooks have been removed.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present Andrea Zanenghi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Ecopages MDX Integration Plugin
|
|
2
|
+
|
|
3
|
+
The `@ecopages/mdx` package adds standalone MDX support for non-React JSX runtimes such as `@kitajs/html`. It uses the MDX compiler through Ecopages' integration system and is intended for server-rendered `.mdx` routes.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bunx jsr add @ecopages/mdx
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Integrating MDX into your Ecopages project is made simple. Import and apply the `mdxPlugin` in your Ecopages configuration as demonstrated below:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { ConfigBuilder } from '@ecopages/core';
|
|
17
|
+
import { mdxPlugin } from '@ecopages/mdx';
|
|
18
|
+
|
|
19
|
+
const config = await new ConfigBuilder()
|
|
20
|
+
.setBaseUrl(import.meta.env.ECOPAGES_BASE_URL)
|
|
21
|
+
.setIntegrations([mdxPlugin()])
|
|
22
|
+
.build();
|
|
23
|
+
|
|
24
|
+
export default config;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
By default, the standalone plugin uses:
|
|
28
|
+
|
|
29
|
+
- `jsxImportSource: '@kitajs/html'`
|
|
30
|
+
- `jsxRuntime: 'automatic'`
|
|
31
|
+
|
|
32
|
+
You can override MDX compiler options, but React runtimes are intentionally not supported here.
|
|
33
|
+
|
|
34
|
+
## Using MDX with React Router
|
|
35
|
+
|
|
36
|
+
If you are using `@ecopages/react` with a client-side router, enable MDX directly within the React plugin instead of using this standalone plugin. This ensures unified routing, hydration, and HMR for both `.tsx` and `.mdx` pages:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { reactPlugin } from '@ecopages/react';
|
|
40
|
+
import { ecoRouter } from '@ecopages/react-router';
|
|
41
|
+
|
|
42
|
+
reactPlugin({
|
|
43
|
+
router: ecoRouter(),
|
|
44
|
+
mdx: { enabled: true },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
See the `@ecopages/react` documentation for details.
|
|
49
|
+
|
|
50
|
+
## React runtimes are not supported here
|
|
51
|
+
|
|
52
|
+
Standalone `mdxPlugin()` rejects `jsxImportSource: 'react'` and related React JSX runtimes. For React-backed MDX, use [@ecopages/react](../react/README.md) with `mdx.enabled`.
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ecopages/mdx",
|
|
3
|
+
"version": "0.2.0-alpha.1",
|
|
4
|
+
"description": "MDX plugin for Ecopages",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ecopages",
|
|
7
|
+
"mdx",
|
|
8
|
+
"plugin"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"main": "./src/mdx.plugin.js",
|
|
12
|
+
"types": "./src/mdx.plugin.d.ts",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"default": "./src/mdx.plugin.js",
|
|
17
|
+
"types": "./src/mdx.plugin.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./declarations": {
|
|
20
|
+
"types": "./src/declarations.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./mdx-loader-plugin": {
|
|
23
|
+
"default": "./src/mdx-loader-plugin.js",
|
|
24
|
+
"types": "./src/mdx-loader-plugin.d.ts"
|
|
25
|
+
},
|
|
26
|
+
"./declarations.ts": {
|
|
27
|
+
"types": "./src/declarations.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./mdx-loader-plugin.ts": {
|
|
30
|
+
"default": "./src/mdx-loader-plugin.js",
|
|
31
|
+
"types": "./src/mdx-loader-plugin.d.ts"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/ecopages/ecopages.git",
|
|
37
|
+
"directory": "packages/integrations/mdx"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@ecopages/core": "0.2.0-alpha.1",
|
|
41
|
+
"@kitajs/html": "^4.1.0",
|
|
42
|
+
"@mdx-js/mdx": "^3.1.0"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@ecopages/logger": "^0.2.2",
|
|
46
|
+
"source-map": "^0.7.6",
|
|
47
|
+
"vfile": "^6.0.3"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
declare module '*.mdx' {
|
|
2
|
+
import type { EcoComponent, EcoComponentConfig, EcoPagesElement, GetMetadata } from '@ecopages/core';
|
|
3
|
+
|
|
4
|
+
const MDXComponent: EcoComponent<Record<string, unknown>>;
|
|
5
|
+
export const config: EcoComponentConfig | undefined;
|
|
6
|
+
export const getMetadata: GetMetadata | undefined;
|
|
7
|
+
export default MDXComponent;
|
|
8
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compile } from "@mdx-js/mdx";
|
|
4
|
+
import { SourceMapGenerator } from "source-map";
|
|
5
|
+
import { VFile } from "vfile";
|
|
6
|
+
function createMdxLoaderPlugin(compilerOptions) {
|
|
7
|
+
const mdxExtensions = compilerOptions?.mdxExtensions ?? [".mdx"];
|
|
8
|
+
const mdExtensions = compilerOptions?.mdExtensions ?? [".md"];
|
|
9
|
+
const allExtensions = [...mdxExtensions, ...mdExtensions];
|
|
10
|
+
const escapedExts = allExtensions.map((ext) => ext.replace(".", "\\."));
|
|
11
|
+
const filter = new RegExp(`(${escapedExts.join("|")})(\\?.*)?$`);
|
|
12
|
+
return {
|
|
13
|
+
name: "mdx-loader",
|
|
14
|
+
setup(build) {
|
|
15
|
+
build.onLoad({ filter }, async (args) => {
|
|
16
|
+
const filePath = args.path.includes("?") ? args.path.split("?")[0] : args.path;
|
|
17
|
+
const source = readFileSync(filePath, "utf-8");
|
|
18
|
+
const file = new VFile({ path: filePath, value: source });
|
|
19
|
+
const compiled = await compile(file, {
|
|
20
|
+
...compilerOptions,
|
|
21
|
+
SourceMapGenerator
|
|
22
|
+
});
|
|
23
|
+
const sourceMap = compiled.map ? `
|
|
24
|
+
//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(compiled.map)).toString("base64")}
|
|
25
|
+
` : "";
|
|
26
|
+
return {
|
|
27
|
+
contents: `${String(compiled.value)}${sourceMap}`,
|
|
28
|
+
loader: compilerOptions?.jsx ? "jsx" : "js",
|
|
29
|
+
resolveDir: path.dirname(args.path)
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
createMdxLoaderPlugin
|
|
37
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
4
|
+
import { type CompileOptions, compile } from '@mdx-js/mdx';
|
|
5
|
+
import { SourceMapGenerator } from 'source-map';
|
|
6
|
+
import { VFile } from 'vfile';
|
|
7
|
+
|
|
8
|
+
export function createMdxLoaderPlugin(compilerOptions?: CompileOptions): EcoBuildPlugin {
|
|
9
|
+
const mdxExtensions = compilerOptions?.mdxExtensions ?? ['.mdx'];
|
|
10
|
+
const mdExtensions = compilerOptions?.mdExtensions ?? ['.md'];
|
|
11
|
+
const allExtensions = [...mdxExtensions, ...mdExtensions];
|
|
12
|
+
const escapedExts = allExtensions.map((ext) => ext.replace('.', '\\.'));
|
|
13
|
+
const filter = new RegExp(`(${escapedExts.join('|')})(\\?.*)?$`);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: 'mdx-loader',
|
|
17
|
+
setup(build) {
|
|
18
|
+
build.onLoad({ filter }, async (args) => {
|
|
19
|
+
const filePath = args.path.includes('?') ? args.path.split('?')[0] : args.path;
|
|
20
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
21
|
+
const file = new VFile({ path: filePath, value: source });
|
|
22
|
+
|
|
23
|
+
const compiled = await compile(file, {
|
|
24
|
+
...compilerOptions,
|
|
25
|
+
SourceMapGenerator,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const sourceMap = compiled.map
|
|
29
|
+
? `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(compiled.map)).toString('base64')}\n`
|
|
30
|
+
: '';
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
contents: `${String(compiled.value)}${sourceMap}`,
|
|
34
|
+
loader: compilerOptions?.jsx ? 'jsx' : 'js',
|
|
35
|
+
resolveDir: path.dirname(args.path),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module contains the MDX renderer
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import type { EcoComponent, EcoComponentConfig, EcoPageFile, EcoPagesElement, GetMetadata, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
|
|
6
|
+
import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
|
|
7
|
+
import type { AssetProcessingService, ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
|
|
8
|
+
import type { CompileOptions } from '@mdx-js/mdx';
|
|
9
|
+
/**
|
|
10
|
+
* A structure representing an MDX file
|
|
11
|
+
*/
|
|
12
|
+
export type MDXFile = {
|
|
13
|
+
default: EcoComponent;
|
|
14
|
+
config?: EcoComponentConfig;
|
|
15
|
+
getMetadata: GetMetadata;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Options for the MDX renderer
|
|
19
|
+
*/
|
|
20
|
+
interface MDXIntegrationRendererOpions<C = EcoPagesElement> extends IntegrationRendererRenderOptions<C> {
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A renderer for the MDX integration.
|
|
24
|
+
*/
|
|
25
|
+
export declare class MDXRenderer extends IntegrationRenderer<EcoPagesElement> {
|
|
26
|
+
name: string;
|
|
27
|
+
compilerOptions: CompileOptions;
|
|
28
|
+
constructor({ compilerOptions, ...options }: {
|
|
29
|
+
appConfig: any;
|
|
30
|
+
assetProcessingService: AssetProcessingService;
|
|
31
|
+
resolvedIntegrationDependencies: ProcessedAsset[];
|
|
32
|
+
runtimeOrigin: string;
|
|
33
|
+
compilerOptions?: CompileOptions;
|
|
34
|
+
});
|
|
35
|
+
buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
|
|
36
|
+
protected importPageFile(file: string): Promise<EcoPageFile<{
|
|
37
|
+
layout?: EcoComponent<any> | {
|
|
38
|
+
config: EcoComponentConfig | undefined;
|
|
39
|
+
};
|
|
40
|
+
}>>;
|
|
41
|
+
render({ params, query, props, locals, pageLocals, metadata, Page, HtmlTemplate, Layout, pageProps, }: MDXIntegrationRendererOpions): Promise<RouteRendererBody>;
|
|
42
|
+
renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Factory function to create an MDX renderer class with specific compiler options.
|
|
46
|
+
*
|
|
47
|
+
* @param compilerOptions - Compiler options for MDX compilation.
|
|
48
|
+
* @returns A new MDXRenderer class extended with the provided context.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createMDXRenderer(compilerOptions: CompileOptions): typeof MDXRenderer;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { IntegrationRenderer } from "@ecopages/core/route-renderer/integration-renderer";
|
|
2
|
+
import { invariant } from "@ecopages/core/utils/invariant";
|
|
3
|
+
import { PLUGIN_NAME } from "./mdx.plugin.js";
|
|
4
|
+
import { rapidhash } from "@ecopages/core/hash";
|
|
5
|
+
class MDXRenderer extends IntegrationRenderer {
|
|
6
|
+
name = PLUGIN_NAME;
|
|
7
|
+
compilerOptions;
|
|
8
|
+
constructor({
|
|
9
|
+
compilerOptions,
|
|
10
|
+
...options
|
|
11
|
+
}) {
|
|
12
|
+
super(options);
|
|
13
|
+
this.compilerOptions = compilerOptions || {};
|
|
14
|
+
}
|
|
15
|
+
async buildRouteRenderAssets(pagePath) {
|
|
16
|
+
const { default: pageComponent } = await this.importPageFile(pagePath);
|
|
17
|
+
const config = pageComponent.config;
|
|
18
|
+
const components = [];
|
|
19
|
+
const resolvedLayout = config?.layout;
|
|
20
|
+
if (resolvedLayout?.config?.dependencies) {
|
|
21
|
+
components.push({ config: resolvedLayout.config });
|
|
22
|
+
}
|
|
23
|
+
if (config?.dependencies) {
|
|
24
|
+
components.push({
|
|
25
|
+
config: {
|
|
26
|
+
...config,
|
|
27
|
+
__eco: {
|
|
28
|
+
id: rapidhash(pagePath).toString(36),
|
|
29
|
+
file: pagePath,
|
|
30
|
+
integration: this.name
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return await this.resolveDependencies(components);
|
|
36
|
+
}
|
|
37
|
+
async importPageFile(file) {
|
|
38
|
+
try {
|
|
39
|
+
const {
|
|
40
|
+
default: Page,
|
|
41
|
+
config,
|
|
42
|
+
getMetadata
|
|
43
|
+
} = await super.importPageFile(file);
|
|
44
|
+
if (typeof Page !== "function") {
|
|
45
|
+
throw new Error("MDX file must export a default function");
|
|
46
|
+
}
|
|
47
|
+
const resolvedLayout = config?.layout;
|
|
48
|
+
if (config) Page.config = config;
|
|
49
|
+
return {
|
|
50
|
+
default: Page,
|
|
51
|
+
layout: resolvedLayout,
|
|
52
|
+
getMetadata
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
invariant(false, `Error importing MDX file: ${error}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async render({
|
|
59
|
+
params,
|
|
60
|
+
query,
|
|
61
|
+
props,
|
|
62
|
+
locals,
|
|
63
|
+
pageLocals,
|
|
64
|
+
metadata,
|
|
65
|
+
Page,
|
|
66
|
+
HtmlTemplate,
|
|
67
|
+
Layout,
|
|
68
|
+
pageProps
|
|
69
|
+
}) {
|
|
70
|
+
try {
|
|
71
|
+
const pageContent = await Page({ params, query, ...props, locals: pageLocals });
|
|
72
|
+
const children = typeof Layout === "function" ? await Layout({ children: pageContent, locals }) : pageContent;
|
|
73
|
+
const body = await HtmlTemplate({
|
|
74
|
+
metadata,
|
|
75
|
+
children,
|
|
76
|
+
pageProps: pageProps || {}
|
|
77
|
+
});
|
|
78
|
+
return this.DOC_TYPE + body;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw this.createRenderError("Error rendering page", error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async renderToResponse(view, props, ctx) {
|
|
84
|
+
try {
|
|
85
|
+
const Layout = view.config?.layout;
|
|
86
|
+
const viewFn = view;
|
|
87
|
+
const pageContent = await viewFn(props);
|
|
88
|
+
let body;
|
|
89
|
+
if (ctx.partial) {
|
|
90
|
+
body = pageContent;
|
|
91
|
+
} else {
|
|
92
|
+
const children = Layout ? await Layout({ children: pageContent }) : pageContent;
|
|
93
|
+
const HtmlTemplate = await this.getHtmlTemplate();
|
|
94
|
+
const metadata = view.metadata ? await view.metadata({
|
|
95
|
+
params: {},
|
|
96
|
+
query: {},
|
|
97
|
+
props,
|
|
98
|
+
appConfig: this.appConfig
|
|
99
|
+
}) : this.appConfig.defaultMetadata;
|
|
100
|
+
body = this.DOC_TYPE + await HtmlTemplate({
|
|
101
|
+
metadata,
|
|
102
|
+
children,
|
|
103
|
+
pageProps: props
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return this.createHtmlResponse(body, ctx);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
throw this.createRenderError("Error rendering view", error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function createMDXRenderer(compilerOptions) {
|
|
113
|
+
return class extends MDXRenderer {
|
|
114
|
+
constructor(options) {
|
|
115
|
+
super({
|
|
116
|
+
...options,
|
|
117
|
+
compilerOptions
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
MDXRenderer,
|
|
124
|
+
createMDXRenderer
|
|
125
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This module contains the MDX renderer
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
EcoComponent,
|
|
8
|
+
EcoComponentConfig,
|
|
9
|
+
EcoPageFile,
|
|
10
|
+
EcoPagesElement,
|
|
11
|
+
GetMetadata,
|
|
12
|
+
IntegrationRendererRenderOptions,
|
|
13
|
+
PageMetadataProps,
|
|
14
|
+
RouteRendererBody,
|
|
15
|
+
} from '@ecopages/core';
|
|
16
|
+
import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
|
|
17
|
+
import { invariant } from '@ecopages/core/utils/invariant';
|
|
18
|
+
import type { AssetProcessingService, ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
|
|
19
|
+
import type { CompileOptions } from '@mdx-js/mdx';
|
|
20
|
+
import { PLUGIN_NAME } from './mdx.plugin.ts';
|
|
21
|
+
import { rapidhash } from '@ecopages/core/hash';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A structure representing an MDX file
|
|
25
|
+
*/
|
|
26
|
+
export type MDXFile = {
|
|
27
|
+
default: EcoComponent;
|
|
28
|
+
config?: EcoComponentConfig;
|
|
29
|
+
getMetadata: GetMetadata;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for the MDX renderer
|
|
34
|
+
*/
|
|
35
|
+
interface MDXIntegrationRendererOpions<C = EcoPagesElement> extends IntegrationRendererRenderOptions<C> {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A renderer for the MDX integration.
|
|
39
|
+
*/
|
|
40
|
+
export class MDXRenderer extends IntegrationRenderer<EcoPagesElement> {
|
|
41
|
+
name = PLUGIN_NAME;
|
|
42
|
+
compilerOptions: CompileOptions;
|
|
43
|
+
|
|
44
|
+
constructor({
|
|
45
|
+
compilerOptions,
|
|
46
|
+
...options
|
|
47
|
+
}: {
|
|
48
|
+
appConfig: any;
|
|
49
|
+
assetProcessingService: AssetProcessingService;
|
|
50
|
+
resolvedIntegrationDependencies: ProcessedAsset[];
|
|
51
|
+
runtimeOrigin: string;
|
|
52
|
+
compilerOptions?: CompileOptions;
|
|
53
|
+
}) {
|
|
54
|
+
super(options);
|
|
55
|
+
this.compilerOptions = compilerOptions || {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override async buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]> {
|
|
59
|
+
const { default: pageComponent } = await this.importPageFile(pagePath);
|
|
60
|
+
const config = pageComponent.config;
|
|
61
|
+
const components: Partial<EcoComponent>[] = [];
|
|
62
|
+
|
|
63
|
+
const resolvedLayout = config?.layout;
|
|
64
|
+
|
|
65
|
+
if (resolvedLayout?.config?.dependencies) {
|
|
66
|
+
components.push({ config: resolvedLayout.config });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config?.dependencies) {
|
|
70
|
+
components.push({
|
|
71
|
+
config: {
|
|
72
|
+
...config,
|
|
73
|
+
__eco: {
|
|
74
|
+
id: rapidhash(pagePath).toString(36),
|
|
75
|
+
file: pagePath,
|
|
76
|
+
integration: this.name,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return await this.resolveDependencies(components);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected override async importPageFile(file: string): Promise<
|
|
86
|
+
EcoPageFile<{
|
|
87
|
+
layout?:
|
|
88
|
+
| EcoComponent<any>
|
|
89
|
+
| {
|
|
90
|
+
config: EcoComponentConfig | undefined;
|
|
91
|
+
};
|
|
92
|
+
}>
|
|
93
|
+
> {
|
|
94
|
+
try {
|
|
95
|
+
const {
|
|
96
|
+
default: Page,
|
|
97
|
+
config,
|
|
98
|
+
getMetadata,
|
|
99
|
+
} = (await super.importPageFile(file)) as EcoPageFile<{
|
|
100
|
+
layout?:
|
|
101
|
+
| EcoComponent<any>
|
|
102
|
+
| {
|
|
103
|
+
config: EcoComponentConfig | undefined;
|
|
104
|
+
};
|
|
105
|
+
}> & {
|
|
106
|
+
config?: EcoComponentConfig;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (typeof Page !== 'function') {
|
|
110
|
+
throw new Error('MDX file must export a default function');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const resolvedLayout = config?.layout;
|
|
114
|
+
|
|
115
|
+
if (config) Page.config = config;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
default: Page,
|
|
119
|
+
layout: resolvedLayout,
|
|
120
|
+
getMetadata,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
invariant(false, `Error importing MDX file: ${error}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async render({
|
|
128
|
+
params,
|
|
129
|
+
query,
|
|
130
|
+
props,
|
|
131
|
+
locals,
|
|
132
|
+
pageLocals,
|
|
133
|
+
metadata,
|
|
134
|
+
Page,
|
|
135
|
+
HtmlTemplate,
|
|
136
|
+
Layout,
|
|
137
|
+
pageProps,
|
|
138
|
+
}: MDXIntegrationRendererOpions): Promise<RouteRendererBody> {
|
|
139
|
+
try {
|
|
140
|
+
const pageContent = await Page({ params, query, ...props, locals: pageLocals });
|
|
141
|
+
const children =
|
|
142
|
+
typeof Layout === 'function' ? await Layout({ children: pageContent, locals }) : pageContent;
|
|
143
|
+
|
|
144
|
+
const body = await HtmlTemplate({
|
|
145
|
+
metadata,
|
|
146
|
+
children,
|
|
147
|
+
pageProps: pageProps || {},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return this.DOC_TYPE + body;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw this.createRenderError('Error rendering page', error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async renderToResponse<P = Record<string, unknown>>(
|
|
157
|
+
view: EcoComponent<P>,
|
|
158
|
+
props: P,
|
|
159
|
+
ctx: RenderToResponseContext,
|
|
160
|
+
): Promise<Response> {
|
|
161
|
+
try {
|
|
162
|
+
const Layout = view.config?.layout as
|
|
163
|
+
| ((props: { children: EcoPagesElement } & Record<string, unknown>) => Promise<EcoPagesElement>)
|
|
164
|
+
| undefined;
|
|
165
|
+
|
|
166
|
+
const viewFn = view as (props: P) => Promise<EcoPagesElement>;
|
|
167
|
+
const pageContent = await viewFn(props);
|
|
168
|
+
|
|
169
|
+
let body: string;
|
|
170
|
+
if (ctx.partial) {
|
|
171
|
+
body = pageContent as string;
|
|
172
|
+
} else {
|
|
173
|
+
const children = Layout ? await Layout({ children: pageContent }) : pageContent;
|
|
174
|
+
|
|
175
|
+
const HtmlTemplate = await this.getHtmlTemplate();
|
|
176
|
+
const metadata: PageMetadataProps = view.metadata
|
|
177
|
+
? await view.metadata({
|
|
178
|
+
params: {},
|
|
179
|
+
query: {},
|
|
180
|
+
props: props as Record<string, unknown>,
|
|
181
|
+
appConfig: this.appConfig,
|
|
182
|
+
})
|
|
183
|
+
: this.appConfig.defaultMetadata;
|
|
184
|
+
|
|
185
|
+
body =
|
|
186
|
+
this.DOC_TYPE +
|
|
187
|
+
(await HtmlTemplate({
|
|
188
|
+
metadata,
|
|
189
|
+
children: children as EcoPagesElement,
|
|
190
|
+
pageProps: props as Record<string, unknown>,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return this.createHtmlResponse(body, ctx);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
throw this.createRenderError('Error rendering view', error);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Factory function to create an MDX renderer class with specific compiler options.
|
|
203
|
+
*
|
|
204
|
+
* @param compilerOptions - Compiler options for MDX compilation.
|
|
205
|
+
* @returns A new MDXRenderer class extended with the provided context.
|
|
206
|
+
*/
|
|
207
|
+
export function createMDXRenderer(compilerOptions: CompileOptions): typeof MDXRenderer {
|
|
208
|
+
return class extends MDXRenderer {
|
|
209
|
+
constructor(options: {
|
|
210
|
+
appConfig: any;
|
|
211
|
+
assetProcessingService: AssetProcessingService;
|
|
212
|
+
resolvedIntegrationDependencies: ProcessedAsset[];
|
|
213
|
+
runtimeOrigin: string;
|
|
214
|
+
}) {
|
|
215
|
+
super({
|
|
216
|
+
...options,
|
|
217
|
+
compilerOptions,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
2
|
+
import type { EcoPagesElement } from '@ecopages/core';
|
|
3
|
+
import { IntegrationPlugin, type IntegrationPluginConfig } from '@ecopages/core/plugins/integration-plugin';
|
|
4
|
+
import type { CompileOptions } from '@mdx-js/mdx';
|
|
5
|
+
import { MDXRenderer } from './mdx-renderer.js';
|
|
6
|
+
/**
|
|
7
|
+
* The name of the MDX plugin
|
|
8
|
+
*/
|
|
9
|
+
export declare const PLUGIN_NAME = "MDX";
|
|
10
|
+
export type MDXPluginConfig = Partial<Omit<IntegrationPluginConfig, 'name'>> & {
|
|
11
|
+
compilerOptions?: CompileOptions;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The MDX plugin class
|
|
15
|
+
* This plugin provides support for MDX components in Ecopages.
|
|
16
|
+
*
|
|
17
|
+
* Standalone `mdxPlugin()` is intended for non-React JSX runtimes such as
|
|
18
|
+
* `@kitajs/html`. React-backed MDX should be configured through
|
|
19
|
+
* `reactPlugin({ mdx: { enabled: true, compilerOptions: ... } })` instead.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MDXPlugin extends IntegrationPlugin<EcoPagesElement> {
|
|
22
|
+
renderer: typeof MDXRenderer;
|
|
23
|
+
private compilerOptions;
|
|
24
|
+
private mdxLoaderPlugin;
|
|
25
|
+
constructor({ compilerOptions, ...options }?: MDXPluginConfig);
|
|
26
|
+
get plugins(): EcoBuildPlugin[];
|
|
27
|
+
setup(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Factory function to create an MDX plugin instance.
|
|
31
|
+
* @param options Configuration options for the MDX plugin
|
|
32
|
+
* @returns A new MDXPlugin instance
|
|
33
|
+
*/
|
|
34
|
+
export declare function mdxPlugin(options?: MDXPluginConfig): MDXPlugin;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { IntegrationPlugin } from "@ecopages/core/plugins/integration-plugin";
|
|
2
|
+
import { deepMerge } from "@ecopages/core/utils/deep-merge";
|
|
3
|
+
import { Logger } from "@ecopages/logger";
|
|
4
|
+
import { createMdxLoaderPlugin } from "./mdx-loader-plugin.js";
|
|
5
|
+
import { createMDXRenderer, MDXRenderer } from "./mdx-renderer.js";
|
|
6
|
+
const appLogger = new Logger("[MDXPlugin]");
|
|
7
|
+
const PLUGIN_NAME = "MDX";
|
|
8
|
+
const defaultOptions = {
|
|
9
|
+
format: "detect",
|
|
10
|
+
outputFormat: "program",
|
|
11
|
+
jsxImportSource: "@kitajs/html",
|
|
12
|
+
jsxRuntime: "automatic",
|
|
13
|
+
development: process.env.NODE_ENV === "development"
|
|
14
|
+
};
|
|
15
|
+
class MDXPlugin extends IntegrationPlugin {
|
|
16
|
+
renderer;
|
|
17
|
+
compilerOptions;
|
|
18
|
+
mdxLoaderPlugin;
|
|
19
|
+
constructor({ compilerOptions, ...options } = { extensions: [".mdx"] }) {
|
|
20
|
+
super({
|
|
21
|
+
name: PLUGIN_NAME,
|
|
22
|
+
extensions: [".mdx"],
|
|
23
|
+
...options
|
|
24
|
+
});
|
|
25
|
+
const finalCompilerOptions = deepMerge({ ...defaultOptions }, compilerOptions);
|
|
26
|
+
const jsxImportSource = finalCompilerOptions.jsxImportSource;
|
|
27
|
+
if (jsxImportSource === "react" || (jsxImportSource?.startsWith("react/") ?? false)) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Standalone `mdxPlugin()` does not support React JSX runtimes. Use `reactPlugin({ mdx: { enabled: true, compilerOptions: ... } })` instead."
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
this.compilerOptions = finalCompilerOptions;
|
|
33
|
+
this.renderer = createMDXRenderer(finalCompilerOptions);
|
|
34
|
+
appLogger.debug(`MDX plugin configured with jsxImportSource: ${jsxImportSource ?? "default"}`);
|
|
35
|
+
}
|
|
36
|
+
get plugins() {
|
|
37
|
+
if (this.mdxLoaderPlugin) {
|
|
38
|
+
return [this.mdxLoaderPlugin];
|
|
39
|
+
}
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
async setup() {
|
|
43
|
+
this.mdxLoaderPlugin = createMdxLoaderPlugin(this.compilerOptions);
|
|
44
|
+
await super.setup();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function mdxPlugin(options) {
|
|
48
|
+
return new MDXPlugin(options);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
MDXPlugin,
|
|
52
|
+
PLUGIN_NAME,
|
|
53
|
+
mdxPlugin
|
|
54
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
2
|
+
import type { EcoPagesElement } from '@ecopages/core';
|
|
3
|
+
import { IntegrationPlugin, type IntegrationPluginConfig } from '@ecopages/core/plugins/integration-plugin';
|
|
4
|
+
import { deepMerge } from '@ecopages/core/utils/deep-merge';
|
|
5
|
+
import { Logger } from '@ecopages/logger';
|
|
6
|
+
import type { CompileOptions } from '@mdx-js/mdx';
|
|
7
|
+
import { createMdxLoaderPlugin } from './mdx-loader-plugin.ts';
|
|
8
|
+
import { createMDXRenderer, MDXRenderer } from './mdx-renderer.ts';
|
|
9
|
+
|
|
10
|
+
const appLogger = new Logger('[MDXPlugin]');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The name of the MDX plugin
|
|
14
|
+
*/
|
|
15
|
+
export const PLUGIN_NAME = 'MDX';
|
|
16
|
+
|
|
17
|
+
export type MDXPluginConfig = Partial<Omit<IntegrationPluginConfig, 'name'>> & {
|
|
18
|
+
compilerOptions?: CompileOptions;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const defaultOptions: CompileOptions = {
|
|
22
|
+
format: 'detect',
|
|
23
|
+
outputFormat: 'program',
|
|
24
|
+
jsxImportSource: '@kitajs/html',
|
|
25
|
+
jsxRuntime: 'automatic',
|
|
26
|
+
development: process.env.NODE_ENV === 'development',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The MDX plugin class
|
|
31
|
+
* This plugin provides support for MDX components in Ecopages.
|
|
32
|
+
*
|
|
33
|
+
* Standalone `mdxPlugin()` is intended for non-React JSX runtimes such as
|
|
34
|
+
* `@kitajs/html`. React-backed MDX should be configured through
|
|
35
|
+
* `reactPlugin({ mdx: { enabled: true, compilerOptions: ... } })` instead.
|
|
36
|
+
*/
|
|
37
|
+
export class MDXPlugin extends IntegrationPlugin<EcoPagesElement> {
|
|
38
|
+
renderer: typeof MDXRenderer;
|
|
39
|
+
private compilerOptions: CompileOptions;
|
|
40
|
+
private mdxLoaderPlugin: EcoBuildPlugin | undefined;
|
|
41
|
+
|
|
42
|
+
constructor({ compilerOptions, ...options }: MDXPluginConfig = { extensions: ['.mdx'] }) {
|
|
43
|
+
super({
|
|
44
|
+
name: PLUGIN_NAME,
|
|
45
|
+
extensions: ['.mdx'],
|
|
46
|
+
...options,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const finalCompilerOptions = deepMerge({ ...defaultOptions }, compilerOptions);
|
|
50
|
+
const jsxImportSource = finalCompilerOptions.jsxImportSource;
|
|
51
|
+
|
|
52
|
+
if (jsxImportSource === 'react' || (jsxImportSource?.startsWith('react/') ?? false)) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'Standalone `mdxPlugin()` does not support React JSX runtimes. Use `reactPlugin({ mdx: { enabled: true, compilerOptions: ... } })` instead.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.compilerOptions = finalCompilerOptions;
|
|
59
|
+
this.renderer = createMDXRenderer(finalCompilerOptions);
|
|
60
|
+
|
|
61
|
+
appLogger.debug(`MDX plugin configured with jsxImportSource: ${jsxImportSource ?? 'default'}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override get plugins(): EcoBuildPlugin[] {
|
|
65
|
+
if (this.mdxLoaderPlugin) {
|
|
66
|
+
return [this.mdxLoaderPlugin];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override async setup(): Promise<void> {
|
|
73
|
+
this.mdxLoaderPlugin = createMdxLoaderPlugin(this.compilerOptions);
|
|
74
|
+
await super.setup();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Factory function to create an MDX plugin instance.
|
|
80
|
+
* @param options Configuration options for the MDX plugin
|
|
81
|
+
* @returns A new MDXPlugin instance
|
|
82
|
+
*/
|
|
83
|
+
export function mdxPlugin(options?: MDXPluginConfig): MDXPlugin {
|
|
84
|
+
return new MDXPlugin(options);
|
|
85
|
+
}
|