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,1342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xmdx Vite plugin for MDX compilation.
|
|
3
|
+
* @module vite-plugin
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { transformWithEsbuild, type ResolvedConfig, type Plugin } from 'vite';
|
|
11
|
+
import MagicString from 'magic-string';
|
|
12
|
+
import { build as esbuildBuild, type BuildResult } from 'esbuild';
|
|
13
|
+
import type { SourceMapInput } from 'rollup';
|
|
14
|
+
import { runParallelEsbuild } from './vite-plugin/esbuild-pool.js';
|
|
15
|
+
import {
|
|
16
|
+
createRegistry,
|
|
17
|
+
starlightLibrary,
|
|
18
|
+
astroLibrary,
|
|
19
|
+
expressiveCodeLibrary,
|
|
20
|
+
type ComponentLibrary,
|
|
21
|
+
type Registry,
|
|
22
|
+
} from 'xmdx/registry';
|
|
23
|
+
import { createPipeline } from './pipeline/index.js';
|
|
24
|
+
import { blocksToJsx } from './transforms/blocks-to-jsx.js';
|
|
25
|
+
import { resolveExpressiveCodeConfig } from './utils/config.js';
|
|
26
|
+
import { stripFrontmatter } from './utils/frontmatter.js';
|
|
27
|
+
import { hasProblematicMdxPatterns, detectProblematicMdxPatterns } from './utils/mdx-detection.js';
|
|
28
|
+
import { extractImportStatements } from './utils/imports.js';
|
|
29
|
+
import { stripQuery, deriveFileOptions, shouldCompile } from './utils/paths.js';
|
|
30
|
+
import {
|
|
31
|
+
VIRTUAL_MODULE_PREFIX,
|
|
32
|
+
OUTPUT_EXTENSION,
|
|
33
|
+
ESBUILD_JSX_CONFIG,
|
|
34
|
+
DEFAULT_IGNORE_PATTERNS,
|
|
35
|
+
STARLIGHT_LAYER_ORDER,
|
|
36
|
+
} from './constants.js';
|
|
37
|
+
import type { XmdxPlugin, PluginHooks, TransformContext } from './types.js';
|
|
38
|
+
|
|
39
|
+
// Debug timing utilities
|
|
40
|
+
const DEBUG_TIMING = process.env.XMDX_DEBUG_TIMING === '1';
|
|
41
|
+
|
|
42
|
+
function debugTime(label: string): void {
|
|
43
|
+
if (DEBUG_TIMING) console.time(`[xmdx:timing] ${label}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function debugTimeEnd(label: string): void {
|
|
47
|
+
if (DEBUG_TIMING) console.timeEnd(`[xmdx:timing] ${label}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function debugLog(message: string): void {
|
|
51
|
+
if (DEBUG_TIMING) console.log(`[xmdx:timing] ${message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Load hook profiler — activated by XMDX_LOAD_PROFILE=1
|
|
55
|
+
const LOAD_PROFILE = process.env.XMDX_LOAD_PROFILE === '1';
|
|
56
|
+
const LOAD_PROFILE_TOP = Number(process.env.XMDX_LOAD_PROFILE_TOP) || 10;
|
|
57
|
+
|
|
58
|
+
type PhaseStats = { totalMs: number; count: number; maxMs: number };
|
|
59
|
+
|
|
60
|
+
class LoadProfiler {
|
|
61
|
+
phases = new Map<string, PhaseStats>();
|
|
62
|
+
cacheHits = 0;
|
|
63
|
+
esbuildCacheHits = 0;
|
|
64
|
+
cacheMisses = 0;
|
|
65
|
+
callCount = 0;
|
|
66
|
+
totalMs = 0;
|
|
67
|
+
slowest: Array<{ file: string; ms: number }> = [];
|
|
68
|
+
private dumped = false;
|
|
69
|
+
private rootFallback = '';
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
process.on('exit', () => {
|
|
73
|
+
if (!this.dumped) this.dump(this.rootFallback);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
setRoot(root: string): void {
|
|
78
|
+
this.rootFallback = root;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private ensure(phase: string): PhaseStats {
|
|
82
|
+
let s = this.phases.get(phase);
|
|
83
|
+
if (!s) { s = { totalMs: 0, count: 0, maxMs: 0 }; this.phases.set(phase, s); }
|
|
84
|
+
return s;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
record(phase: string, ms: number): void {
|
|
88
|
+
const s = this.ensure(phase);
|
|
89
|
+
s.totalMs += ms;
|
|
90
|
+
s.count++;
|
|
91
|
+
if (ms > s.maxMs) s.maxMs = ms;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
recordFile(file: string, ms: number): void {
|
|
95
|
+
this.callCount++;
|
|
96
|
+
this.totalMs += ms;
|
|
97
|
+
// Keep top-N slowest
|
|
98
|
+
if (this.slowest.length < LOAD_PROFILE_TOP || ms > this.slowest[this.slowest.length - 1]!.ms) {
|
|
99
|
+
this.slowest.push({ file, ms });
|
|
100
|
+
this.slowest.sort((a, b) => b.ms - a.ms);
|
|
101
|
+
if (this.slowest.length > LOAD_PROFILE_TOP) this.slowest.length = LOAD_PROFILE_TOP;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dump(root: string): void {
|
|
106
|
+
if (this.dumped) return;
|
|
107
|
+
this.dumped = true;
|
|
108
|
+
const p = (label: string) => `[xmdx:load-profiler] ${label}`;
|
|
109
|
+
console.info(p(`calls=${this.callCount} total=${this.totalMs.toFixed(0)}ms`));
|
|
110
|
+
console.info(p(`esbuild-cache-hit=${this.esbuildCacheHits} compilation-cache-hit=${this.cacheHits} cache-miss=${this.cacheMisses}`));
|
|
111
|
+
for (const [phase, s] of this.phases) {
|
|
112
|
+
console.info(p(`${phase} total=${s.totalMs.toFixed(0)}ms avg=${s.count ? (s.totalMs / s.count).toFixed(2) : 0}ms max=${s.maxMs.toFixed(2)}ms count=${s.count}`));
|
|
113
|
+
}
|
|
114
|
+
const overhead = this.totalMs - [...this.phases.values()].reduce((a, s) => a + s.totalMs, 0);
|
|
115
|
+
console.info(p(`overhead total=${overhead.toFixed(0)}ms avg=${this.callCount ? (overhead / this.callCount).toFixed(2) : 0}ms`));
|
|
116
|
+
if (this.slowest.length > 0) {
|
|
117
|
+
console.info(p(`top ${this.slowest.length} slowest files:`));
|
|
118
|
+
for (const { file, ms } of this.slowest) {
|
|
119
|
+
console.info(p(` ${ms.toFixed(0)}ms ${file.replace(root, '')}`));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const loadProfiler = LOAD_PROFILE ? new LoadProfiler() : null;
|
|
126
|
+
|
|
127
|
+
// Import from extracted vite-plugin modules
|
|
128
|
+
import type {
|
|
129
|
+
XmdxBinding,
|
|
130
|
+
XmdxCompiler,
|
|
131
|
+
CompileResult,
|
|
132
|
+
MdxBatchCompileResult,
|
|
133
|
+
XmdxPluginOptions,
|
|
134
|
+
} from './vite-plugin/types.js';
|
|
135
|
+
import { loadXmdxBinding, ENABLE_SHIKI, IS_MDAST } from './vite-plugin/binding-loader.js';
|
|
136
|
+
import { compileFallbackModule } from './vite-plugin/jsx-module.js';
|
|
137
|
+
import { wrapMdxModule } from './vite-plugin/mdx-wrapper.js';
|
|
138
|
+
import { normalizeStarlightComponents } from './vite-plugin/normalize-config.js';
|
|
139
|
+
import { ShikiManager } from './vite-plugin/shiki-manager.js';
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Resolves library configuration from options.
|
|
143
|
+
* Supports both new `libraries` API and legacy `starlightComponents` option.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveLibraries(options: XmdxPluginOptions): {
|
|
146
|
+
libraries: ComponentLibrary[];
|
|
147
|
+
registry: Registry;
|
|
148
|
+
} {
|
|
149
|
+
// New API: explicit libraries array
|
|
150
|
+
if (Array.isArray(options.libraries)) {
|
|
151
|
+
const registry = createRegistry(options.libraries);
|
|
152
|
+
return { libraries: options.libraries, registry };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Legacy API: derive libraries from starlightComponents option
|
|
156
|
+
const libraries: ComponentLibrary[] = [astroLibrary];
|
|
157
|
+
|
|
158
|
+
if (options.starlightComponents) {
|
|
159
|
+
libraries.push(starlightLibrary);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (options.expressiveCode) {
|
|
163
|
+
libraries.push(expressiveCodeLibrary);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const registry = createRegistry(libraries);
|
|
167
|
+
return { libraries, registry };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// require() for CJS interop with glob package
|
|
171
|
+
const require = createRequire(import.meta.url);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Collects hooks from an array of plugins, organizing them by hook type.
|
|
175
|
+
*/
|
|
176
|
+
function collectHooks(plugins: XmdxPlugin[]): PluginHooks {
|
|
177
|
+
const hooks: PluginHooks = {
|
|
178
|
+
afterParse: [],
|
|
179
|
+
beforeInject: [],
|
|
180
|
+
beforeOutput: [],
|
|
181
|
+
preprocess: [],
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Sort plugins: 'pre' first, then undefined, then 'post'
|
|
185
|
+
const sorted = [...plugins].sort((a, b) => {
|
|
186
|
+
const order: Record<string, number> = { pre: 0, undefined: 1, post: 2 };
|
|
187
|
+
const aOrder = order[a.enforce ?? 'undefined'] ?? 1;
|
|
188
|
+
const bOrder = order[b.enforce ?? 'undefined'] ?? 1;
|
|
189
|
+
return aOrder - bOrder;
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
for (const plugin of sorted) {
|
|
193
|
+
if (plugin.afterParse) hooks.afterParse.push(plugin.afterParse);
|
|
194
|
+
if (plugin.beforeInject) hooks.beforeInject.push(plugin.beforeInject);
|
|
195
|
+
if (plugin.beforeOutput) hooks.beforeOutput.push(plugin.beforeOutput);
|
|
196
|
+
if (plugin.preprocess) hooks.preprocess.push(plugin.preprocess);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return hooks;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Creates the Xmdx Vite plugin that intercepts `.md`/`.mdx` files
|
|
204
|
+
* before `@astrojs/mdx` runs.
|
|
205
|
+
*/
|
|
206
|
+
export function xmdxPlugin(userOptions: XmdxPluginOptions = {}): Plugin {
|
|
207
|
+
let compiler: XmdxCompiler | undefined;
|
|
208
|
+
let resolvedConfig: ResolvedConfig | undefined;
|
|
209
|
+
const sourceLookup = new Map<string, string>();
|
|
210
|
+
type CachedModuleResult = NonNullable<import('./vite-plugin/types.js').ModuleBatchCompileResult['results'][number]['result']> & {
|
|
211
|
+
originalSource?: string;
|
|
212
|
+
processedSource?: string;
|
|
213
|
+
};
|
|
214
|
+
type CachedMdxResult = NonNullable<MdxBatchCompileResult['results'][number]['result']> & {
|
|
215
|
+
originalSource?: string;
|
|
216
|
+
processedSource?: string;
|
|
217
|
+
};
|
|
218
|
+
const originalSourceCache = new Map<string, string>(); // Raw markdown before preprocess hooks
|
|
219
|
+
const processedSourceCache = new Map<string, string>(); // Preprocessed markdown fed to compiler
|
|
220
|
+
const moduleCompilationCache = new Map<string, CachedModuleResult>(); // MD files compiled to modules via Rust
|
|
221
|
+
const mdxCompilationCache = new Map<string, CachedMdxResult>(); // MDX files compiled via mdxjs-rs
|
|
222
|
+
const esbuildCache = new Map<string, { code: string; map?: SourceMapInput }>(); // Pre-compiled esbuild results
|
|
223
|
+
const fallbackFiles = new Set<string>();
|
|
224
|
+
|
|
225
|
+
// Persistent cache for SSR/Client 2-pass builds
|
|
226
|
+
// These survive between buildStart calls, avoiding redundant recompilation
|
|
227
|
+
let buildPassCount = 0;
|
|
228
|
+
const persistentCache = {
|
|
229
|
+
esbuild: new Map<string, { code: string; map?: SourceMapInput }>(),
|
|
230
|
+
moduleCompilation: new Map<string, CachedModuleResult>(),
|
|
231
|
+
mdxCompilation: new Map<string, CachedMdxResult>(),
|
|
232
|
+
fallbackFiles: new Set<string>(),
|
|
233
|
+
fallbackReasons: new Map<string, string>(),
|
|
234
|
+
};
|
|
235
|
+
const fallbackReasons = new Map<string, string>();
|
|
236
|
+
const processedFiles = new Set<string>();
|
|
237
|
+
let totalProcessingTimeMs = 0;
|
|
238
|
+
|
|
239
|
+
const providedBinding = userOptions.binding ?? null;
|
|
240
|
+
|
|
241
|
+
// Collect hooks from plugins
|
|
242
|
+
const plugins = userOptions.plugins ?? [];
|
|
243
|
+
const hooks = collectHooks(plugins);
|
|
244
|
+
|
|
245
|
+
// Create pipeline once (shared across buildStart and load hooks)
|
|
246
|
+
const transformPipeline = createPipeline({
|
|
247
|
+
afterParse: hooks.afterParse,
|
|
248
|
+
beforeInject: hooks.beforeInject,
|
|
249
|
+
beforeOutput: hooks.beforeOutput,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Build compiler options with default code_sample_components
|
|
253
|
+
const compilerOptions = {
|
|
254
|
+
...(userOptions.compiler ?? {}),
|
|
255
|
+
jsx: {
|
|
256
|
+
...(userOptions.compiler?.jsx ?? {}),
|
|
257
|
+
code_sample_components:
|
|
258
|
+
userOptions.compiler?.jsx?.code_sample_components ?? ['Code', 'Prism'],
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const include = userOptions.include ?? shouldCompile;
|
|
263
|
+
const starlightComponents = userOptions.starlightComponents ?? false;
|
|
264
|
+
const expressiveCode = resolveExpressiveCodeConfig(
|
|
265
|
+
userOptions.expressiveCode ?? false
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Resolve libraries and create registry
|
|
269
|
+
const { registry } = resolveLibraries(userOptions);
|
|
270
|
+
|
|
271
|
+
// Track whether Starlight is configured for gating default directive handling
|
|
272
|
+
const hasStarlightConfigured = Boolean(userOptions.starlightComponents) ||
|
|
273
|
+
(Array.isArray(userOptions.libraries) &&
|
|
274
|
+
userOptions.libraries.some(lib => lib === starlightLibrary));
|
|
275
|
+
|
|
276
|
+
// MDX import handling options
|
|
277
|
+
const mdxOptions = userOptions.mdx;
|
|
278
|
+
|
|
279
|
+
const unwrapVirtual = (value: string | undefined): string | undefined =>
|
|
280
|
+
value && value.startsWith(VIRTUAL_MODULE_PREFIX)
|
|
281
|
+
? value.slice(VIRTUAL_MODULE_PREFIX.length)
|
|
282
|
+
: value;
|
|
283
|
+
|
|
284
|
+
// Enable Shiki when:
|
|
285
|
+
// 1. XMDX_SHIKI=1 env var is set, OR
|
|
286
|
+
// 2. ExpressiveCode is explicitly disabled (fallback highlighting)
|
|
287
|
+
const shikiManager = new ShikiManager(ENABLE_SHIKI || !expressiveCode);
|
|
288
|
+
|
|
289
|
+
// Lazy compiler initialization to avoid Vite module runner timing issues
|
|
290
|
+
const getCompiler = async (): Promise<XmdxCompiler> => {
|
|
291
|
+
if (!compiler) {
|
|
292
|
+
const binding = providedBinding ?? (await loadXmdxBinding());
|
|
293
|
+
const createCompiler = binding.createCompiler
|
|
294
|
+
? binding.createCompiler
|
|
295
|
+
: (cfg: Record<string, unknown>) => new binding.XmdxCompiler!(cfg);
|
|
296
|
+
compiler = createCompiler(compilerOptions);
|
|
297
|
+
}
|
|
298
|
+
return compiler;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
name: 'vite-plugin-xmdx',
|
|
303
|
+
enforce: 'pre',
|
|
304
|
+
|
|
305
|
+
configResolved(config) {
|
|
306
|
+
resolvedConfig = config;
|
|
307
|
+
loadProfiler?.setRoot(config.root);
|
|
308
|
+
if (config.esbuild == null) {
|
|
309
|
+
(config as { esbuild: object }).esbuild = {
|
|
310
|
+
jsx: 'automatic',
|
|
311
|
+
jsxImportSource: 'astro',
|
|
312
|
+
};
|
|
313
|
+
} else if (config.esbuild !== false) {
|
|
314
|
+
const esbuildConfig = config.esbuild as Record<string, unknown>;
|
|
315
|
+
if (esbuildConfig.jsx == null) {
|
|
316
|
+
esbuildConfig.jsx = 'automatic';
|
|
317
|
+
}
|
|
318
|
+
if (esbuildConfig.jsxImportSource == null) {
|
|
319
|
+
esbuildConfig.jsxImportSource = 'astro';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Ensure native binding is treated as external to avoid Vite SSR runner involvement
|
|
323
|
+
const optimizeDeps = (config as Record<string, any>).optimizeDeps ?? {};
|
|
324
|
+
const exclude: string[] = optimizeDeps.exclude ?? [];
|
|
325
|
+
if (!exclude.includes('xmdx-napi')) {
|
|
326
|
+
exclude.push('xmdx-napi');
|
|
327
|
+
}
|
|
328
|
+
optimizeDeps.exclude = exclude;
|
|
329
|
+
(config as Record<string, any>).optimizeDeps = optimizeDeps;
|
|
330
|
+
|
|
331
|
+
const ssr = (config as Record<string, any>).ssr ?? {};
|
|
332
|
+
const ssrExternal: string[] = ssr.external ?? [];
|
|
333
|
+
if (!ssrExternal.includes('xmdx-napi')) {
|
|
334
|
+
ssrExternal.push('xmdx-napi');
|
|
335
|
+
}
|
|
336
|
+
ssr.external = ssrExternal;
|
|
337
|
+
(config as Record<string, any>).ssr = ssr;
|
|
338
|
+
// Note: Binding/compiler initialization deferred to buildStart/load hooks
|
|
339
|
+
// to avoid Vite module runner timing issues with async imports
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
transform(code, id) {
|
|
343
|
+
// Dev mode only — build uses Head.astro overlay for layer ordering.
|
|
344
|
+
if (resolvedConfig?.command !== 'serve' || !hasStarlightConfigured) return;
|
|
345
|
+
// Target .astro files containing <head> (root layouts like Page.astro)
|
|
346
|
+
if (!id.endsWith('.astro') || !code.includes('<head>')) return;
|
|
347
|
+
|
|
348
|
+
const ms = new MagicString(code, { filename: id });
|
|
349
|
+
ms.replace('<head>', `<head><style is:inline>${STARLIGHT_LAYER_ORDER}</style>`);
|
|
350
|
+
return {
|
|
351
|
+
code: ms.toString(),
|
|
352
|
+
map: ms.generateMap({ hires: 'boundary' }),
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
async buildStart() {
|
|
357
|
+
// Only batch compile in build mode (not dev/serve)
|
|
358
|
+
if (resolvedConfig?.command !== 'build') return;
|
|
359
|
+
|
|
360
|
+
buildPassCount++;
|
|
361
|
+
debugLog(`Build pass ${buildPassCount}`);
|
|
362
|
+
|
|
363
|
+
// Pass 2+: Reuse cached results from previous build pass (SSR → Client)
|
|
364
|
+
if (buildPassCount > 1 && persistentCache.esbuild.size > 0) {
|
|
365
|
+
debugTime('buildStart:total');
|
|
366
|
+
debugLog(`Reusing ${persistentCache.esbuild.size} cached esbuild results from pass ${buildPassCount - 1}`);
|
|
367
|
+
|
|
368
|
+
// Restore all caches from persistent storage
|
|
369
|
+
for (const [k, v] of persistentCache.esbuild) {
|
|
370
|
+
esbuildCache.set(k, v);
|
|
371
|
+
}
|
|
372
|
+
for (const [k, v] of persistentCache.moduleCompilation) {
|
|
373
|
+
moduleCompilationCache.set(k, v);
|
|
374
|
+
}
|
|
375
|
+
for (const [k, v] of persistentCache.mdxCompilation) {
|
|
376
|
+
mdxCompilationCache.set(k, v);
|
|
377
|
+
}
|
|
378
|
+
for (const file of persistentCache.fallbackFiles) {
|
|
379
|
+
fallbackFiles.add(file);
|
|
380
|
+
}
|
|
381
|
+
for (const [k, v] of persistentCache.fallbackReasons) {
|
|
382
|
+
fallbackReasons.set(k, v);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.info(
|
|
386
|
+
`[xmdx] Build pass ${buildPassCount}: Reusing ${persistentCache.esbuild.size} cached results`
|
|
387
|
+
);
|
|
388
|
+
debugTimeEnd('buildStart:total');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Check for potential cache inconsistency
|
|
393
|
+
const root = resolvedConfig.root;
|
|
394
|
+
const astroDir = path.join(root, '.astro');
|
|
395
|
+
const distDir = path.join(root, 'dist');
|
|
396
|
+
if (existsSync(astroDir) && !existsSync(distDir)) {
|
|
397
|
+
console.warn('[xmdx] Stale cache detected (.astro exists but dist does not). Consider running `rm -rf .astro` if you encounter module resolution errors.');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
debugTime('buildStart:total');
|
|
401
|
+
debugTime('buildStart:glob');
|
|
402
|
+
|
|
403
|
+
// Find all MD/MDX files (use CJS require to avoid Vite's module runner)
|
|
404
|
+
const { glob } = require('glob') as {
|
|
405
|
+
glob: (
|
|
406
|
+
pattern: string,
|
|
407
|
+
options: { cwd: string; ignore: string[]; absolute: boolean }
|
|
408
|
+
) => Promise<string[]>;
|
|
409
|
+
};
|
|
410
|
+
const files = await glob('**/*.{md,mdx}', {
|
|
411
|
+
cwd: resolvedConfig.root,
|
|
412
|
+
ignore: [...DEFAULT_IGNORE_PATTERNS],
|
|
413
|
+
absolute: true,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
debugTimeEnd('buildStart:glob');
|
|
417
|
+
debugLog(`Found ${files.length} markdown files`);
|
|
418
|
+
|
|
419
|
+
if (files.length === 0) {
|
|
420
|
+
debugTimeEnd('buildStart:total');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
debugTime('buildStart:readFiles');
|
|
425
|
+
|
|
426
|
+
// Track fallback pattern statistics
|
|
427
|
+
const fallbackStats = {
|
|
428
|
+
disallowedImports: 0,
|
|
429
|
+
noAllowImports: 0,
|
|
430
|
+
};
|
|
431
|
+
const disallowedImportSources = new Map<string, number>();
|
|
432
|
+
|
|
433
|
+
// Read all files in parallel and prepare batch inputs
|
|
434
|
+
const inputsOrNull = await Promise.all(
|
|
435
|
+
files.map(async (file) => {
|
|
436
|
+
const rawSource = await readFile(file, 'utf8');
|
|
437
|
+
let processedSource = rawSource;
|
|
438
|
+
|
|
439
|
+
// Apply preprocess hooks (same as load hook does)
|
|
440
|
+
for (const preprocessHook of hooks.preprocess) {
|
|
441
|
+
processedSource = preprocessHook(processedSource, file);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Pre-detect problematic patterns - these files will be handled by Astro's MDX plugin
|
|
445
|
+
const detection = detectProblematicMdxPatterns(processedSource, mdxOptions);
|
|
446
|
+
if (detection.hasProblematicPatterns) {
|
|
447
|
+
fallbackFiles.add(file);
|
|
448
|
+
fallbackReasons.set(file, detection.reason ?? 'Unknown pattern');
|
|
449
|
+
|
|
450
|
+
// Track statistics
|
|
451
|
+
if (detection.disallowedImports && detection.disallowedImports.length > 0) {
|
|
452
|
+
fallbackStats.disallowedImports++;
|
|
453
|
+
for (const src of detection.disallowedImports) {
|
|
454
|
+
disallowedImportSources.set(src, (disallowedImportSources.get(src) ?? 0) + 1);
|
|
455
|
+
}
|
|
456
|
+
} else if (detection.allImports && detection.allImports.length > 0) {
|
|
457
|
+
fallbackStats.noAllowImports++;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
originalSourceCache.set(file, rawSource); // For TransformContext.source
|
|
464
|
+
processedSourceCache.set(file, processedSource); // For potential reuse in cache fast path
|
|
465
|
+
return { id: file, source: processedSource, filepath: file };
|
|
466
|
+
})
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
debugTimeEnd('buildStart:readFiles');
|
|
470
|
+
|
|
471
|
+
const inputs = inputsOrNull.filter(
|
|
472
|
+
(i): i is NonNullable<typeof i> => i !== null
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
if (fallbackFiles.size > 0) {
|
|
476
|
+
const breakdown: string[] = [];
|
|
477
|
+
if (fallbackStats.disallowedImports > 0) {
|
|
478
|
+
breakdown.push(`${fallbackStats.disallowedImports} with disallowed imports`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
console.info(
|
|
482
|
+
`[xmdx] Pre-detected ${fallbackFiles.size} files with patterns incompatible with markdown-rs (delegating to Astro MDX)` +
|
|
483
|
+
(breakdown.length > 0 ? ` [${breakdown.join(', ')}]` : '')
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Log top disallowed import sources for debugging when many files fallback
|
|
487
|
+
if (disallowedImportSources.size > 0 && fallbackFiles.size >= 10) {
|
|
488
|
+
const topSources = Array.from(disallowedImportSources.entries())
|
|
489
|
+
.sort((a, b) => b[1] - a[1])
|
|
490
|
+
.slice(0, 5)
|
|
491
|
+
.map(([src, count]) => `${src} (${count})`);
|
|
492
|
+
console.info(
|
|
493
|
+
`[xmdx] Top disallowed import sources: ${topSources.join(', ')}`
|
|
494
|
+
);
|
|
495
|
+
console.info(
|
|
496
|
+
`[xmdx] Tip: Add these to your preset's allowImports to reduce fallback rate`
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (inputs.length === 0) {
|
|
502
|
+
debugTimeEnd('buildStart:total');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
debugTime('buildStart:batchCompile');
|
|
508
|
+
debugTime('buildStart:shikiInit');
|
|
509
|
+
|
|
510
|
+
// Start Shiki init in parallel with batch compile (Shiki doesn't depend on compile results)
|
|
511
|
+
const shikiPromise = shikiManager.init();
|
|
512
|
+
|
|
513
|
+
// Separate MD and MDX files for different compilation paths
|
|
514
|
+
const mdInputs = inputs.filter(i => !i.filepath?.endsWith('.mdx'));
|
|
515
|
+
const mdxInputs = inputs.filter(i => i.filepath?.endsWith('.mdx'));
|
|
516
|
+
|
|
517
|
+
debugLog(`Separated: ${mdInputs.length} MD files, ${mdxInputs.length} MDX files`);
|
|
518
|
+
|
|
519
|
+
// Batch compile with parallel processing
|
|
520
|
+
const binding = providedBinding ?? (await loadXmdxBinding());
|
|
521
|
+
|
|
522
|
+
// Compile MD files to complete Astro modules via Rust (no TypeScript wrapping needed)
|
|
523
|
+
let mdStats = { succeeded: 0, total: 0, failed: 0, processingTimeMs: 0 };
|
|
524
|
+
if (mdInputs.length > 0) {
|
|
525
|
+
const mdBatchResult = binding.compileBatchToModule(mdInputs, {
|
|
526
|
+
continueOnError: true,
|
|
527
|
+
config: compilerOptions,
|
|
528
|
+
});
|
|
529
|
+
mdStats = mdBatchResult.stats;
|
|
530
|
+
|
|
531
|
+
// Cache MD module results
|
|
532
|
+
for (const result of mdBatchResult.results) {
|
|
533
|
+
if (result.result) {
|
|
534
|
+
moduleCompilationCache.set(result.id, {
|
|
535
|
+
...result.result,
|
|
536
|
+
originalSource: originalSourceCache.get(result.id),
|
|
537
|
+
processedSource: processedSourceCache.get(result.id),
|
|
538
|
+
});
|
|
539
|
+
} else if (result.error) {
|
|
540
|
+
// Track compilation failures for fallback
|
|
541
|
+
fallbackFiles.add(result.id);
|
|
542
|
+
fallbackReasons.set(result.id, result.error);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Compile MDX files with mdxjs-rs
|
|
548
|
+
let mdxStats = { succeeded: 0, total: 0, failed: 0, processingTimeMs: 0 };
|
|
549
|
+
if (mdxInputs.length > 0) {
|
|
550
|
+
const mdxBatchResult = binding.compileMdxBatch(mdxInputs, {
|
|
551
|
+
continueOnError: true,
|
|
552
|
+
config: compilerOptions,
|
|
553
|
+
});
|
|
554
|
+
mdxStats = mdxBatchResult.stats;
|
|
555
|
+
|
|
556
|
+
// Cache MDX results
|
|
557
|
+
for (const result of mdxBatchResult.results) {
|
|
558
|
+
if (result.result) {
|
|
559
|
+
mdxCompilationCache.set(result.id, {
|
|
560
|
+
...result.result,
|
|
561
|
+
originalSource: originalSourceCache.get(result.id),
|
|
562
|
+
processedSource: processedSourceCache.get(result.id),
|
|
563
|
+
});
|
|
564
|
+
} else if (result.error) {
|
|
565
|
+
// Track MDX compilation failures for fallback
|
|
566
|
+
fallbackFiles.add(result.id);
|
|
567
|
+
fallbackReasons.set(result.id, result.error);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
debugTimeEnd('buildStart:batchCompile');
|
|
573
|
+
|
|
574
|
+
const totalSucceeded = mdStats.succeeded + mdxStats.succeeded;
|
|
575
|
+
const totalFiles = mdStats.total + mdxStats.total;
|
|
576
|
+
const totalTime = mdStats.processingTimeMs + mdxStats.processingTimeMs;
|
|
577
|
+
|
|
578
|
+
console.info(
|
|
579
|
+
`[xmdx] Batch compiled ${totalSucceeded}/${totalFiles} files in ${totalTime.toFixed(0)}ms` +
|
|
580
|
+
(mdxInputs.length > 0 ? ` (${mdxStats.succeeded} MDX via mdxjs-rs)` : '')
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
// Batch esbuild transformation for fast-path eligible files
|
|
584
|
+
const esbuildStartTime = performance.now();
|
|
585
|
+
const jsxInputs: Array<{ id: string; virtualId: string; jsx: string }> = [];
|
|
586
|
+
|
|
587
|
+
// Wait for Shiki initialization (started in parallel with batch compile)
|
|
588
|
+
const resolvedShiki = await shikiPromise;
|
|
589
|
+
debugTimeEnd('buildStart:shikiInit');
|
|
590
|
+
|
|
591
|
+
// Normalize starlightComponents for TransformContext
|
|
592
|
+
const normalizedStarlightComponents = normalizeStarlightComponents(starlightComponents);
|
|
593
|
+
|
|
594
|
+
debugTime('buildStart:pipelineProcessing');
|
|
595
|
+
|
|
596
|
+
// Collect all compiled entries for batch esbuild processing
|
|
597
|
+
// MD files: complete modules from Rust (no TypeScript wrapping)
|
|
598
|
+
// MDX files: wrapped via wrapMdxModule (mdxjs-rs output)
|
|
599
|
+
const mdModuleEntries: Array<[string, CachedModuleResult]> = [];
|
|
600
|
+
for (const [filename, cached] of moduleCompilationCache) {
|
|
601
|
+
mdModuleEntries.push([filename, cached]);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// MDX files use the mdxjs-rs output directly
|
|
605
|
+
const mdxFastPathEntries: Array<[string, CachedMdxResult]> = [];
|
|
606
|
+
for (const [filename, cached] of mdxCompilationCache) {
|
|
607
|
+
mdxFastPathEntries.push([filename, cached]);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Process MD module entries in chunks (complete modules from Rust)
|
|
611
|
+
const PIPELINE_CHUNK_SIZE = 50;
|
|
612
|
+
for (let i = 0; i < mdModuleEntries.length; i += PIPELINE_CHUNK_SIZE) {
|
|
613
|
+
const chunk = mdModuleEntries.slice(i, i + PIPELINE_CHUNK_SIZE);
|
|
614
|
+
const chunkResults = await Promise.all(
|
|
615
|
+
chunk.map(async ([filename, cached]) => {
|
|
616
|
+
// Parse frontmatter
|
|
617
|
+
let frontmatter: Record<string, unknown> = {};
|
|
618
|
+
if (cached.frontmatterJson) {
|
|
619
|
+
try {
|
|
620
|
+
frontmatter = JSON.parse(cached.frontmatterJson) as Record<string, unknown>;
|
|
621
|
+
} catch {
|
|
622
|
+
frontmatter = {};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const headings = cached.headings || [];
|
|
626
|
+
|
|
627
|
+
// Use complete module code from Rust (no wrapHtmlInJsxModule needed)
|
|
628
|
+
const jsxCode = cached.code;
|
|
629
|
+
|
|
630
|
+
// Get source for hooks
|
|
631
|
+
const sourceForHooks =
|
|
632
|
+
originalSourceCache.get(filename) ??
|
|
633
|
+
cached.originalSource ??
|
|
634
|
+
processedSourceCache.get(filename) ??
|
|
635
|
+
cached.processedSource ??
|
|
636
|
+
'';
|
|
637
|
+
|
|
638
|
+
// Create transform context and run pipeline
|
|
639
|
+
const ctx: TransformContext = {
|
|
640
|
+
code: jsxCode,
|
|
641
|
+
source: sourceForHooks,
|
|
642
|
+
filename,
|
|
643
|
+
frontmatter,
|
|
644
|
+
headings,
|
|
645
|
+
registry,
|
|
646
|
+
config: {
|
|
647
|
+
expressiveCode,
|
|
648
|
+
starlightComponents: normalizedStarlightComponents,
|
|
649
|
+
shiki: shikiManager.forCode(jsxCode, resolvedShiki),
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
const transformed = await transformPipeline(ctx);
|
|
654
|
+
const virtualId = `${VIRTUAL_MODULE_PREFIX}${filename}${OUTPUT_EXTENSION}`;
|
|
655
|
+
return { id: filename, virtualId, jsx: transformed.code };
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
jsxInputs.push(...chunkResults);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Process MDX files in chunks (mdxjs-rs output is already JavaScript)
|
|
662
|
+
for (let i = 0; i < mdxFastPathEntries.length; i += PIPELINE_CHUNK_SIZE) {
|
|
663
|
+
const chunk = mdxFastPathEntries.slice(i, i + PIPELINE_CHUNK_SIZE);
|
|
664
|
+
const chunkResults = await Promise.all(
|
|
665
|
+
chunk.map(async ([filename, cached]) => {
|
|
666
|
+
// Parse frontmatter
|
|
667
|
+
let frontmatter: Record<string, unknown> = {};
|
|
668
|
+
if (cached.frontmatterJson) {
|
|
669
|
+
try {
|
|
670
|
+
frontmatter = JSON.parse(cached.frontmatterJson) as Record<string, unknown>;
|
|
671
|
+
} catch {
|
|
672
|
+
frontmatter = {};
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const headings = cached.headings || [];
|
|
676
|
+
|
|
677
|
+
// Wrap MDX output in Astro component format
|
|
678
|
+
const jsxCode = wrapMdxModule(cached.code, {
|
|
679
|
+
frontmatter,
|
|
680
|
+
headings,
|
|
681
|
+
registry,
|
|
682
|
+
}, filename);
|
|
683
|
+
|
|
684
|
+
// Get source for hooks
|
|
685
|
+
const sourceForHooks =
|
|
686
|
+
originalSourceCache.get(filename) ??
|
|
687
|
+
cached.originalSource ??
|
|
688
|
+
processedSourceCache.get(filename) ??
|
|
689
|
+
cached.processedSource ??
|
|
690
|
+
'';
|
|
691
|
+
|
|
692
|
+
// Create transform context and run pipeline
|
|
693
|
+
const ctx: TransformContext = {
|
|
694
|
+
code: jsxCode,
|
|
695
|
+
source: sourceForHooks,
|
|
696
|
+
filename,
|
|
697
|
+
frontmatter,
|
|
698
|
+
headings,
|
|
699
|
+
registry,
|
|
700
|
+
config: {
|
|
701
|
+
expressiveCode,
|
|
702
|
+
starlightComponents: normalizedStarlightComponents,
|
|
703
|
+
shiki: shikiManager.forCode(jsxCode, resolvedShiki),
|
|
704
|
+
},
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
const transformed = await transformPipeline(ctx);
|
|
708
|
+
const virtualId = `${VIRTUAL_MODULE_PREFIX}${filename}${OUTPUT_EXTENSION}`;
|
|
709
|
+
return { id: filename, virtualId, jsx: transformed.code };
|
|
710
|
+
})
|
|
711
|
+
);
|
|
712
|
+
jsxInputs.push(...chunkResults);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
debugTimeEnd('buildStart:pipelineProcessing');
|
|
716
|
+
debugLog(`Pipeline processed ${jsxInputs.length} files for esbuild batch (${mdModuleEntries.length} MD modules, ${mdxFastPathEntries.length} MDX)`);
|
|
717
|
+
|
|
718
|
+
if (jsxInputs.length > 0) {
|
|
719
|
+
debugTime('buildStart:esbuild');
|
|
720
|
+
|
|
721
|
+
// Batch transform all JSX through esbuild
|
|
722
|
+
// Use parallel workers for large batches (>= 100 files)
|
|
723
|
+
try {
|
|
724
|
+
const useParallel = jsxInputs.length >= 100;
|
|
725
|
+
|
|
726
|
+
let usedParallel = false;
|
|
727
|
+
if (useParallel) {
|
|
728
|
+
// Parallel worker-based esbuild for large batches
|
|
729
|
+
try {
|
|
730
|
+
debugLog(`Using parallel esbuild workers for ${jsxInputs.length} files`);
|
|
731
|
+
const parallelResults = await runParallelEsbuild(
|
|
732
|
+
jsxInputs.map((input) => ({ id: input.id, jsx: input.jsx }))
|
|
733
|
+
);
|
|
734
|
+
for (const [id, result] of parallelResults) {
|
|
735
|
+
esbuildCache.set(id, { code: result.code, map: result.map as SourceMapInput });
|
|
736
|
+
}
|
|
737
|
+
usedParallel = true;
|
|
738
|
+
} catch (workerErr) {
|
|
739
|
+
// Workers failed - fall through to single-threaded mode
|
|
740
|
+
debugLog(`Worker esbuild failed, falling back to single-threaded: ${workerErr}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!usedParallel) {
|
|
745
|
+
// Single-threaded esbuild for small batches (lower overhead)
|
|
746
|
+
const entryMap = new Map<string, { id: string; jsx: string }>();
|
|
747
|
+
for (let i = 0; i < jsxInputs.length; i++) {
|
|
748
|
+
const entry = `entry${i}.jsx`;
|
|
749
|
+
const input = jsxInputs[i]!;
|
|
750
|
+
entryMap.set(entry, { id: input.id, jsx: input.jsx });
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const result: BuildResult = await esbuildBuild({
|
|
754
|
+
write: false,
|
|
755
|
+
bundle: false,
|
|
756
|
+
format: 'esm',
|
|
757
|
+
sourcemap: 'external',
|
|
758
|
+
loader: { '.jsx': 'jsx' },
|
|
759
|
+
jsx: 'transform',
|
|
760
|
+
jsxFactory: '_jsx',
|
|
761
|
+
jsxFragment: '_Fragment',
|
|
762
|
+
entryPoints: Array.from(entryMap.keys()),
|
|
763
|
+
outdir: 'out',
|
|
764
|
+
plugins: [
|
|
765
|
+
{
|
|
766
|
+
name: 'xmdx-virtual-jsx',
|
|
767
|
+
setup(build) {
|
|
768
|
+
build.onResolve({ filter: /^entry\d+\.jsx$/ }, (args) => {
|
|
769
|
+
return { path: args.path, namespace: 'xmdx-jsx' };
|
|
770
|
+
});
|
|
771
|
+
build.onResolve({ filter: /.*/ }, (args) => {
|
|
772
|
+
return { path: args.path, external: true };
|
|
773
|
+
});
|
|
774
|
+
build.onLoad({ filter: /.*/, namespace: 'xmdx-jsx' }, (args) => {
|
|
775
|
+
const entry = entryMap.get(args.path);
|
|
776
|
+
return entry ? { contents: entry.jsx, loader: 'jsx' } : null;
|
|
777
|
+
});
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
],
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
for (const output of result.outputFiles || []) {
|
|
784
|
+
const basename = path.basename(output.path);
|
|
785
|
+
if (basename.endsWith('.map')) continue;
|
|
786
|
+
const entryName = basename.replace(/\.js$/, '.jsx');
|
|
787
|
+
const entry = entryMap.get(entryName);
|
|
788
|
+
if (entry) {
|
|
789
|
+
const mapOutput = result.outputFiles?.find((o) => o.path === output.path + '.map');
|
|
790
|
+
esbuildCache.set(entry.id, {
|
|
791
|
+
code: output.text,
|
|
792
|
+
map: mapOutput?.text as SourceMapInput | undefined,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const esbuildEndTime = performance.now();
|
|
799
|
+
console.info(
|
|
800
|
+
`[xmdx] Batch esbuild transformed ${esbuildCache.size} files in ${(esbuildEndTime - esbuildStartTime).toFixed(0)}ms` +
|
|
801
|
+
(usedParallel ? ' (parallel workers)' : '')
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
debugTimeEnd('buildStart:esbuild');
|
|
805
|
+
} catch (esbuildErr) {
|
|
806
|
+
debugTimeEnd('buildStart:esbuild');
|
|
807
|
+
this.warn(
|
|
808
|
+
`[xmdx] Batch esbuild failed, will use individual transforms: ${esbuildErr}`
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Persist caches for subsequent build passes (SSR → Client)
|
|
814
|
+
for (const [k, v] of esbuildCache) {
|
|
815
|
+
persistentCache.esbuild.set(k, v);
|
|
816
|
+
}
|
|
817
|
+
for (const [k, v] of moduleCompilationCache) {
|
|
818
|
+
persistentCache.moduleCompilation.set(k, v);
|
|
819
|
+
}
|
|
820
|
+
for (const [k, v] of mdxCompilationCache) {
|
|
821
|
+
persistentCache.mdxCompilation.set(k, v);
|
|
822
|
+
}
|
|
823
|
+
for (const file of fallbackFiles) {
|
|
824
|
+
persistentCache.fallbackFiles.add(file);
|
|
825
|
+
}
|
|
826
|
+
for (const [k, v] of fallbackReasons) {
|
|
827
|
+
persistentCache.fallbackReasons.set(k, v);
|
|
828
|
+
}
|
|
829
|
+
debugLog(`Persisted ${persistentCache.esbuild.size} esbuild results for subsequent passes`);
|
|
830
|
+
|
|
831
|
+
debugTimeEnd('buildStart:total');
|
|
832
|
+
} catch (err) {
|
|
833
|
+
debugTimeEnd('buildStart:total');
|
|
834
|
+
this.warn(
|
|
835
|
+
`[xmdx] Batch compile skipped due to binding load failure: ${err}`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
|
|
840
|
+
async resolveId(sourceId, importer) {
|
|
841
|
+
if (sourceId.startsWith(VIRTUAL_MODULE_PREFIX)) {
|
|
842
|
+
return sourceId;
|
|
843
|
+
}
|
|
844
|
+
const normalizedImporter = stripQuery(unwrapVirtual(importer) ?? '');
|
|
845
|
+
const normalizedSource = unwrapVirtual(sourceId) ?? sourceId;
|
|
846
|
+
const cleanId = stripQuery(normalizedSource);
|
|
847
|
+
if (!include(cleanId)) {
|
|
848
|
+
if (
|
|
849
|
+
importer?.startsWith(VIRTUAL_MODULE_PREFIX) &&
|
|
850
|
+
normalizedImporter &&
|
|
851
|
+
!path.isAbsolute(sourceId) &&
|
|
852
|
+
sourceId.startsWith('.')
|
|
853
|
+
) {
|
|
854
|
+
return path.resolve(path.dirname(normalizedImporter), sourceId);
|
|
855
|
+
}
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
const resolved = await this.resolve(cleanId, normalizedImporter, {
|
|
859
|
+
skipSelf: true,
|
|
860
|
+
});
|
|
861
|
+
const fallback = (): string => {
|
|
862
|
+
if (path.isAbsolute(cleanId)) {
|
|
863
|
+
return cleanId;
|
|
864
|
+
}
|
|
865
|
+
if (normalizedImporter) {
|
|
866
|
+
return path.resolve(path.dirname(normalizedImporter), cleanId);
|
|
867
|
+
}
|
|
868
|
+
return cleanId;
|
|
869
|
+
};
|
|
870
|
+
const resolvedId =
|
|
871
|
+
resolved && resolved.id
|
|
872
|
+
? stripQuery(unwrapVirtual(resolved.id) ?? resolved.id)
|
|
873
|
+
: fallback();
|
|
874
|
+
|
|
875
|
+
// Pre-detected fallback files should be handled by Astro's MDX plugin
|
|
876
|
+
// which has proper remark-directive support and user-configured plugins
|
|
877
|
+
if (fallbackFiles.has(resolvedId)) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Dev mode pre-detection: check if file needs fallback before returning virtualId
|
|
882
|
+
// This ensures dev mode delegates problematic files to Astro MDX just like build mode does
|
|
883
|
+
if (resolvedConfig?.command !== 'build') {
|
|
884
|
+
try {
|
|
885
|
+
const source = await readFile(resolvedId, 'utf8');
|
|
886
|
+
let processedSource = source;
|
|
887
|
+
for (const preprocessHook of hooks.preprocess) {
|
|
888
|
+
processedSource = preprocessHook(processedSource, resolvedId);
|
|
889
|
+
}
|
|
890
|
+
const detection = detectProblematicMdxPatterns(processedSource, mdxOptions);
|
|
891
|
+
if (detection.hasProblematicPatterns) {
|
|
892
|
+
fallbackFiles.add(resolvedId);
|
|
893
|
+
fallbackReasons.set(resolvedId, detection.reason ?? 'Pre-detected problematic MDX patterns (dev mode)');
|
|
894
|
+
return null; // Delegate to Astro's MDX plugin
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
// File read failed, let normal path handle it
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const virtualId = `${VIRTUAL_MODULE_PREFIX}${resolvedId}${OUTPUT_EXTENSION}`;
|
|
902
|
+
sourceLookup.set(virtualId, resolvedId);
|
|
903
|
+
return virtualId;
|
|
904
|
+
},
|
|
905
|
+
|
|
906
|
+
async load(id) {
|
|
907
|
+
if (!id.startsWith(VIRTUAL_MODULE_PREFIX)) {
|
|
908
|
+
return null;
|
|
909
|
+
}
|
|
910
|
+
const filename =
|
|
911
|
+
sourceLookup.get(id) ??
|
|
912
|
+
stripQuery(id.slice(VIRTUAL_MODULE_PREFIX.length).replace(new RegExp(`${OUTPUT_EXTENSION.replace('.', '\\.')}$`), ''));
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
// FASTEST PATH: Check esbuild cache first (O(1) lookup, populated in buildStart)
|
|
916
|
+
const loadStart = LOAD_PROFILE ? performance.now() : 0;
|
|
917
|
+
const cachedEsbuildResult = esbuildCache.get(filename);
|
|
918
|
+
if (cachedEsbuildResult) {
|
|
919
|
+
processedFiles.add(filename);
|
|
920
|
+
if (loadProfiler) {
|
|
921
|
+
const elapsed = performance.now() - loadStart;
|
|
922
|
+
loadProfiler.esbuildCacheHits++;
|
|
923
|
+
loadProfiler.recordFile(filename, elapsed);
|
|
924
|
+
}
|
|
925
|
+
return cachedEsbuildResult;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Check cache FIRST, before any file I/O (populated in build mode by buildStart)
|
|
929
|
+
const cachedModule = moduleCompilationCache.get(filename);
|
|
930
|
+
const cachedMdx = mdxCompilationCache.get(filename);
|
|
931
|
+
const isMdx = filename.endsWith('.mdx');
|
|
932
|
+
|
|
933
|
+
// FAST PATH: MD files with complete modules from Rust
|
|
934
|
+
if (cachedModule && !isMdx) {
|
|
935
|
+
const startTime = performance.now();
|
|
936
|
+
let frontmatter: Record<string, unknown> = {};
|
|
937
|
+
if (cachedModule.frontmatterJson) {
|
|
938
|
+
try {
|
|
939
|
+
frontmatter = JSON.parse(cachedModule.frontmatterJson) as Record<string, unknown>;
|
|
940
|
+
} catch {
|
|
941
|
+
frontmatter = {};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const headings = cachedModule.headings || [];
|
|
945
|
+
|
|
946
|
+
// Use complete module code from Rust (no wrapHtmlInJsxModule needed)
|
|
947
|
+
const result: CompileResult = {
|
|
948
|
+
code: cachedModule.code,
|
|
949
|
+
map: null,
|
|
950
|
+
frontmatter_json: cachedModule.frontmatterJson,
|
|
951
|
+
headings,
|
|
952
|
+
imports: [],
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
const endTime = performance.now();
|
|
956
|
+
totalProcessingTimeMs += endTime - startTime;
|
|
957
|
+
processedFiles.add(filename);
|
|
958
|
+
|
|
959
|
+
const normalizedStarlightComponents = normalizeStarlightComponents(starlightComponents);
|
|
960
|
+
const sourceForHooks =
|
|
961
|
+
originalSourceCache.get(filename) ??
|
|
962
|
+
cachedModule.originalSource ??
|
|
963
|
+
processedSourceCache.get(filename) ??
|
|
964
|
+
cachedModule.processedSource ??
|
|
965
|
+
(await readFile(filename, 'utf8'));
|
|
966
|
+
const ctx: TransformContext = {
|
|
967
|
+
code: result.code,
|
|
968
|
+
source: sourceForHooks,
|
|
969
|
+
filename,
|
|
970
|
+
frontmatter,
|
|
971
|
+
headings,
|
|
972
|
+
registry,
|
|
973
|
+
config: {
|
|
974
|
+
expressiveCode,
|
|
975
|
+
starlightComponents: normalizedStarlightComponents,
|
|
976
|
+
shiki: await shikiManager.getFor(result.code),
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
const tpStart = LOAD_PROFILE ? performance.now() : 0;
|
|
981
|
+
const transformed = await transformPipeline(ctx);
|
|
982
|
+
result.code = transformed.code;
|
|
983
|
+
if (loadProfiler) loadProfiler.record('transform-pipeline', performance.now() - tpStart);
|
|
984
|
+
|
|
985
|
+
const esStart = LOAD_PROFILE ? performance.now() : 0;
|
|
986
|
+
const esbuildResult = await transformWithEsbuild(result.code, id, ESBUILD_JSX_CONFIG);
|
|
987
|
+
if (loadProfiler) loadProfiler.record('esbuild', performance.now() - esStart);
|
|
988
|
+
|
|
989
|
+
if (loadProfiler) {
|
|
990
|
+
const elapsed = performance.now() - loadStart;
|
|
991
|
+
loadProfiler.cacheHits++;
|
|
992
|
+
loadProfiler.recordFile(filename, elapsed);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return {
|
|
996
|
+
code: esbuildResult.code,
|
|
997
|
+
map: esbuildResult.map ?? result.map ?? undefined,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// FAST PATH: MDX files compiled via mdxjs-rs
|
|
1002
|
+
if (cachedMdx && isMdx) {
|
|
1003
|
+
const startTime = performance.now();
|
|
1004
|
+
let frontmatter: Record<string, unknown> = {};
|
|
1005
|
+
if (cachedMdx.frontmatterJson) {
|
|
1006
|
+
try {
|
|
1007
|
+
frontmatter = JSON.parse(cachedMdx.frontmatterJson) as Record<string, unknown>;
|
|
1008
|
+
} catch {
|
|
1009
|
+
frontmatter = {};
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
const headings = cachedMdx.headings || [];
|
|
1013
|
+
|
|
1014
|
+
// Wrap MDX output in Astro component format
|
|
1015
|
+
const jsxCode = wrapMdxModule(cachedMdx.code, {
|
|
1016
|
+
frontmatter,
|
|
1017
|
+
headings,
|
|
1018
|
+
registry,
|
|
1019
|
+
}, filename);
|
|
1020
|
+
|
|
1021
|
+
const result: CompileResult = {
|
|
1022
|
+
code: jsxCode,
|
|
1023
|
+
map: null,
|
|
1024
|
+
frontmatter_json: cachedMdx.frontmatterJson,
|
|
1025
|
+
headings,
|
|
1026
|
+
imports: [],
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const endTime = performance.now();
|
|
1030
|
+
totalProcessingTimeMs += endTime - startTime;
|
|
1031
|
+
processedFiles.add(filename);
|
|
1032
|
+
|
|
1033
|
+
const normalizedStarlightComponents = normalizeStarlightComponents(starlightComponents);
|
|
1034
|
+
const sourceForHooks =
|
|
1035
|
+
originalSourceCache.get(filename) ??
|
|
1036
|
+
cachedMdx.originalSource ??
|
|
1037
|
+
processedSourceCache.get(filename) ??
|
|
1038
|
+
cachedMdx.processedSource ??
|
|
1039
|
+
(await readFile(filename, 'utf8'));
|
|
1040
|
+
const ctx: TransformContext = {
|
|
1041
|
+
code: result.code,
|
|
1042
|
+
source: sourceForHooks,
|
|
1043
|
+
filename,
|
|
1044
|
+
frontmatter,
|
|
1045
|
+
headings,
|
|
1046
|
+
registry,
|
|
1047
|
+
config: {
|
|
1048
|
+
expressiveCode,
|
|
1049
|
+
starlightComponents: normalizedStarlightComponents,
|
|
1050
|
+
shiki: await shikiManager.getFor(result.code),
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
|
|
1054
|
+
const tpStart = LOAD_PROFILE ? performance.now() : 0;
|
|
1055
|
+
const transformed = await transformPipeline(ctx);
|
|
1056
|
+
result.code = transformed.code;
|
|
1057
|
+
if (loadProfiler) loadProfiler.record('transform-pipeline', performance.now() - tpStart);
|
|
1058
|
+
|
|
1059
|
+
const esStart = LOAD_PROFILE ? performance.now() : 0;
|
|
1060
|
+
const esbuildResult = await transformWithEsbuild(result.code, id, ESBUILD_JSX_CONFIG);
|
|
1061
|
+
if (loadProfiler) loadProfiler.record('esbuild', performance.now() - esStart);
|
|
1062
|
+
|
|
1063
|
+
if (loadProfiler) {
|
|
1064
|
+
const elapsed = performance.now() - loadStart;
|
|
1065
|
+
loadProfiler.cacheHits++;
|
|
1066
|
+
loadProfiler.recordFile(filename, elapsed);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
code: esbuildResult.code,
|
|
1071
|
+
map: esbuildResult.map ?? result.map ?? undefined,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Lazy initialize compiler on first use (only needed for cache miss path)
|
|
1076
|
+
if (loadProfiler) loadProfiler.cacheMisses++;
|
|
1077
|
+
const currentCompiler = await getCompiler();
|
|
1078
|
+
|
|
1079
|
+
// Only read file if cache miss
|
|
1080
|
+
const source = await readFile(filename, 'utf8');
|
|
1081
|
+
originalSourceCache.set(filename, source);
|
|
1082
|
+
|
|
1083
|
+
// Apply preprocess hooks
|
|
1084
|
+
let processedSource = source;
|
|
1085
|
+
for (const preprocessHook of hooks.preprocess) {
|
|
1086
|
+
processedSource = preprocessHook(processedSource, filename);
|
|
1087
|
+
}
|
|
1088
|
+
processedSourceCache.set(filename, processedSource);
|
|
1089
|
+
|
|
1090
|
+
// Early detection of problematic patterns - skip to fallback
|
|
1091
|
+
// Note: Pre-detected files from buildStart are handled by resolveId returning null
|
|
1092
|
+
// This catches files that weren't pre-detected (e.g., preprocess hooks revealed the pattern)
|
|
1093
|
+
const detection = detectProblematicMdxPatterns(processedSource, mdxOptions);
|
|
1094
|
+
if (detection.hasProblematicPatterns) {
|
|
1095
|
+
this.warn(
|
|
1096
|
+
`[xmdx] Skipping ${filename}: ${detection.reason ?? 'contains patterns incompatible with markdown-rs'}`
|
|
1097
|
+
);
|
|
1098
|
+
fallbackFiles.add(filename);
|
|
1099
|
+
fallbackReasons.set(filename, detection.reason ?? 'Detected problematic MDX patterns');
|
|
1100
|
+
// Use @mdx-js/mdx as fallback compiler for runtime-detected files
|
|
1101
|
+
return compileFallbackModule(filename, processedSource, id, registry, hasStarlightConfigured);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
const startTime = performance.now();
|
|
1105
|
+
const compileStart = LOAD_PROFILE ? performance.now() : 0;
|
|
1106
|
+
let result: CompileResult;
|
|
1107
|
+
let frontmatter: Record<string, unknown> = {};
|
|
1108
|
+
let headings: Array<{ depth: number; slug: string; text: string }> = [];
|
|
1109
|
+
|
|
1110
|
+
// MDX files: Use mdxjs-rs for full MDX support (JSX, ESM imports, etc.)
|
|
1111
|
+
if (isMdx) {
|
|
1112
|
+
const binding = await loadXmdxBinding();
|
|
1113
|
+
|
|
1114
|
+
// Compile single MDX file with mdxjs-rs
|
|
1115
|
+
const mdxBatchResult = binding.compileMdxBatch(
|
|
1116
|
+
[{ id: filename, source: processedSource }],
|
|
1117
|
+
{ continueOnError: false, config: compilerOptions }
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
const mdxResult = mdxBatchResult.results[0];
|
|
1121
|
+
if (mdxResult?.error) {
|
|
1122
|
+
throw new Error(`MDX compilation failed: ${mdxResult.error}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (!mdxResult?.result) {
|
|
1125
|
+
throw new Error(`MDX compilation returned no result for ${filename}`);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// Parse frontmatter and headings
|
|
1129
|
+
if (mdxResult.result.frontmatterJson) {
|
|
1130
|
+
try {
|
|
1131
|
+
frontmatter = JSON.parse(mdxResult.result.frontmatterJson) as Record<string, unknown>;
|
|
1132
|
+
} catch {
|
|
1133
|
+
frontmatter = {};
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
headings = mdxResult.result.headings || [];
|
|
1137
|
+
|
|
1138
|
+
// Wrap MDX output in Astro component format
|
|
1139
|
+
const jsxCode = wrapMdxModule(mdxResult.result.code, {
|
|
1140
|
+
frontmatter,
|
|
1141
|
+
headings,
|
|
1142
|
+
registry,
|
|
1143
|
+
}, filename);
|
|
1144
|
+
|
|
1145
|
+
result = {
|
|
1146
|
+
code: jsxCode,
|
|
1147
|
+
map: null,
|
|
1148
|
+
frontmatter_json: mdxResult.result.frontmatterJson ?? '',
|
|
1149
|
+
headings,
|
|
1150
|
+
imports: [],
|
|
1151
|
+
};
|
|
1152
|
+
} else if (IS_MDAST) {
|
|
1153
|
+
// MD files: Use markdown-rs via parseBlocks
|
|
1154
|
+
const binding = await loadXmdxBinding();
|
|
1155
|
+
|
|
1156
|
+
// Extract user imports BEFORE processing (user imports take precedence over registry)
|
|
1157
|
+
const userImports = extractImportStatements(processedSource);
|
|
1158
|
+
|
|
1159
|
+
// Strip frontmatter before passing to parseBlocks
|
|
1160
|
+
// Otherwise the mdast pipeline renders YAML as regular text
|
|
1161
|
+
const contentSource = stripFrontmatter(processedSource);
|
|
1162
|
+
|
|
1163
|
+
const parseResult = binding.parseBlocks(contentSource, {
|
|
1164
|
+
enable_directives: true,
|
|
1165
|
+
});
|
|
1166
|
+
headings = parseResult.headings;
|
|
1167
|
+
|
|
1168
|
+
// Extract frontmatter from original source (before stripping)
|
|
1169
|
+
const frontmatterResult = binding.parseFrontmatter(processedSource);
|
|
1170
|
+
frontmatter = frontmatterResult.frontmatter || {};
|
|
1171
|
+
|
|
1172
|
+
result = {
|
|
1173
|
+
code: blocksToJsx(parseResult.blocks, frontmatter, headings, registry, filename, userImports),
|
|
1174
|
+
map: null,
|
|
1175
|
+
frontmatter_json: JSON.stringify(frontmatter),
|
|
1176
|
+
headings,
|
|
1177
|
+
imports: [],
|
|
1178
|
+
};
|
|
1179
|
+
} else {
|
|
1180
|
+
const fileOptions = deriveFileOptions(filename, resolvedConfig?.root);
|
|
1181
|
+
result = currentCompiler.compile(processedSource, filename, fileOptions);
|
|
1182
|
+
if (result.frontmatter_json) {
|
|
1183
|
+
try {
|
|
1184
|
+
frontmatter = JSON.parse(result.frontmatter_json) as Record<string, unknown>;
|
|
1185
|
+
} catch {
|
|
1186
|
+
frontmatter = {};
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
headings = result.headings || [];
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (loadProfiler) loadProfiler.record('compile', performance.now() - compileStart);
|
|
1193
|
+
const endTime = performance.now();
|
|
1194
|
+
totalProcessingTimeMs += endTime - startTime;
|
|
1195
|
+
processedFiles.add(filename);
|
|
1196
|
+
|
|
1197
|
+
if (result.code == null || typeof result.code !== 'string') {
|
|
1198
|
+
throw new Error(`Compiler returned undefined or invalid code for ${filename}`);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (result.diagnostics?.warnings?.length) {
|
|
1202
|
+
for (const warning of result.diagnostics.warnings) {
|
|
1203
|
+
this.warn(`[xmdx] ${filename}:${warning.line}: ${warning.message}`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const normalizedStarlightComponents = normalizeStarlightComponents(starlightComponents);
|
|
1208
|
+
const ctx: TransformContext = {
|
|
1209
|
+
code: result.code,
|
|
1210
|
+
source,
|
|
1211
|
+
filename,
|
|
1212
|
+
frontmatter,
|
|
1213
|
+
headings,
|
|
1214
|
+
registry,
|
|
1215
|
+
config: {
|
|
1216
|
+
expressiveCode,
|
|
1217
|
+
starlightComponents: normalizedStarlightComponents,
|
|
1218
|
+
shiki: await shikiManager.getFor(result.code),
|
|
1219
|
+
},
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
const tpStart2 = LOAD_PROFILE ? performance.now() : 0;
|
|
1223
|
+
const transformed = await transformPipeline(ctx);
|
|
1224
|
+
result.code = transformed.code;
|
|
1225
|
+
if (loadProfiler) loadProfiler.record('transform-pipeline', performance.now() - tpStart2);
|
|
1226
|
+
|
|
1227
|
+
if (Array.isArray(result?.imports)) {
|
|
1228
|
+
for (const dep of result.imports) {
|
|
1229
|
+
if (dep?.path) {
|
|
1230
|
+
this.addWatchFile(dep.path);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const esStart2 = LOAD_PROFILE ? performance.now() : 0;
|
|
1236
|
+
const esbuildResult = await transformWithEsbuild(result.code, id, ESBUILD_JSX_CONFIG);
|
|
1237
|
+
if (loadProfiler) loadProfiler.record('esbuild', performance.now() - esStart2);
|
|
1238
|
+
|
|
1239
|
+
if (loadProfiler) {
|
|
1240
|
+
const elapsed = performance.now() - loadStart;
|
|
1241
|
+
loadProfiler.recordFile(filename, elapsed);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return {
|
|
1245
|
+
code: esbuildResult.code,
|
|
1246
|
+
map: esbuildResult.map ?? result.map ?? undefined,
|
|
1247
|
+
};
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
const message = (error as Error)?.message || String(error);
|
|
1250
|
+
const shouldFallback =
|
|
1251
|
+
message.includes('Vite module runner has been closed') ||
|
|
1252
|
+
message.includes('Markdown parser error') ||
|
|
1253
|
+
message.includes('Markdown parse error') ||
|
|
1254
|
+
message.includes('Transform failed') ||
|
|
1255
|
+
message.includes('Compiler returned undefined') ||
|
|
1256
|
+
message.includes('Cannot read properties of undefined') ||
|
|
1257
|
+
message.includes('Cannot read properties of null');
|
|
1258
|
+
|
|
1259
|
+
if (shouldFallback) {
|
|
1260
|
+
fallbackFiles.add(filename);
|
|
1261
|
+
fallbackReasons.set(filename, message);
|
|
1262
|
+
this.warn(`[xmdx] Falling back to @mdx-js/mdx for ${filename}: ${message}`);
|
|
1263
|
+
|
|
1264
|
+
// Try to invalidate the module in dev server mode
|
|
1265
|
+
const config = resolvedConfig as unknown as {
|
|
1266
|
+
server?: {
|
|
1267
|
+
moduleGraph?: {
|
|
1268
|
+
getModuleById: (id: string) => object | null;
|
|
1269
|
+
invalidateModule: (mod: object) => void;
|
|
1270
|
+
};
|
|
1271
|
+
};
|
|
1272
|
+
};
|
|
1273
|
+
if (config?.server?.moduleGraph) {
|
|
1274
|
+
const mod = config.server.moduleGraph.getModuleById(id);
|
|
1275
|
+
if (mod) {
|
|
1276
|
+
config.server.moduleGraph.invalidateModule(mod);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Re-read and process the file for fallback compilation
|
|
1281
|
+
const fallbackSource = await readFile(filename, 'utf8');
|
|
1282
|
+
let processedFallbackSource = fallbackSource;
|
|
1283
|
+
for (const preprocessHook of hooks.preprocess) {
|
|
1284
|
+
processedFallbackSource = preprocessHook(processedFallbackSource, filename);
|
|
1285
|
+
}
|
|
1286
|
+
return compileFallbackModule(filename, processedFallbackSource, id, registry, hasStarlightConfigured);
|
|
1287
|
+
}
|
|
1288
|
+
throw new Error(`[xmdx] Compile failed for ${filename}: ${message}`);
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
|
|
1292
|
+
async buildEnd() {
|
|
1293
|
+
if (loadProfiler) loadProfiler.dump(resolvedConfig?.root ?? '');
|
|
1294
|
+
|
|
1295
|
+
if (process.env.XMDX_STATS !== '1') return;
|
|
1296
|
+
|
|
1297
|
+
const totalFiles = processedFiles.size + fallbackFiles.size;
|
|
1298
|
+
|
|
1299
|
+
const stats = {
|
|
1300
|
+
timestamp: new Date().toISOString(),
|
|
1301
|
+
totalFiles,
|
|
1302
|
+
processedByXmdx: processedFiles.size,
|
|
1303
|
+
handledByAstro: fallbackFiles.size,
|
|
1304
|
+
handledByAstroRate:
|
|
1305
|
+
totalFiles > 0
|
|
1306
|
+
? `${((fallbackFiles.size / totalFiles) * 100).toFixed(2)}%`
|
|
1307
|
+
: '0%',
|
|
1308
|
+
preValidationSkips: {
|
|
1309
|
+
count: 0,
|
|
1310
|
+
files: [] as string[],
|
|
1311
|
+
},
|
|
1312
|
+
runtimeFallbacks: {
|
|
1313
|
+
count: fallbackFiles.size,
|
|
1314
|
+
files: Array.from(fallbackFiles).map((file) => ({
|
|
1315
|
+
file: file.replace(resolvedConfig?.root ?? '', ''),
|
|
1316
|
+
reason: fallbackReasons.get(file) ?? 'unknown',
|
|
1317
|
+
})),
|
|
1318
|
+
},
|
|
1319
|
+
fallbacks: fallbackFiles.size,
|
|
1320
|
+
fallbackRate:
|
|
1321
|
+
totalFiles > 0
|
|
1322
|
+
? `${((fallbackFiles.size / totalFiles) * 100).toFixed(2)}%`
|
|
1323
|
+
: '0%',
|
|
1324
|
+
fallbackFiles: Array.from(fallbackFiles).map((file) => ({
|
|
1325
|
+
file: file.replace(resolvedConfig?.root ?? '', ''),
|
|
1326
|
+
reason: fallbackReasons.get(file) ?? 'unknown',
|
|
1327
|
+
})),
|
|
1328
|
+
performance: {
|
|
1329
|
+
totalProcessingTimeMs: Math.round(totalProcessingTimeMs * 100) / 100,
|
|
1330
|
+
averageFileTimeMs:
|
|
1331
|
+
processedFiles.size > 0
|
|
1332
|
+
? Math.round((totalProcessingTimeMs / processedFiles.size) * 100) / 100
|
|
1333
|
+
: 0,
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
const outputPath = path.join(resolvedConfig?.root ?? '.', 'xmdx-stats.json');
|
|
1338
|
+
await writeFile(outputPath, JSON.stringify(stats, null, 2));
|
|
1339
|
+
console.info(`[xmdx] Stats written to ${outputPath}`);
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
}
|