astro-xmdx 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/index.ts +8 -0
  2. package/package.json +80 -0
  3. package/src/constants.ts +52 -0
  4. package/src/index.ts +150 -0
  5. package/src/pipeline/index.ts +38 -0
  6. package/src/pipeline/orchestrator.test.ts +324 -0
  7. package/src/pipeline/orchestrator.ts +121 -0
  8. package/src/pipeline/pipe.test.ts +251 -0
  9. package/src/pipeline/pipe.ts +70 -0
  10. package/src/pipeline/types.ts +59 -0
  11. package/src/plugins.test.ts +274 -0
  12. package/src/presets/index.ts +225 -0
  13. package/src/transforms/blocks-to-jsx.test.ts +590 -0
  14. package/src/transforms/blocks-to-jsx.ts +617 -0
  15. package/src/transforms/expressive-code.test.ts +274 -0
  16. package/src/transforms/expressive-code.ts +147 -0
  17. package/src/transforms/index.test.ts +143 -0
  18. package/src/transforms/index.ts +100 -0
  19. package/src/transforms/inject-components.test.ts +406 -0
  20. package/src/transforms/inject-components.ts +184 -0
  21. package/src/transforms/shiki.test.ts +289 -0
  22. package/src/transforms/shiki.ts +312 -0
  23. package/src/types.ts +92 -0
  24. package/src/utils/config.test.ts +252 -0
  25. package/src/utils/config.ts +146 -0
  26. package/src/utils/frontmatter.ts +33 -0
  27. package/src/utils/imports.test.ts +518 -0
  28. package/src/utils/imports.ts +201 -0
  29. package/src/utils/mdx-detection.test.ts +41 -0
  30. package/src/utils/mdx-detection.ts +209 -0
  31. package/src/utils/paths.test.ts +206 -0
  32. package/src/utils/paths.ts +92 -0
  33. package/src/utils/validation.test.ts +60 -0
  34. package/src/utils/validation.ts +15 -0
  35. package/src/vite-plugin/binding-loader.ts +81 -0
  36. package/src/vite-plugin/directive-rewriter.test.ts +331 -0
  37. package/src/vite-plugin/directive-rewriter.ts +272 -0
  38. package/src/vite-plugin/esbuild-pool.ts +173 -0
  39. package/src/vite-plugin/index.ts +37 -0
  40. package/src/vite-plugin/jsx-module.ts +106 -0
  41. package/src/vite-plugin/mdx-wrapper.ts +328 -0
  42. package/src/vite-plugin/normalize-config.test.ts +78 -0
  43. package/src/vite-plugin/normalize-config.ts +29 -0
  44. package/src/vite-plugin/shiki-highlighter.ts +46 -0
  45. package/src/vite-plugin/shiki-manager.test.ts +175 -0
  46. package/src/vite-plugin/shiki-manager.ts +53 -0
  47. package/src/vite-plugin/types.ts +189 -0
  48. package/src/vite-plugin.ts +1342 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Directive rewriting utilities for fallback MDX compilation
