@ecopages/mdx 0.2.0-alpha.1 → 0.2.0-alpha.10

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 CHANGED
@@ -8,24 +8,26 @@ All notable changes to `@ecopages/mdx` are documented here.
8
8
 
9
9
  ### Features
10
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.
11
+ - Made the MDX integration standalone so MDX routes can server-render without requiring the React integration.
12
+ - Switched MDX compilation to the async pipeline for async remark and rehype plugin support.
13
+
14
+ ### Bug Fixes
15
+
16
+ - Fixed configured `.md` extensions to compile as MDX instead of plain markdown so top-level `import` and `export` statements work when `.md` is opted in.
17
+ - Fixed loader registration to respect configured extensions so standalone MDX no longer hijacks React `.mdx` pages during shared development and build flows.
18
+ - Fixed native Node startup compatibility by using Node-safe `source-map` interop.
14
19
 
15
20
  ### Refactoring
16
21
 
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`).
22
+ - Removed the React-specific renderer and HMR code from the package and aligned MDX with the unified orchestration pipeline.
21
23
 
22
24
  ### Documentation
23
25
 
24
- - README updated to clarify the integration is now usable without the React integration.
26
+ - Updated the README for standalone MDX registration and the current integration setup.
25
27
 
26
28
  ---
27
29
 
28
30
  ## Migration Notes
29
31
 
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.
32
+ - Register `@ecopages/mdx` and `@ecopages/react` separately when you want MDX server rendering together with React client hydration.
33
+ - The previous React-specific MDX path, including `useReact` and the React-specific HMR hooks, has been removed.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
- # Ecopages MDX Integration Plugin
1
+ # @ecopages/mdx
2
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.
3
+ Integration plugin for standalone MDX support in Ecopages, specifically designed for non-React JSX runtimes (such as `@kitajs/html`). It configures the MDX compiler to process `.mdx` routes natively.
4
4
 
5
- ## Install
5
+ ## Installation
6
6
 
7
7
  ```bash
8
8
  bunx jsr add @ecopages/mdx
@@ -10,10 +10,10 @@ bunx jsr add @ecopages/mdx
10
10
 
11
11
  ## Usage
12
12
 
13
- Integrating MDX into your Ecopages project is made simple. Import and apply the `mdxPlugin` in your Ecopages configuration as demonstrated below:
13
+ Import and apply the `mdxPlugin` in your `eco.config.ts`:
14
14
 
15
15
  ```ts
16
- import { ConfigBuilder } from '@ecopages/core';
16
+ import { ConfigBuilder } from '@ecopages/core/config-builder';
17
17
  import { mdxPlugin } from '@ecopages/mdx';
18
18
 
19
19
  const config = await new ConfigBuilder()
@@ -29,11 +29,12 @@ By default, the standalone plugin uses:
29
29
  - `jsxImportSource: '@kitajs/html'`
30
30
  - `jsxRuntime: 'automatic'`
31
31
 
32
- You can override MDX compiler options, but React runtimes are intentionally not supported here.
32
+ > [!WARNING]
33
+ > React runtimes are intentionally rejected by this standalone plugin.
33
34
 
34
- ## Using MDX with React Router
35
+ ## Using MDX with React
35
36
 
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
+ If you are using `@ecopages/react` and building a full React application, **do not** use this standalone MDX plugin. Instead, enable MDX directly within the React plugin configuration to ensure unified hydration, client-side routing, and HMR:
37
38
 
38
39
  ```ts
39
40
  import { reactPlugin } from '@ecopages/react';
@@ -44,9 +45,3 @@ reactPlugin({
44
45
  mdx: { enabled: true },
45
46
  });
46
47
  ```
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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/mdx",
3
- "version": "0.2.0-alpha.1",
3
+ "version": "0.2.0-alpha.10",
4
4
  "description": "MDX plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -37,7 +37,7 @@
37
37
  "directory": "packages/integrations/mdx"
38
38
  },
39
39
  "peerDependencies": {
40
- "@ecopages/core": "0.2.0-alpha.1",
40
+ "@ecopages/core": "0.2.0-alpha.10",
41
41
  "@kitajs/html": "^4.1.0",
42
42
  "@mdx-js/mdx": "^3.1.0"
43
43
  },
@@ -1,8 +1,15 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { compile } from "@mdx-js/mdx";
4
- import { SourceMapGenerator } from "source-map";
4
+ import sourceMap from "source-map";
5
5
  import { VFile } from "vfile";
6
+ function resolveCompileFormat(filePath, compilerOptions) {
7
+ const configuredFormat = compilerOptions?.format;
8
+ if (configuredFormat && configuredFormat !== "detect") {
9
+ return configuredFormat;
10
+ }
11
+ return path.extname(filePath).toLowerCase() === ".md" ? "mdx" : configuredFormat;
12
+ }
6
13
  function createMdxLoaderPlugin(compilerOptions) {
7
14
  const mdxExtensions = compilerOptions?.mdxExtensions ?? [".mdx"];
8
15
  const mdExtensions = compilerOptions?.mdExtensions ?? [".md"];
@@ -18,13 +25,14 @@ function createMdxLoaderPlugin(compilerOptions) {
18
25
  const file = new VFile({ path: filePath, value: source });
19
26
  const compiled = await compile(file, {
20
27
  ...compilerOptions,
21
- SourceMapGenerator
28
+ format: resolveCompileFormat(filePath, compilerOptions),
29
+ SourceMapGenerator: sourceMap.SourceMapGenerator
22
30
  });
23
- const sourceMap = compiled.map ? `
31
+ const inlineSourceMap = compiled.map ? `
24
32
  //# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(compiled.map)).toString("base64")}
25
33
  ` : "";
