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.
- package/index.ts +8 -0
- package/package.json +80 -0
- package/src/constants.ts +52 -0
- package/src/index.ts +150 -0
- package/src/pipeline/index.ts +38 -0
- package/src/pipeline/orchestrator.test.ts +324 -0
- package/src/pipeline/orchestrator.ts +121 -0
- package/src/pipeline/pipe.test.ts +251 -0
- package/src/pipeline/pipe.ts +70 -0
- package/src/pipeline/types.ts +59 -0
- package/src/plugins.test.ts +274 -0
- package/src/presets/index.ts +225 -0
- package/src/transforms/blocks-to-jsx.test.ts +590 -0
- package/src/transforms/blocks-to-jsx.ts +617 -0
- package/src/transforms/expressive-code.test.ts +274 -0
- package/src/transforms/expressive-code.ts +147 -0
- package/src/transforms/index.test.ts +143 -0
- package/src/transforms/index.ts +100 -0
- package/src/transforms/inject-components.test.ts +406 -0
- package/src/transforms/inject-components.ts +184 -0
- package/src/transforms/shiki.test.ts +289 -0
- package/src/transforms/shiki.ts +312 -0
- package/src/types.ts +92 -0
- package/src/utils/config.test.ts +252 -0
- package/src/utils/config.ts +146 -0
- package/src/utils/frontmatter.ts +33 -0
- package/src/utils/imports.test.ts +518 -0
- package/src/utils/imports.ts +201 -0
- package/src/utils/mdx-detection.test.ts +41 -0
- package/src/utils/mdx-detection.ts +209 -0
- package/src/utils/paths.test.ts +206 -0
- package/src/utils/paths.ts +92 -0
- package/src/utils/validation.test.ts +60 -0
- package/src/utils/validation.ts +15 -0
- package/src/vite-plugin/binding-loader.ts +81 -0
- package/src/vite-plugin/directive-rewriter.test.ts +331 -0
- package/src/vite-plugin/directive-rewriter.ts +272 -0
- package/src/vite-plugin/esbuild-pool.ts +173 -0
- package/src/vite-plugin/index.ts +37 -0
- package/src/vite-plugin/jsx-module.ts +106 -0
- package/src/vite-plugin/mdx-wrapper.ts +328 -0
- package/src/vite-plugin/normalize-config.test.ts +78 -0
- package/src/vite-plugin/normalize-config.ts +29 -0
- package/src/vite-plugin/shiki-highlighter.ts +46 -0
- package/src/vite-plugin/shiki-manager.test.ts +175 -0
- package/src/vite-plugin/shiki-manager.ts +53 -0
- package/src/vite-plugin/types.ts +189 -0
- 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
|
+
}
|