3
+ * @module vite-plugin/directive-rewriter
4
+ */
5
+
6
+ import type { Registry } from 'xmdx/registry';
7
+ import { starlightLibrary } from 'xmdx/registry';
8
+ import { collectImportedNames, insertAfterImports } from '../utils/imports.js';
9
+
10
+ /**
11
+ * Opening directive state for stack tracking.
12
+ */
13
+ type DirectiveOpening = {
14
+ name: string;
15
+ bracketTitle: string | null;
16
+ rawAttrs: string;
17
+ prefix: string; // Leading whitespace and blockquote markers (e.g., " ", "> ", " > > ")
18
+ componentName: string;
19
+ };
20
+
21
+ /**
22
+ * Escapes a value for use in an HTML/JSX attribute.
23
+ */
24
+ function escapeAttributeValue(value: string): string {
25
+ return value.replace(/&/g, '&').replace(/"/g, '"');
26
+ }
27
+
28
+ /**
29
+ * Normalizes directive attributes, stripping outer braces and filtering out reserved attrs.
30
+ */
31
+ function normalizeDirectiveAttrs(attrs: string, hasBracketTitle: boolean): string {
32
+ if (!attrs) {
33
+ return '';
34
+ }
35
+
36
+ // Strip outer braces from remark-directive syntax: {key="value"} → key="value"
37
+ let normalized = attrs.trim();
38
+ if (normalized.startsWith('{') && normalized.endsWith('}')) {
39
+ normalized = normalized.slice(1, -1).trim();
40
+ }
41
+
42
+ const tokens = normalized.split(/\s+/).filter(Boolean);
43
+ const cleaned: string[] = [];
44
+ for (const tok of tokens) {
45
+ const key = tok.split('=')[0]?.trim() ?? '';
46
+ if (!key) continue;
47
+ const lower = key.toLowerCase();
48
+ if (lower === 'type') continue;
49
+ if (hasBracketTitle && lower === 'title') continue;
50
+ cleaned.push(tok);
51
+ }
52
+ return cleaned.join(' ');
53
+ }
54
+
55
+ /**
56
+ * Parses an opening directive line (e.g., ":::note[Title]").
57
+ */
58
+ function parseOpeningDirective(
59
+ afterPrefix: string,
60
+ supported: Set<string>,
61
+ prefix: string
62
+ ): { name: string; bracketTitle: string | null; rawAttrs: string; prefix: string } | null {
63
+ // Content is already after the prefix; check for directive start
64
+ if (!afterPrefix.startsWith(':::')) {
65
+ return null;
66
+ }
67
+
68
+ let rest = afterPrefix.slice(3);
69
+ let name = '';
70
+ while (rest.length > 0 && /[A-Za-z]/.test(rest[0] ?? '')) {
71
+ name += (rest[0] ?? '').toLowerCase();
72
+ rest = rest.slice(1);
73
+ }
74
+
75
+ if (!name || !supported.has(name)) {
76
+ return null;
77
+ }
78
+
79
+ let bracketTitle: string | null = null;
80
+ if (rest.startsWith('[')) {
81
+ rest = rest.slice(1);
82
+ let title = '';
83
+ while (rest.length > 0) {
84
+ const ch = rest[0] ?? '';
85
+ rest = rest.slice(1);
86
+ if (ch === ']') {
87
+ bracketTitle = title;
88
+ break;
89
+ }
90
+ title += ch;
91
+ }
92
+ }
93
+
94
+ const rawAttrs = normalizeDirectiveAttrs(rest.trim(), Boolean(bracketTitle));
95
+ return { name, bracketTitle, rawAttrs, prefix };
96
+ }
97
+
98
+ /**
99
+ * Parses a closing directive line (":::").
100
+ */
101
+ function parseDirectiveCloser(afterPrefix: string, prefix: string): { prefix: string } | null {
102
+ // Check if the content after prefix is exactly `:::`
103
+ if (afterPrefix.trim() === ':::') {
104
+ return { prefix };
105
+ }
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * Rewrites directive syntax (:::note, :::tip, etc.) to JSX component syntax.
111
+ * Used for fallback MDX compilation when xmdx-core can't handle the file.
112
+ */
113
+ export function rewriteFallbackDirectives(
114
+ source: string,
115
+ registry: Registry | null,
116
+ hasStarlightConfigured: boolean
117
+ ): { code: string; usedComponents: Set<string>; changed: boolean } {
118
+ if (!source) {
119
+ return { code: source, usedComponents: new Set(), changed: false };
120
+ }
121
+
122
+ // Get directives from registry, fall back to starlightLibrary defaults
123
+ const registryDirectives = registry?.getSupportedDirectives().map((name) => name.toLowerCase()) ?? [];
124
+ const supportedSet = new Set(registryDirectives);
125
+
126
+ // Add Starlight directives only if registry is empty AND Starlight is configured
127
+ const useDefaultDirectives = supportedSet.size === 0 && hasStarlightConfigured;
128
+ if (useDefaultDirectives) {
129
+ const starlightDirectives = starlightLibrary.directiveMappings ?? [];
130
+ for (const mapping of starlightDirectives) {
131
+ supportedSet.add(mapping.directive.toLowerCase());
132
+ }
133
+ }
134
+
135
+ const lines = source.split(/\r?\n/);
136
+ const output: string[] = [];
137
+ const stack: DirectiveOpening[] = [];
138
+ const usedComponents = new Set<string>();
139
+ let changed = false;
140
+ let inFence = false;
141
+ let fenceChar: string | null = null;
142
+
143
+ for (const line of lines) {
144
+ // Extract prefix (whitespace + blockquote markers) like we do for directives
145
+ const prefixMatch = line.match(/^(\s*(?:>\s*)*)/);
146
+ const prefix = prefixMatch?.[1] ?? '';
147
+ const afterPrefix = line.slice(prefix.length);
148
+
149
+ // Check for code fence after stripping prefix (handles blockquoted code fences)
150
+ const fenceMatch = afterPrefix.match(/^([`~]{3,})/);
151
+ if (fenceMatch) {
152
+ const char = fenceMatch[1]?.[0] ?? null;
153
+ if (!inFence) {
154
+ inFence = true;
155
+ fenceChar = char;
156
+ } else if (char && fenceChar === char) {
157
+ inFence = false;
158
+ fenceChar = null;
159
+ }
160
+ output.push(line);
161
+ continue;
162
+ }
163
+
164
+ if (inFence) {
165
+ output.push(line);
166
+ continue;
167
+ }
168
+
169
+ const opening = parseOpeningDirective(afterPrefix, supportedSet, prefix);
170
+ if (opening) {
171
+ // Try registry first, then fall back to starlightLibrary
172
+ const mapping = registry?.getDirectiveMapping(opening.name)
173
+ ?? (useDefaultDirectives
174
+ ? starlightLibrary.directiveMappings?.find(m => m.directive.toLowerCase() === opening.name)
175
+ : null);
176
+ if (!mapping) {
177
+ output.push(line);
178
+ continue;
179
+ }
180
+
181
+ const componentName = mapping.component;
182
+ const props: string[] = ['data-mf-source="directive"'];
183
+ if (mapping.injectProps) {
184
+ for (const [propKey, propSource] of Object.entries(mapping.injectProps)) {
185
+ if (propSource.source === 'directive_name') {
186
+ props.push(`${propKey}="${escapeAttributeValue(opening.name)}"`);
187
+ } else if (propSource.source === 'bracket_title' && opening.bracketTitle) {
188
+ props.push(`${propKey}="${escapeAttributeValue(opening.bracketTitle)}"`);
189
+ } else if (propSource.source === 'literal' && propSource.value) {
190
+ props.push(`${propKey}="${escapeAttributeValue(propSource.value)}"`);
191
+ }
192
+ }
193
+ }
194
+
195
+ if (opening.bracketTitle) {
196
+ props.push(`title="${escapeAttributeValue(opening.bracketTitle)}"`);
197
+ }
198
+ if (opening.rawAttrs) {
199
+ props.push(opening.rawAttrs);
200
+ }
201
+
202
+ const propsStr = props.length > 0 ? ` ${props.join(' ')}` : '';
203
+ output.push(`${opening.prefix}<${componentName}${propsStr}>`);
204
+ stack.push({ ...opening, componentName });
205
+ usedComponents.add(componentName);
206
+ changed = true;
207
+ continue;
208
+ }
209
+
210
+ const closer = parseDirectiveCloser(afterPrefix, prefix);
211
+ if (closer && stack.length > 0) {
212
+ const opened = stack.pop();
213
+ if (opened) {
214
+ output.push(`${opened.prefix}</${opened.componentName}>`);
215
+ changed = true;
216
+ continue;
217
+ }
218
+ }
219
+
220
+ output.push(line);
221
+ }
222
+
223
+ while (stack.length > 0) {
224
+ const opened = stack.pop();
225
+ if (opened) {
226
+ output.push(`${opened.prefix}</${opened.componentName}>`);
227
+ }
228
+ }
229
+
230
+ return { code: output.join('\n'), usedComponents, changed };
231
+ }
232
+
233
+ /**
234
+ * Injects component imports for components used in rewritten directives.
235
+ */
236
+ export function injectFallbackImports(
237
+ source: string,
238
+ usedComponents: Set<string>,
239
+ registry: Registry | null,
240
+ hasStarlightConfigured: boolean
241
+ ): string {
242
+ if (!source || usedComponents.size === 0) {
243
+ return source;
244
+ }
245
+
246
+ const imported = collectImportedNames(source);
247
+ const importLines: string[] = [];
248
+
249
+ for (const componentName of usedComponents) {
250
+ if (imported.has(componentName)) {
251
+ continue;
252
+ }
253
+ const def = registry?.getComponent(componentName);
254
+ if (def) {
255
+ if (def.exportType === 'named') {
256
+ importLines.push(`import { ${componentName} } from '${def.modulePath}';`);
257
+ } else {
258
+ importLines.push(`import ${componentName} from '${def.modulePath}/${componentName}.astro';`);
259
+ }
260
+ } else if (componentName === 'Aside' && hasStarlightConfigured) {
261
+ // Fallback for Starlight Aside component when using default directives
262
+ // Only inject if Starlight is actually configured to avoid module-not-found errors
263
+ importLines.push(`import { Aside } from '@astrojs/starlight/components';`);
264
+ }
265
+ }
266
+
267
+ if (importLines.length === 0) {
268
+ return source;
269
+ }
270
+
271
+ return insertAfterImports(source, importLines.join('\n'));
272
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Worker pool management for parallel esbuild processing.
3
+ * Spawns multiple worker threads to handle large batches of JSX files.
4
+ * Uses inline worker code to avoid Node.js TypeScript loading issues.
5
+ * @module vite-plugin/esbuild-pool
6
+ */
7
+
8
+ import { Worker } from 'node:worker_threads';
9
+ import os from 'node:os';
10
+
11
+ interface WorkerInput {
12
+ entries: Array<{ entryName: string; id: string; jsx: string }>;
13
+ }
14
+
15
+ interface WorkerOutput {
16
+ results: Array<{ id: string; code: string; map?: string }>;
17
+ errors: Array<{ id: string; error: string }>;
18
+ }
19
+
20
+ /**
21
+ * Inline worker code as a string.
22
+ * This avoids issues with Node.js not supporting TypeScript in node_modules.
23
+ */
24
+ const WORKER_CODE = `
25
+ const { parentPort } = require('node:worker_threads');
26
+ const { build: esbuildBuild } = require('esbuild');
27
+ const path = require('node:path');
28
+
29
+ async function processChunk(input) {
30
+ const entryMap = new Map();
31
+ for (const entry of input.entries) {
32
+ entryMap.set(entry.entryName, { id: entry.id, jsx: entry.jsx });
33
+ }
34
+
35
+ const results = [];
36
+ const errors = [];
37
+
38
+ try {
39
+ const result = await esbuildBuild({
40
+ write: false,
41
+ bundle: false,
42
+ format: 'esm',
43
+ sourcemap: 'external',
44
+ loader: { '.jsx': 'jsx' },
45
+ jsx: 'transform',
46
+ jsxFactory: '_jsx',
47
+ jsxFragment: '_Fragment',
48
+ entryPoints: Array.from(entryMap.keys()),
49
+ outdir: 'out',
50
+ plugins: [{
51
+ name: 'xmdx-virtual-jsx-worker',
52
+ setup(build) {
53
+ build.onResolve({ filter: /^entry\\d+\\.jsx$/ }, args => ({
54
+ path: args.path, namespace: 'xmdx-jsx'
55
+ }));
56
+ build.onResolve({ filter: /.*/ }, args => ({
57
+ path: args.path, external: true
58
+ }));
59
+ build.onLoad({ filter: /.*/, namespace: 'xmdx-jsx' }, args => {
60
+ const entry = entryMap.get(args.path);
61
+ return entry ? { contents: entry.jsx, loader: 'jsx' } : null;
62
+ });
63
+ }
64
+ }]
65
+ });
66
+
67
+ for (const output of result.outputFiles || []) {
68
+ const basename = path.basename(output.path);
69
+ if (basename.endsWith('.map')) continue;
70
+ const entryName = basename.replace(/\\.js$/, '.jsx');
71
+ const entry = entryMap.get(entryName);
72
+ if (entry) {
73
+ const mapOutput = result.outputFiles.find(o => o.path === output.path + '.map');
74
+ results.push({ id: entry.id, code: output.text, map: mapOutput?.text });
75
+ }
76
+ }
77
+ } catch (err) {
78
+ for (const entry of input.entries) {
79
+ errors.push({ id: entry.id, error: err.message });
80
+ }
81
+ }
82
+
83
+ return { results, errors };
84
+ }
85
+
86
+ parentPort.on('message', async (input) => {
87
+ const output = await processChunk(input);
88
+ parentPort.postMessage(output);
89
+ });
90
+ `;
91
+
92
+ /**
93
+ * Run esbuild in parallel using worker threads.
94
+ * Splits the input into chunks and processes them concurrently.
95
+ *
96
+ * @param jsxInputs - Array of JSX inputs to transform
97
+ * @returns Map of file IDs to transformed code and source maps
98
+ */
99
+ export async function runParallelEsbuild(
100
+ jsxInputs: Array<{ id: string; jsx: string }>
101
+ ): Promise<Map<string, { code: string; map?: string }>> {
102
+ // Use CPU count - 1, capped at 8 workers max
103
+ const workerCount = Math.max(1, Math.min(os.cpus().length - 1, 8));
104
+ const chunkSize = Math.ceil(jsxInputs.length / workerCount);
105
+
106
+ // Split into chunks
107
+ const chunks: Array<Array<{ entryName: string; id: string; jsx: string }>> = [];
108
+ for (let i = 0; i < jsxInputs.length; i += chunkSize) {
109
+ const chunk = jsxInputs.slice(i, i + chunkSize).map((input, j) => ({
110
+ entryName: `entry${i + j}.jsx`,
111
+ id: input.id,
112
+ jsx: input.jsx,
113
+ }));
114
+ chunks.push(chunk);
115
+ }
116
+
117
+ // Run workers in parallel
118
+ const workerPromises = chunks.map((chunk) => runWorker(chunk));
119
+ const results = await Promise.all(workerPromises);
120
+
121
+ // Merge results
122
+ const merged = new Map<string, { code: string; map?: string }>();
123
+ for (const result of results) {
124
+ for (const item of result.results) {
125
+ merged.set(item.id, { code: item.code, map: item.map });
126
+ }
127
+ // Log any errors (but don't fail the whole batch)
128
+ for (const error of result.errors) {
129
+ console.warn(`[xmdx] Worker esbuild error for ${error.id}: ${error.error}`);
130
+ }
131
+ }
132
+
133
+ return merged;
134
+ }
135
+
136
+ /**
137
+ * Run a single worker with the given entries using inline code.
138
+ */
139
+ function runWorker(
140
+ entries: Array<{ entryName: string; id: string; jsx: string }>
141
+ ): Promise<WorkerOutput> {
142
+ return new Promise<WorkerOutput>((resolve, reject) => {
143
+ // Use eval mode to execute inline JavaScript code
144
+ const worker = new Worker(WORKER_CODE, { eval: true });
145
+
146
+ // 60 second timeout
147
+ const timeout = setTimeout(() => {
148
+ worker.terminate();
149
+ reject(new Error('Worker timeout after 60s'));
150
+ }, 60000);
151
+
152
+ worker.on('message', (output: WorkerOutput) => {
153
+ clearTimeout(timeout);
154
+ worker.terminate();
155
+ resolve(output);
156
+ });
157
+
158
+ worker.on('error', (err) => {
159
+ clearTimeout(timeout);
160
+ worker.terminate();
161
+ reject(err);
162
+ });
163
+
164
+ worker.on('exit', (code) => {
165
+ if (code !== 0) {
166
+ clearTimeout(timeout);
167
+ reject(new Error(`Worker exited with code ${code}`));
168
+ }
169
+ });
170
+
171
+ worker.postMessage({ entries } satisfies WorkerInput);
172
+ });
173
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Vite plugin modules for Xmdx
3
+ * @module vite-plugin
4
+ */
5
+
6
+ // Re-export types
7
+ export type {
8
+ XmdxBinding,
9
+ XmdxCompiler,
10
+ CompileResult,
11
+ BatchCompileResult,
12
+ ParseBlocksResult,
13
+ XmdxPluginOptions,
14
+ DocumentFragment,
15
+ Node,
16
+ Element,
17
+ TextNode,
18
+ } from './types.js';
19
+
20
+ export { DEFAULT_EXTENSIONS } from '../utils/paths.js';
21
+
22
+ // Re-export binding loader
23
+ export { loadXmdxBinding, resetBindingPromise, ENABLE_SHIKI, IS_MDAST } from './binding-loader.js';
24
+
25
+ // Re-export JSX module utilities
26
+ export { compileFallbackModule } from './jsx-module.js';
27
+
28
+ // Re-export directive rewriter
29
+ export { rewriteFallbackDirectives, injectFallbackImports } from './directive-rewriter.js';
30
+
31
+ // Re-export config normalization utilities
32
+ export { normalizeStarlightComponents } from './normalize-config.js';
33
+ export type { NormalizedStarlightComponents } from './normalize-config.js';
34
+
35
+ // Re-export shiki highlighter
36
+ export { createShikiHighlighter } from './shiki-highlighter.js';
37
+ export type { ShikiHighlighter } from '../transforms/shiki.js';
@@ -0,0 +1,106 @@
1
+ /**
2
+ * JSX module generation utilities
3
+ * @module vite-plugin/jsx-module
4
+ */
5
+
6
+ import type { SourceMapInput } from 'rollup';
7
+ import type { Registry } from 'xmdx/registry';
8
+ import { transformWithEsbuild } from 'vite';
9
+ import { compile as compileMdx } from '@mdx-js/mdx';
10
+ import remarkGfm from 'remark-gfm';
11
+ import remarkDirective from 'remark-directive';
12
+ import { ESBUILD_JSX_CONFIG } from '../constants.js';
13
+ import { stripFrontmatter } from '../utils/frontmatter.js';
14
+ import { loadXmdxBinding } from './binding-loader.js';
15
+ import { rewriteFallbackDirectives, injectFallbackImports } from './directive-rewriter.js';
16
+
17
+ /**
18
+ * Compiles a fallback module using @mdx-js/mdx.
19
+ * Used for files with patterns that xmdx-core can't handle.
20
+ */
21
+ export async function compileFallbackModule(
22
+ filename: string,
23
+ source: string,
24
+ virtualId: string,
25
+ registry: Registry | null,
26
+ hasStarlightConfigured: boolean
27
+ ): Promise<{ code: string; map?: SourceMapInput }> {
28
+ let frontmatter: Record<string, unknown> = {};
29
+ try {
30
+ const binding = await loadXmdxBinding();
31
+ const frontmatterResult = binding.parseFrontmatter(source);
32
+ frontmatter = frontmatterResult.frontmatter || {};
33
+ } catch {
34
+ frontmatter = {};
35
+ }
36
+
37
+ let sourceWithoutFrontmatter = stripFrontmatter(source);
38
+ const directiveResult = rewriteFallbackDirectives(sourceWithoutFrontmatter, registry, hasStarlightConfigured);
39
+ if (directiveResult.changed) {
40
+ sourceWithoutFrontmatter = injectFallbackImports(
41
+ directiveResult.code,
42
+ directiveResult.usedComponents,
43
+ registry,
44
+ hasStarlightConfigured
45
+ );
46
+ }
47
+ // Use @mdx-js/mdx to compile files that xmdx can't handle
48
+ // (e.g., files with import/export statements)
49
+ // Include remark-gfm for GFM features (tables, strikethrough, task lists)
50
+ // and remark-directive to handle unconverted ::: directives gracefully
51
+ const compiled = await compileMdx(sourceWithoutFrontmatter, {
52
+ jsxImportSource: 'astro',
53
+ remarkPlugins: [remarkGfm, remarkDirective],
54
+ // Don't use providerImportSource as it requires @mdx-js/react
55
+ // which may not be installed
56
+ });
57
+
58
+ // The compiled output is a VFile, get the string value
59
+ const mdxCode = String(compiled);
60
+
61
+ // Normalize MDX default export so we can wrap with Astro createComponent
62
+ const mdxWithoutDefault = mdxCode
63
+ .replace(/export default function MDXContent/g, 'function MDXContent')
64
+ .replace(/export default MDXContent\s*;/g, '')
65
+ .replace(/export\s*\{\s*MDXContent\s+as\s+default\s*\};?/g, '');
66
+
67
+ // Wrap in Astro-compatible module format
68
+ // @mdx-js/mdx outputs ESM with `export default function MDXContent(...)`
69
+ // We need to add Content, frontmatter and getHeadings exports for Astro compatibility
70
+ // Note: MDXContent is the default export function from @mdx-js/mdx
71
+ const wrappedCode = `
72
+ import { createComponent, renderJSX } from 'astro/runtime/server/index.js';
73
+ import { Fragment } from 'astro/jsx-runtime';
74
+ ${mdxWithoutDefault}
75
+
76
+ // Re-export for Astro compatibility
77
+ // Wrap MDXContent so it renders as an Astro component factory
78
+ const XmdxContent = createComponent(
79
+ (result, props, _slots) =>
80
+ renderJSX(
81
+ result,
82
+ MDXContent({
83
+ ...(props ?? {}),
84
+ // Ensure Astro's Fragment is available for <Fragment slot="..."> usage in MDX.
85
+ components: { ...(props?.components ?? {}), Fragment },
86
+ })
87
+ ),
88
+ ${JSON.stringify(filename)}
89
+ );
90
+ export { MDXContent };
91
+ export const Content = XmdxContent;
92
+ export const file = ${JSON.stringify(filename)};
93
+ export const url = undefined;
94
+ export function getHeadings() { return []; }
95
+ export const frontmatter = ${JSON.stringify(frontmatter)};
96
+ export default XmdxContent;
97
+ `;
98
+
99
+ // Transform JSX through esbuild (same as the main compilation path)
100
+ const esbuildResult = await transformWithEsbuild(wrappedCode, virtualId, ESBUILD_JSX_CONFIG);
101
+
102
+ return {
103
+ code: esbuildResult.code,
104
+ map: esbuildResult.map as SourceMapInput | undefined,
105
+ };
106
+ }