@ecopages/mdx 0.2.0-alpha.8 → 0.2.0-beta.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 CHANGED
@@ -1,13 +1,15 @@
1
1
  # @ecopages/mdx
2
2
 
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.
3
+ Integration plugin for standalone MDX support in Ecopages for non-React JSX runtimes such as `@kitajs/html`. Use it when MDX should render directly on the server without React hydration.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- bunx jsr add @ecopages/mdx
8
+ bun add @ecopages/mdx @kitajs/html @mdx-js/mdx
9
9
  ```
10
10
 
11
+ `@kitajs/html` and `@mdx-js/mdx` are required peer dependencies for this package.
12
+
11
13
  ## Usage
12
14
 
13
15
  Import and apply the `mdxPlugin` in your `eco.config.ts`:
@@ -29,9 +31,52 @@ By default, the standalone plugin uses:
29
31
  - `jsxImportSource: '@kitajs/html'`
30
32
  - `jsxRuntime: 'automatic'`
31
33
 
34
+ ## What This Integration Owns
35
+
36
+ - `.mdx` route files.
37
+ - Optional `.md` routes when you opt them into `extensions`.
38
+ - MDX compilation against a non-React JSX runtime.
39
+
40
+ ## Configure Markdown Extensions
41
+
42
+ Use `extensions` when both `.mdx` and `.md` files should run through the MDX loader.
43
+
44
+ ```ts
45
+ import { mdxPlugin } from '@ecopages/mdx';
46
+
47
+ mdxPlugin({
48
+ extensions: ['.mdx', '.md'],
49
+ });
50
+ ```
51
+
52
+ ## Compiler Options
53
+
54
+ Pass `compilerOptions` to add remark, rehype, or recma plugins while keeping the non-React JSX runtime managed by the integration.
55
+
56
+ ```ts
57
+ import { mdxPlugin } from '@ecopages/mdx';
58
+
59
+ mdxPlugin({
60
+ compilerOptions: {
61
+ remarkPlugins: [],
62
+ rehypePlugins: [],
63
+ },
64
+ });
65
+ ```
66
+
32
67
  > [!WARNING]
33
68
  > React runtimes are intentionally rejected by this standalone plugin.
34
69
 
70
+ ## Mixed Rendering
71
+
72
+ Standalone MDX can own the page shell or nested MDX foreign subtrees in a mixed-renderer app. When another integration reaches an MDX-owned foreign child, Ecopages hands that foreign subtree back to the MDX renderer so the MDX runtime can finish serialization before the outer renderer resumes.
73
+
74
+ Important:
75
+
76
+ - Components that may render foreign children must declare those children in `config.dependencies.components`.
77
+ - Ecopages validates mixed-renderer ownership from declared dependencies during render preparation rather than inferring every foreign subtree from rendered HTML alone.
78
+ - Standalone MDX keeps its own page normalization and non-React JSX runtime behavior.
79
+
35
80
  ## Using MDX with React
36
81
 
37
82
  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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/mdx",
3
- "version": "0.2.0-alpha.8",
3
+ "version": "0.2.0-beta.0",
4
4
  "description": "MDX plugin for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -37,12 +37,12 @@
37
37
  "directory": "packages/integrations/mdx"
38
38
  },
39
39
  "peerDependencies": {
40
- "@ecopages/core": "0.2.0-alpha.8",
40
+ "@ecopages/core": "0.2.0-beta.0",
41
41
  "@kitajs/html": "^4.1.0",
42
42
  "@mdx-js/mdx": "^3.1.0"
43
43
  },
44
44
  "dependencies": {
45
- "@ecopages/logger": "^0.2.2",
45
+ "@ecopages/logger": "^0.2.3",
46
46
  "source-map": "^0.7.6",
47
47
  "vfile": "^6.0.3"
48
48
  }
@@ -1,3 +1,3 @@
1
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
1
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
2
2
  import { type CompileOptions } from '@mdx-js/mdx';
3
3
  export declare function createMdxLoaderPlugin(compilerOptions?: CompileOptions): EcoBuildPlugin;
@@ -2,50 +2,27 @@
2
2
  * This module contains the MDX renderer
3
3
  * @module
4
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';
5
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, EcoPagesElement, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
6
+ import { IntegrationRenderer, type PageBrowserGraphContribution, type PageBrowserGraphContributionContext, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
8
7
  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
- };
8
+ import type { MDXRendererOptions } from './mdx.types.js';
9
+ export type { MDXFile, MDXRendererConfig, MDXRendererOptions } from './mdx.types.js';
17
10
  /**
18
11
  * Options for the MDX renderer
19
12
  */
20
- interface MDXIntegrationRendererOpions<C = EcoPagesElement> extends IntegrationRendererRenderOptions<C> {
13
+ interface MDXIntegrationRendererOptions<C = EcoPagesElement> extends IntegrationRendererRenderOptions<C> {
21
14
  }
22
15
  /**
23
16
  * A renderer for the MDX integration.
24
17
  */
25
18
  export declare class MDXRenderer extends IntegrationRenderer<EcoPagesElement> {
26
19
  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>;
20
+ readonly compilerOptions: CompileOptions;
21
+ private isFunctionComponent;
22
+ constructor({ mdxConfig, ...options }: MDXRendererOptions);
23
+ protected collectPageBrowserGraphContribution(context: PageBrowserGraphContributionContext): Promise<PageBrowserGraphContribution>;
24
+ protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(_file: string, pageModule: TPageModule): TPageModule;
25
+ renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
26
+ render({ params, query, props, locals, pageLocals, metadata, Page, HtmlTemplate, Layout, pageProps, }: MDXIntegrationRendererOptions): Promise<RouteRendererBody>;
42
27
  renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
43
28
  }
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 {};
@@ -1,19 +1,22 @@
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";
1
+ import { assertIntegrationInvariant } from "@ecopages/core/plugins/integration-plugin";
2
+ import {
3
+ IntegrationRenderer
4
+ } from "@ecopages/core/route-renderer/integration-renderer";
5
+ import { MDX_PLUGIN_NAME } from "./mdx.constants.js";
4
6
  import { rapidhash } from "@ecopages/core/hash";
5
7
  class MDXRenderer extends IntegrationRenderer {
6
- name = PLUGIN_NAME;
8
+ name = MDX_PLUGIN_NAME;
7
9
  compilerOptions;
8
- constructor({
9
- compilerOptions,
10
- ...options
11
- }) {
10
+ isFunctionComponent(component) {
11
+ return typeof component === "function";
12
+ }
13
+ constructor({ mdxConfig, ...options }) {
12
14
  super(options);
13
- this.compilerOptions = compilerOptions || {};
15
+ this.compilerOptions = mdxConfig?.compilerOptions ?? {};
14
16
  }
15
- async buildRouteRenderAssets(pagePath) {
16
- const { default: pageComponent } = await this.importPageFile(pagePath);
17
+ async collectPageBrowserGraphContribution(context) {
18
+ const { file: pagePath, pageModule } = context;
19
+ const { default: pageComponent } = pageModule;
17
20
  const config = pageComponent.config;
18
21
  const components = [];
19
22
  const resolvedLayout = config?.layout;
@@ -32,28 +35,32 @@ class MDXRenderer extends IntegrationRenderer {
32
35
  }
33
36
  });
34
37
  }
35
- return await this.resolveDependencies(components);
38
+ return { assets: await this.resolveDependencies(components) };
36
39
  }
37
- async importPageFile(file) {
40
+ normalizeImportedPageFile(_file, pageModule) {
38
41
  try {
39
- const {
40
- default: Page,
41
- config,
42
- getMetadata
43
- } = await super.importPageFile(file);
42
+ const mdxModule = pageModule;
43
+ const { default: Page, config, getMetadata } = mdxModule;
44
44
  if (typeof Page !== "function") {
45
45
  throw new Error("MDX file must export a default function");
46
46
  }
47
47
  const resolvedLayout = config?.layout;
48
48
  if (config) Page.config = config;
49
49
  return {
50
+ ...pageModule,
50
51
  default: Page,
51
52
  layout: resolvedLayout,
52
53
  getMetadata
53
54
  };
54
55
  } catch (error) {
55
- invariant(false, `Error importing MDX file: ${error}`);
56
+ assertIntegrationInvariant(false, `Error importing MDX file: ${error}`);
57
+ }
58
+ }
59
+ async renderComponent(input) {
60
+ if (!this.isFunctionComponent(input.component)) {
61
+ throw new TypeError("MDX renderer expected a callable component.");
56
62
  }
63
+ return this.renderStringComponentWithQueuedForeignSubtrees(input, input.component);
57
64
  }
58
65
  async render({
59
66
  params,
@@ -68,58 +75,36 @@ class MDXRenderer extends IntegrationRenderer {
68
75
  pageProps
69
76
  }) {
70
77
  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({
78
+ return await this.renderPageWithDocumentShell({
79
+ page: {
80
+ component: Page,
81
+ props: { params, query, ...props, locals: pageLocals }
82
+ },
83
+ layout: Layout ? {
84
+ component: Layout,
85
+ props: locals ? { locals } : {}
86
+ } : void 0,
87
+ htmlTemplate: HtmlTemplate,
74
88
  metadata,
75
- children,
76
89
  pageProps: pageProps || {}
77
90
  });
78
- return this.DOC_TYPE + body;
79
91
  } catch (error) {
80
92
  throw this.createRenderError("Error rendering page", error);
81
93
  }
82
94
  }
83
95
  async renderToResponse(view, props, ctx) {
84
96
  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);
97
+ return await this.renderViewWithDocumentShell({
98
+ view,
99
+ props,
100
+ ctx,
101
+ layout: view.config?.layout
102
+ });
107
103
  } catch (error) {
108
104
  throw this.createRenderError("Error rendering view", error);
109
105
  }
110
106
  }
111
107
  }
112
- function createMDXRenderer(compilerOptions) {
113
- return class extends MDXRenderer {
114
- constructor(options) {
115
- super({
116
- ...options,
117
- compilerOptions
118
- });
119
- }
120
- };
121
- }
122
108
  export {
123
- MDXRenderer,
124
- createMDXRenderer
109
+ MDXRenderer
125
110
  };
@@ -0,0 +1 @@
1
+ export declare const MDX_PLUGIN_NAME = "MDX";
@@ -0,0 +1,4 @@
1
+ const MDX_PLUGIN_NAME = "MDX";
2
+ export {
3
+ MDX_PLUGIN_NAME
4
+ };
@@ -1,15 +1,12 @@
1
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
2
1
  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';
2
+ import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
5
3
  import { MDXRenderer } from './mdx-renderer.js';
4
+ import type { MDXPluginConfig } from './mdx.types.js';
5
+ export type { MDXPluginConfig, MDXRendererConfig, MDXRendererOptions } from './mdx.types.js';
6
6
  /**
7
7
  * The name of the MDX plugin
8
8
  */
9
9
  export declare const PLUGIN_NAME = "MDX";
10
- export type MDXPluginConfig = Partial<Omit<IntegrationPluginConfig, 'name'>> & {
11
- compilerOptions?: CompileOptions;
12
- };
13
10
  /**
14
11
  * The MDX plugin class
15
12
  * This plugin provides support for MDX components in Ecopages.
@@ -20,9 +17,12 @@ export type MDXPluginConfig = Partial<Omit<IntegrationPluginConfig, 'name'>> & {
20
17
  */
21
18
  export declare class MDXPlugin extends IntegrationPlugin<EcoPagesElement> {
22
19
  renderer: typeof MDXRenderer;
23
- private compilerOptions;
20
+ private readonly compilerOptions;
24
21
  private mdxLoaderPlugin;
25
22
  constructor({ compilerOptions, ...options }?: MDXPluginConfig);
23
+ initializeRenderer(options?: {
24
+ rendererModules?: unknown;
25
+ }): MDXRenderer;
26
26
  get plugins(): EcoBuildPlugin[];
27
27
  /**
28
28
  * Materializes the MDX loader once so config-time sealing and runtime setup
package/src/mdx.plugin.js CHANGED
@@ -1,10 +1,13 @@
1
- import { IntegrationPlugin } from "@ecopages/core/plugins/integration-plugin";
2
- import { deepMerge } from "@ecopages/core/utils/deep-merge";
1
+ import {
2
+ IntegrationPlugin,
3
+ mergeIntegrationOptions
4
+ } from "@ecopages/core/plugins/integration-plugin";
3
5
  import { Logger } from "@ecopages/logger";
4
6
  import { createMdxLoaderPlugin } from "./mdx-loader-plugin.js";
5
- import { createMDXRenderer, MDXRenderer } from "./mdx-renderer.js";
7
+ import { MDX_PLUGIN_NAME } from "./mdx.constants.js";
8
+ import { MDXRenderer } from "./mdx-renderer.js";
6
9
  const appLogger = new Logger("[MDXPlugin]");
7
- const PLUGIN_NAME = "MDX";
10
+ const PLUGIN_NAME = MDX_PLUGIN_NAME;
8
11
  const defaultOptions = {
9
12
  format: "detect",
10
13
  outputFormat: "program",
@@ -25,7 +28,7 @@ function splitMarkdownExtensions(extensions) {
25
28
  return { mdExtensions, mdxExtensions };
26
29
  }
27
30
  class MDXPlugin extends IntegrationPlugin {
28
- renderer;
31
+ renderer = MDXRenderer;
29
32
  compilerOptions;
30
33
  mdxLoaderPlugin;
31
34
  constructor({ compilerOptions, ...options } = { extensions: [".mdx"] }) {
@@ -35,7 +38,7 @@ class MDXPlugin extends IntegrationPlugin {
35
38
  ...options
36
39
  });
37
40
  const { mdExtensions, mdxExtensions } = splitMarkdownExtensions(this.extensions);
38
- const finalCompilerOptions = deepMerge(
41
+ const finalCompilerOptions = mergeIntegrationOptions(
39
42
  {
40
43
  ...defaultOptions,
41
44
  mdxExtensions,
@@ -50,9 +53,17 @@ class MDXPlugin extends IntegrationPlugin {
50
53
  );
51
54
  }
52
55
  this.compilerOptions = finalCompilerOptions;
53
- this.renderer = createMDXRenderer(finalCompilerOptions);
54
56
  appLogger.debug(`MDX plugin configured with jsxImportSource: ${jsxImportSource ?? "default"}`);
55
57
  }
58
+ initializeRenderer(options) {
59
+ const renderer = new this.renderer({
60
+ ...this.createRendererOptions(options),
61
+ mdxConfig: {
62
+ compilerOptions: this.compilerOptions
63
+ }
64
+ });
65
+ return this.attachRendererRuntimeServices(renderer);
66
+ }
56
67
  get plugins() {
57
68
  if (this.mdxLoaderPlugin) {
58
69
  return [this.mdxLoaderPlugin];
@@ -0,0 +1,26 @@
1
+ import type { EcoComponent, EcoComponentConfig, EcoPagesAppConfig, GetMetadata } from '@ecopages/core';
2
+ import type { IntegrationPluginConfig } from '@ecopages/core/plugins/integration-plugin';
3
+ import type { AssetProcessingService, ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
4
+ import type { CompileOptions } from '@mdx-js/mdx';
5
+ export type MDXPluginConfig = Partial<Omit<IntegrationPluginConfig, 'name'>> & {
6
+ compilerOptions?: CompileOptions;
7
+ };
8
+ export type MDXRendererConfig = {
9
+ compilerOptions?: CompileOptions;
10
+ };
11
+ export type MDXRendererOptions = {
12
+ appConfig: EcoPagesAppConfig;
13
+ assetProcessingService: AssetProcessingService;
14
+ resolvedIntegrationDependencies: ProcessedAsset[];
15
+ rendererModules?: unknown;
16
+ runtimeOrigin: string;
17
+ mdxConfig?: MDXRendererConfig;
18
+ };
19
+ /**
20
+ * A structure representing an MDX file.
21
+ */
22
+ export type MDXFile = {
23
+ default: EcoComponent;
24
+ config?: EcoComponentConfig;
25
+ getMetadata: GetMetadata;
26
+ };
File without changes
package/CHANGELOG.md DELETED
@@ -1,33 +0,0 @@
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
- - 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.
19
-
20
- ### Refactoring
21
-
22
- - Removed the React-specific renderer and HMR code from the package and aligned MDX with the unified orchestration pipeline.
23
-
24
- ### Documentation
25
-
26
- - Updated the README for standalone MDX registration and the current integration setup.
27
-
28
- ---
29
-
30
- ## Migration Notes
31
-
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.
@@ -1,63 +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 sourceMap from 'source-map';
6
- import { VFile } from 'vfile';
7
-
8
- /**
9
- * Resolves the MDX parser mode for a source file.
10
- *
11
- * Files with a `.md` extension must be forced into `mdx` mode when the caller
12
- * explicitly opts them into the MDX pipeline. Leaving the compiler in `detect`
13
- * mode would treat `.md` files as plain markdown, causing top-level ESM such as
14
- * `import` and `export` to render as text instead of being compiled.
15
- *
16
- * @param filePath Absolute or relative source file path.
17
- * @param compilerOptions User-provided MDX compiler options.
18
- * @returns The compile format that should be passed to `@mdx-js/mdx`.
19
- */
20
- function resolveCompileFormat(filePath: string, compilerOptions?: CompileOptions): CompileOptions['format'] {
21
- const configuredFormat = compilerOptions?.format;
22
-
23
- if (configuredFormat && configuredFormat !== 'detect') {
24
- return configuredFormat;
25
- }
26
-
27
- return path.extname(filePath).toLowerCase() === '.md' ? 'mdx' : configuredFormat;
28
- }
29
-
30
- export function createMdxLoaderPlugin(compilerOptions?: CompileOptions): EcoBuildPlugin {
31
- const mdxExtensions = compilerOptions?.mdxExtensions ?? ['.mdx'];
32
- const mdExtensions = compilerOptions?.mdExtensions ?? ['.md'];
33
- const allExtensions = [...mdxExtensions, ...mdExtensions];
34
- const escapedExts = allExtensions.map((ext) => ext.replace('.', '\\.'));
35
- const filter = new RegExp(`(${escapedExts.join('|')})(\\?.*)?$`);
36
-
37
- return {
38
- name: 'mdx-loader',
39
- setup(build) {
40
- build.onLoad({ filter }, async (args) => {
41
- const filePath = args.path.includes('?') ? args.path.split('?')[0] : args.path;
42
- const source = readFileSync(filePath, 'utf-8');
43
- const file = new VFile({ path: filePath, value: source });
44
-
45
- const compiled = await compile(file, {
46
- ...compilerOptions,
47
- format: resolveCompileFormat(filePath, compilerOptions),
48
- SourceMapGenerator: sourceMap.SourceMapGenerator,
49
- });
50
-
51
- const inlineSourceMap = compiled.map
52
- ? `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(compiled.map)).toString('base64')}\n`
53
- : '';
54
-
55
- return {
56
- contents: `${String(compiled.value)}${inlineSourceMap}`,
57
- loader: compilerOptions?.jsx ? 'jsx' : 'js',
58
- resolveDir: path.dirname(args.path),
59
- };
60
- });
61
- },
62
- };
63
- }
@@ -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,141 +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
- * Splits configured markdown extensions into the two buckets understood by the
31
- * MDX loader.
32
- *
33
- * `.mdx` remains the native MDX extension list. `.md` is special: it is only
34
- * treated as MDX when a caller explicitly opts it into the pipeline, so we keep
35
- * it separate rather than hiding that behavior inside a pair of constructor
36
- * filters.
37
- */
38
- function splitMarkdownExtensions(extensions: string[]): Pick<CompileOptions, 'mdExtensions' | 'mdxExtensions'> {
39
- const mdExtensions: string[] = [];
40
- const mdxExtensions: string[] = [];
41
-
42
- for (const extension of extensions) {
43
- if (extension === '.md') {
44
- mdExtensions.push(extension);
45
- continue;
46
- }
47
-
48
- mdxExtensions.push(extension);
49
- }
50
-
51
- return { mdExtensions, mdxExtensions };
52
- }
53
-
54
- /**
55
- * The MDX plugin class
56
- * This plugin provides support for MDX components in Ecopages.
57
- *
58
- * Standalone `mdxPlugin()` is intended for non-React JSX runtimes such as
59
- * `@kitajs/html`. React-backed MDX should be configured through
60
- * `reactPlugin({ mdx: { enabled: true, compilerOptions: ... } })` instead.
61
- */
62
- export class MDXPlugin extends IntegrationPlugin<EcoPagesElement> {
63
- renderer: typeof MDXRenderer;
64
- private compilerOptions: CompileOptions;
65
- private mdxLoaderPlugin: EcoBuildPlugin | undefined;
66
-
67
- constructor({ compilerOptions, ...options }: MDXPluginConfig = { extensions: ['.mdx'] }) {
68
- super({
69
- name: PLUGIN_NAME,
70
- extensions: ['.mdx'],
71
- ...options,
72
- });
73
-
74
- const { mdExtensions, mdxExtensions } = splitMarkdownExtensions(this.extensions);
75
-
76
- const finalCompilerOptions = deepMerge(
77
- {
78
- ...defaultOptions,
79
- mdxExtensions,
80
- mdExtensions,
81
- },
82
- compilerOptions,
83
- );
84
- const jsxImportSource = finalCompilerOptions.jsxImportSource;
85
-
86
- if (jsxImportSource === 'react' || (jsxImportSource?.startsWith('react/') ?? false)) {
87
- throw new Error(
88
- 'Standalone `mdxPlugin()` does not support React JSX runtimes. Use `reactPlugin({ mdx: { enabled: true, compilerOptions: ... } })` instead.',
89
- );
90
- }
91
-
92
- this.compilerOptions = finalCompilerOptions;
93
- this.renderer = createMDXRenderer(finalCompilerOptions);
94
-
95
- appLogger.debug(`MDX plugin configured with jsxImportSource: ${jsxImportSource ?? 'default'}`);
96
- }
97
-
98
- override get plugins(): EcoBuildPlugin[] {
99
- if (this.mdxLoaderPlugin) {
100
- return [this.mdxLoaderPlugin];
101
- }
102
-
103
- return [];
104
- }
105
-
106
- /**
107
- * Materializes the MDX loader once so config-time sealing and runtime setup
108
- * can share the same loader instance.
109
- */
110
- private ensureLoaderPlugin(): void {
111
- if (this.mdxLoaderPlugin) {
112
- return;
113
- }
114
-
115
- this.mdxLoaderPlugin = createMdxLoaderPlugin(this.compilerOptions);
116
- }
117
-
118
- /**
119
- * Prepares the MDX loader contribution before config build seals the manifest.
120
- */
121
- override async prepareBuildContributions(): Promise<void> {
122
- this.ensureLoaderPlugin();
123
- }
124
-
125
- /**
126
- * Runs runtime-only MDX setup after build contributions are already prepared.
127
- */
128
- override async setup(): Promise<void> {
129
- this.ensureLoaderPlugin();
130
- await super.setup();
131
- }
132
- }
133
-
134
- /**
135
- * Factory function to create an MDX plugin instance.
136
- * @param options Configuration options for the MDX plugin
137
- * @returns A new MDXPlugin instance
138
- */
139
- export function mdxPlugin(options?: MDXPluginConfig): MDXPlugin {
140
- return new MDXPlugin(options);
141
- }