26
34
  return {
27
- contents: `${String(compiled.value)}${sourceMap}`,
35
+ contents: `${String(compiled.value)}${inlineSourceMap}`,
28
36
  loader: compilerOptions?.jsx ? "jsx" : "js",
29
37
  resolveDir: path.dirname(args.path)
30
38
  };
@@ -24,6 +24,18 @@ export declare class MDXPlugin extends IntegrationPlugin<EcoPagesElement> {
24
24
  private mdxLoaderPlugin;
25
25
  constructor({ compilerOptions, ...options }?: MDXPluginConfig);
26
26
  get plugins(): EcoBuildPlugin[];
27
+ /**
28
+ * Materializes the MDX loader once so config-time sealing and runtime setup
29
+ * can share the same loader instance.
30
+ */
31
+ private ensureLoaderPlugin;
32
+ /**
33
+ * Prepares the MDX loader contribution before config build seals the manifest.
34
+ */
35
+ prepareBuildContributions(): Promise<void>;
36
+ /**
37
+ * Runs runtime-only MDX setup after build contributions are already prepared.
38
+ */
27
39
  setup(): Promise<void>;
28
40
  }
29
41
  /**
package/src/mdx.plugin.js CHANGED
@@ -12,6 +12,18 @@ const defaultOptions = {
12
12
  jsxRuntime: "automatic",
13
13
  development: process.env.NODE_ENV === "development"
14
14
  };
15
+ function splitMarkdownExtensions(extensions) {
16
+ const mdExtensions = [];
17
+ const mdxExtensions = [];
18
+ for (const extension of extensions) {
19
+ if (extension === ".md") {
20
+ mdExtensions.push(extension);
21
+ continue;
22
+ }
23
+ mdxExtensions.push(extension);
24
+ }
25
+ return { mdExtensions, mdxExtensions };
26
+ }
15
27
  class MDXPlugin extends IntegrationPlugin {
16
28
  renderer;
17
29
  compilerOptions;
@@ -22,7 +34,15 @@ class MDXPlugin extends IntegrationPlugin {
22
34
  extensions: [".mdx"],
23
35
  ...options
24
36
  });
25
- const finalCompilerOptions = deepMerge({ ...defaultOptions }, compilerOptions);
37
+ const { mdExtensions, mdxExtensions } = splitMarkdownExtensions(this.extensions);
38
+ const finalCompilerOptions = deepMerge(
39
+ {
40
+ ...defaultOptions,
41
+ mdxExtensions,
42
+ mdExtensions
43
+ },
44
+ compilerOptions
45
+ );
26
46
  const jsxImportSource = finalCompilerOptions.jsxImportSource;
27
47
  if (jsxImportSource === "react" || (jsxImportSource?.startsWith("react/") ?? false)) {
28
48
  throw new Error(
@@ -39,8 +59,27 @@ class MDXPlugin extends IntegrationPlugin {
39
59
  }
40
60
  return [];
41
61
  }
42
- async setup() {
62
+ /**
63
+ * Materializes the MDX loader once so config-time sealing and runtime setup
64
+ * can share the same loader instance.
65
+ */
66
+ ensureLoaderPlugin() {
67
+ if (this.mdxLoaderPlugin) {
68
+ return;
69
+ }
43
70
  this.mdxLoaderPlugin = createMdxLoaderPlugin(this.compilerOptions);
71
+ }
72
+ /**
73
+ * Prepares the MDX loader contribution before config build seals the manifest.
74
+ */
75
+ async prepareBuildContributions() {
76
+ this.ensureLoaderPlugin();
77
+ }
78
+ /**
79
+ * Runs runtime-only MDX setup after build contributions are already prepared.
80
+ */
81
+ async setup() {
82
+ this.ensureLoaderPlugin();
44
83
  await super.setup();
45
84
  }
46
85
  }
@@ -1,40 +0,0 @@
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
- }
@@ -1,221 +0,0 @@
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
- }
package/src/mdx.plugin.ts DELETED
@@ -1,85 +0,0 @@
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
- }