hexo-renderer-mdx 1.0.4 → 1.1.1

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 (3) hide show
  1. package/README.md +11 -2
  2. package/index.js +509 -20
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -8,8 +8,9 @@ A [Hexo](https://hexo.io/) renderer plugin for [MDX](https://mdxjs.com/) - Markd
8
8
  - ⚛️ React component integration
9
9
  - 📝 Markdown compatibility
10
10
  - 🎨 Custom component support
11
- - ES6 import statements for external packages
12
- - �� Fast compilation with @mdx-js/mdx
11
+ - 📁 ES6 import statements for external packages
12
+ - Fast compilation with @mdx-js/mdx
13
+ - 🔄 Automatic hydration bundle rebuilds on `hexo generate` and when components change during `hexo server`
13
14
 
14
15
  ## Installation
15
16
 
@@ -211,6 +212,14 @@ hexo deploy
211
212
 
212
213
  The MDX files are compiled to static HTML - no JavaScript runtime needed on your site!
213
214
 
215
+ #### Client-side hydration bundles (auto-built)
216
+
217
+ If your MDX imports local React components, the renderer will emit a hydration entry in `public/.hexo-mdx-entry/` and automatically bundle it to `public/assets/mdx-hydrate-*.js`.
218
+
219
+ - `hexo generate` runs bundling automatically after generation; no manual esbuild step is required.
220
+ - During `hexo server`, component edits trigger targeted regeneration and bundling so the client asset stays fresh.
221
+ - Avoid keeping old `mdx-hydrate-*.js` files in `source/assets/`; Hexo would copy them into `public/assets` and overwrite the freshly bundled output.
222
+
214
223
  ## Usage
215
224
 
216
225
  After installation, you can create `.mdx` files in your `source/_posts` or `source` directory.
package/index.js CHANGED
@@ -5,6 +5,7 @@ const React = require('react');
5
5
  const fs = require('fs');
6
6
  const { createRequire } = require('module');
7
7
  const { pathToFileURL, fileURLToPath } = require('url');
8
+ const crypto = require('crypto');
8
9
 
9
10
  let babelRegistered = false;
10
11
  function ensureBabelRegister(filePath) {
@@ -88,12 +89,20 @@ async function loadCompile() {
88
89
  async function mdxRenderer(data) {
89
90
  const { text, path: filePath } = data;
90
91
 
92
+ // Initialize dependencies Set for tracking imported component files
93
+ if (!data.dependencies) {
94
+ data.dependencies = new Set();
95
+ }
96
+
91
97
  try {
92
98
  // Ensure Babel can handle JSX/TS imports from MDX files (e.g., local components).
93
99
  ensureBabelRegister(filePath);
94
100
 
95
101
  // Ensure compile function is loaded
96
102
  await loadCompile();
103
+
104
+ // Stable per-file hash to namespace hydration ids and bundles
105
+ const fileHash = crypto.createHash('md5').update(filePath).digest('hex').slice(0, 8);
97
106
 
98
107
  // Read the original file directly to bypass Hexo's template processing
99
108
  let content;
@@ -142,30 +151,43 @@ async function mdxRenderer(data) {
142
151
  // For primitive or function values, just set as default
143
152
  return { default: mod };
144
153
  };
154
+ // Collect components used so we can hydrate them client-side
155
+ const componentsForHydration = [];
145
156
  const dynamicImport = (specifier) => {
146
157
  const asString = String(specifier);
147
158
  const req = createRequire(filePath);
148
- // Check if it's already a file:// URL string
149
- if (asString.startsWith('file://')) {
150
- try {
151
- const fsPath = fileURLToPath(asString);
152
- return Promise.resolve(toModuleNamespace(req(fsPath)));
153
- } catch (err) {
154
- // Re-throw with better error message
155
- throw new Error(`Failed to require file:// URL: ${err.message}`);
156
- }
157
- }
159
+
160
+ // Resolve a filesystem path for this specifier
161
+ let fsPath;
158
162
  try {
159
- // Try to construct a URL to see if it's relative
160
- const resolvedUrl = new URL(asString, pathToFileURL(filePath));
161
- if (resolvedUrl.protocol === 'file:') {
162
- return Promise.resolve(toModuleNamespace(req(fileURLToPath(resolvedUrl))));
163
+ if (asString.startsWith('file://')) {
164
+ fsPath = fileURLToPath(asString);
165
+ } else {
166
+ const resolvedUrl = new URL(asString, pathToFileURL(filePath));
167
+ if (resolvedUrl.protocol === 'file:') {
168
+ fsPath = fileURLToPath(resolvedUrl);
169
+ }
163
170
  }
164
- return Promise.resolve(toModuleNamespace(req(asString)));
165
- } catch (urlErr) {
166
- // If URL construction failed, try bare require
167
- return Promise.resolve(toModuleNamespace(req(asString)));
171
+ } catch (e) {
172
+ // ignore - will try bare require
168
173
  }
174
+
175
+ // Create a placeholder component for server-side rendering
176
+ const placeholderId = `mdx-cmp-${fileHash}-${componentsForHydration.length + 1}`;
177
+ const Placeholder = (props) => {
178
+ return React.createElement('div', { 'data-mdx-component': placeholderId });
179
+ };
180
+
181
+ // Record mapping for hydration bundle (use filesystem path when available, otherwise the original specifier)
182
+ componentsForHydration.push({ id: placeholderId, spec: fsPath || asString });
183
+
184
+ // Register component file as a dependency so Hexo watches it for changes
185
+ if (fsPath && data.dependencies) {
186
+ data.dependencies.add(fsPath);
187
+ }
188
+
189
+ // Return an ES-like namespace with default export set to placeholder
190
+ return Promise.resolve({ default: Placeholder });
169
191
  };
170
192
 
171
193
  // Swap all occurrences of 'import(' (awaited or not) with our shim to avoid vm dynamic import callbacks.
@@ -180,8 +202,67 @@ async function mdxRenderer(data) {
180
202
  const html = renderToString(
181
203
  React.createElement(MDXContent, {})
182
204
  );
205
+
206
+ // If there are components to hydrate, generate a client bundle using esbuild (if available)
207
+ let finalHtml = html;
208
+ if (componentsForHydration.length > 0) {
209
+ try {
210
+ const esbuild = require('esbuild');
211
+ const os = require('os');
212
+ const tmpdir = os.tmpdir();
213
+ const hash = fileHash;
214
+ const outName = `mdx-hydrate-${hash}.js`;
215
+ // Output compiled hydration bundle and temporary entry into the site's public directory
216
+ const projectRoot = hexo && hexo.base_dir ? hexo.base_dir : process.cwd();
217
+ const publicDir = (hexo && hexo.public_dir) ? hexo.public_dir : require('path').join(projectRoot, 'public');
218
+ const outDir = require('path').join(publicDir, 'assets');
219
+ const entryPath = require('path').join(publicDir, '.hexo-mdx-entry', `mdx-entry-${hash}.mjs`);
220
+
221
+ const imports = componentsForHydration.map((c, i) => {
222
+ // Convert absolute path to relative path from entry directory
223
+ let importPath = c.spec;
224
+ if (require('path').isAbsolute(importPath)) {
225
+ importPath = require('path').relative(require('path').dirname(entryPath), importPath);
226
+ }
227
+ // Normalize slashes for JS import
228
+ importPath = importPath.replace(/\\/g, '/');
229
+ // Ensure relative imports start with ./ or ../
230
+ if (!importPath.startsWith('.')) {
231
+ importPath = './' + importPath;
232
+ }
233
+ return `import C${i} from ${JSON.stringify(importPath)};`;
234
+ }).join('\n');
235
+
236
+ const mapping = componentsForHydration.map((c, i) => ` '${c.id}': C${i}`).join(',\n');
237
+
238
+ const entrySource = `import React from 'react';\nimport { hydrateRoot } from 'react-dom/client';\n\n// Make React available globally for imported components\nwindow.React = React;\n\n${imports}\n\nconst mapping = {\n${mapping}\n};\n\nObject.keys(mapping).forEach(id => {\n const Comp = mapping[id];\n const el = document.querySelector('[data-mdx-component="'+id+'"]');\n if (el) {\n hydrateRoot(el, React.createElement(Comp, {}));\n }\n});\n`;
239
+
240
+ require('fs').mkdirSync(require('path').dirname(entryPath), { recursive: true });
241
+ require('fs').writeFileSync(entryPath, entrySource, 'utf8');
242
+ require('fs').mkdirSync(outDir, { recursive: true });
243
+
244
+ esbuild.buildSync({
245
+ entryPoints: [entryPath],
246
+ bundle: true,
247
+ format: 'esm',
248
+ outfile: require('path').join(outDir, outName),
249
+ platform: 'browser',
250
+ jsx: 'transform',
251
+ jsxFactory: 'React.createElement',
252
+ jsxFragment: 'React.Fragment',
253
+ minify: false,
254
+ absWorkingDir: process.cwd(),
255
+ loader: { '.jsx': 'jsx', '.js': 'js', '.mjs': 'js' }
256
+ });
257
+
258
+ // Hydration bundle is placed under /assets in the public dir
259
+ finalHtml = `<div id="mdx-root-${hash}">${html}</div><script type="module" src="/assets/${outName}"></script>`;
260
+ } catch (err) {
261
+ console.error('MDX hydration bundle failed:', err.message);
262
+ }
263
+ }
183
264
 
184
- return html;
265
+ return finalHtml;
185
266
  } catch (err) {
186
267
  // Provide more detailed error information
187
268
  const errorMsg = `MDX compilation failed for ${filePath}: ${err.message}`;
@@ -199,6 +280,414 @@ async function mdxRenderer(data) {
199
280
  * Register the MDX renderer with Hexo
200
281
  * Note: Using disableNunjucks: true to prevent template processing of {{ }} syntax
201
282
  */
202
- hexo.extend.renderer.register('mdx', 'html', mdxRenderer, {
283
+ const path = require('path');
284
+ const chokidar = require('chokidar');
285
+ const componentDependencies = new Map(); // Map of component path -> Set of MDX files that import it
286
+
287
+ // Bundle the entry file with esbuild to create the hydration client bundle
288
+ function bundleEntryToPublic() {
289
+ try {
290
+ const esbuild = require('esbuild');
291
+ const crypto = require('crypto');
292
+ const projectRoot = hexo && hexo.base_dir ? hexo.base_dir : process.cwd();
293
+ const publicDir = (hexo && hexo.public_dir) ? hexo.public_dir : path.join(projectRoot, 'public');
294
+
295
+ // Clear require cache for components before bundling to ensure fresh imports
296
+ Object.keys(require.cache).forEach(key => {
297
+ if (key.includes('source/components') || key.includes('source\\components')) {
298
+ delete require.cache[key];
299
+ }
300
+ });
301
+
302
+ // Find all entry files in public/.hexo-mdx-entry and bundle each one
303
+ const entryDir = path.join(publicDir, '.hexo-mdx-entry');
304
+ if (!fs.existsSync(entryDir)) {
305
+ return; // No entry generated, skip bundling
306
+ }
307
+
308
+ // Get all entry files
309
+ let entryFiles = [];
310
+ try {
311
+ const files = fs.readdirSync(entryDir);
312
+ entryFiles = files.filter(f => f.startsWith('mdx-entry-') && f.endsWith('.mjs'));
313
+ } catch (e) {
314
+ return; // Error reading entry dir, skip
315
+ }
316
+
317
+ if (entryFiles.length === 0) return;
318
+
319
+ const outDir = path.join(publicDir, 'assets');
320
+ fs.mkdirSync(outDir, { recursive: true });
321
+
322
+ // Bundle each entry file individually
323
+ entryFiles.forEach(entryFile => {
324
+ const entryPath = path.join(entryDir, entryFile);
325
+ const hash = entryFile.match(/mdx-entry-([a-f0-9]+)/)?.[1] || 'unknown';
326
+ const outName = `mdx-hydrate-${hash}.js`;
327
+
328
+ try {
329
+ esbuild.buildSync({
330
+ entryPoints: [entryPath],
331
+ bundle: true,
332
+ format: 'iife',
333
+ outfile: path.join(outDir, outName),
334
+ platform: 'browser',
335
+ target: 'es2017',
336
+ minify: false,
337
+ absWorkingDir: process.cwd(),
338
+ loader: { '.jsx': 'jsx', '.js': 'js', '.mjs': 'js' }
339
+ });
340
+ console.log(`INFO ✓ Bundled entry to ${path.join(outDir, outName)}`);
341
+ } catch (err) {
342
+ console.warn(`INFO Bundle error for ${entryFile}: ${err.message}`);
343
+ }
344
+ });
345
+ } catch (err) {
346
+ // Silently skip if esbuild is unavailable
347
+ }
348
+ }
349
+
350
+ // Bundle a single entry by its MDX file hash (targets only one output)
351
+ function bundleEntryByHash(hash) {
352
+ try {
353
+ const esbuild = require('esbuild');
354
+ const projectRoot = hexo && hexo.base_dir ? hexo.base_dir : process.cwd();
355
+ const publicDir = (hexo && hexo.public_dir) ? hexo.public_dir : path.join(projectRoot, 'public');
356
+ const entryPath = path.join(publicDir, '.hexo-mdx-entry', `mdx-entry-${hash}.mjs`);
357
+ const outDir = path.join(publicDir, 'assets');
358
+ const outName = `mdx-hydrate-${hash}.js`;
359
+
360
+ if (!fs.existsSync(entryPath)) {
361
+ return; // No entry for this hash yet
362
+ }
363
+
364
+ fs.mkdirSync(outDir, { recursive: true });
365
+ esbuild.buildSync({
366
+ entryPoints: [entryPath],
367
+ bundle: true,
368
+ format: 'iife',
369
+ outfile: path.join(outDir, outName),
370
+ platform: 'browser',
371
+ target: 'es2017',
372
+ minify: false,
373
+ absWorkingDir: process.cwd(),
374
+ loader: { '.jsx': 'jsx', '.js': 'js', '.mjs': 'js' }
375
+ });
376
+ console.log(`INFO ✓ Bundled entry to ${path.join(outDir, outName)}`);
377
+ } catch (err) {
378
+ console.warn(`INFO Bundle error for hash ${hash}: ${err && err.message}`);
379
+ }
380
+ }
381
+
382
+ // Persist component -> [mdxFiles] mapping into the public dir so it ships with the site
383
+ function saveComponentPathJson() {
384
+ try {
385
+ const projectRoot = hexo && hexo.base_dir ? hexo.base_dir : process.cwd();
386
+ const publicDir = (hexo && hexo.public_dir) ? hexo.public_dir : path.join(projectRoot, 'public');
387
+ const publicOut = path.join(publicDir, 'hexo-renderer-mdx.component-path.json');
388
+ const obj = {};
389
+ componentDependencies.forEach((mdxSet, compPath) => {
390
+ try {
391
+ obj[compPath] = Array.from(mdxSet);
392
+ } catch (e) {
393
+ obj[compPath] = [];
394
+ }
395
+ });
396
+ fs.mkdirSync(publicDir, { recursive: true });
397
+ fs.writeFileSync(publicOut, JSON.stringify(obj, null, 2), 'utf8');
398
+ } catch (err) {
399
+ console.warn('Could not write component-path JSON:', err && err.message);
400
+ }
401
+ }
402
+
403
+ // Wrap renderer to track component dependencies
404
+ const originalMdxRenderer = mdxRenderer;
405
+ async function mdxRendererWithTracking(data) {
406
+ const result = await originalMdxRenderer(data);
407
+
408
+ // Track which components this MDX file depends on
409
+ if (data.dependencies && data.dependencies.size > 0) {
410
+ data.dependencies.forEach(componentPath => {
411
+ if (!componentDependencies.has(componentPath)) {
412
+ componentDependencies.set(componentPath, new Set());
413
+ }
414
+ componentDependencies.get(componentPath).add(data.path);
415
+ });
416
+ // Persist mapping to JSON so it's available across runs and survives hexo clean
417
+ try {
418
+ saveComponentPathJson();
419
+ } catch (e) {
420
+ // ignore
421
+ }
422
+ }
423
+
424
+ return result;
425
+ }
426
+
427
+ hexo.extend.renderer.register('mdx', 'html', mdxRendererWithTracking, {
203
428
  disableNunjucks: true
204
429
  });
430
+
431
+ /**
432
+ * Watch component files and trigger full site regeneration when they change
433
+ */
434
+ let mdxComponentWatcher = null;
435
+ // Only register watcher in real Hexo server runs
436
+ if (
437
+ hexo &&
438
+ hexo.extend &&
439
+ hexo.extend.filter &&
440
+ typeof hexo.extend.filter.register === 'function' &&
441
+ hexo.env && hexo.env.cmd === 'server'
442
+ ) {
443
+ hexo.extend.filter.register('after_init', function() {
444
+ // Set up file watcher for component paths from the JSON mapping
445
+ const sourceDir = path.join(hexo.source_dir, 'components');
446
+ const projectRoot = hexo && hexo.base_dir ? hexo.base_dir : process.cwd();
447
+ const componentPathJsonPath = path.join(projectRoot, 'hexo-renderer-mdx.component-path.json');
448
+
449
+ // Only initialize the persistent watcher during `hexo server` runs.
450
+ // For other commands (clean/generate), skip watcher to allow the process to exit.
451
+ if (!hexo || !hexo.env || hexo.env.cmd !== 'server') {
452
+ return;
453
+ }
454
+
455
+ // Function to read component paths from JSON and extract keys
456
+ function getComponentPathsFromJson() {
457
+ try {
458
+ if (fs.existsSync(componentPathJsonPath)) {
459
+ const mapping = JSON.parse(fs.readFileSync(componentPathJsonPath, 'utf8')) || {};
460
+ return Object.keys(mapping).filter(p => fs.existsSync(p));
461
+ }
462
+ } catch (e) {
463
+ // ignore parse/read errors
464
+ }
465
+ return [];
466
+ }
467
+
468
+ // Function to recreate the watcher with current component paths
469
+ function recreateWatcher() {
470
+ if (mdxComponentWatcher) {
471
+ mdxComponentWatcher.close();
472
+ }
473
+
474
+ const componentPaths = getComponentPathsFromJson();
475
+
476
+ // Watch both the component files and the JSON mapping file itself
477
+ const pathsToWatch = [...componentPaths, componentPathJsonPath];
478
+
479
+ if (pathsToWatch.length === 0) {
480
+ console.log(`INFO No component paths to watch yet`);
481
+ return;
482
+ }
483
+
484
+ try {
485
+ mdxComponentWatcher = chokidar.watch(pathsToWatch, {
486
+ ignored: /node_modules|\.git/,
487
+ persistent: true
488
+ });
489
+
490
+ console.log(`INFO Watching ${componentPaths.length} component path(s)`);
491
+
492
+ // Add event listeners for debugging
493
+ mdxComponentWatcher.on('ready', () => {
494
+ console.log('INFO Watcher ready, monitoring for changes...');
495
+ });
496
+
497
+ mdxComponentWatcher.on('error', (error) => {
498
+ console.error('INFO Watcher error:', error);
499
+ });
500
+
501
+ mdxComponentWatcher.on('all', (event, watchedPath) => {
502
+ if (event === 'change' || event === 'add' || event === 'unlink') {
503
+ console.log(`INFO Watcher event: ${event} - ${watchedPath}`);
504
+
505
+ // If the JSON mapping file was changed, update the watcher
506
+ if (watchedPath === componentPathJsonPath && (event === 'change' || event === 'add')) {
507
+ console.log(`INFO Component mapping updated, refreshing watched paths...`);
508
+ process.nextTick(() => {
509
+ recreateWatcher();
510
+ });
511
+ return;
512
+ }
513
+ }
514
+ });
515
+ } catch (err) {
516
+ console.warn('Failed to create component watcher:', err.message);
517
+ }
518
+ }
519
+
520
+ try {
521
+ const handleComponentChange = (changedPath) => {
522
+ console.log(`\nINFO ⚡ Component file changed: ${changedPath}`);
523
+ console.log(`INFO Clearing caches and triggering regeneration...`);
524
+
525
+ // PAUSE the watcher to prevent it from detecting the file deletions during regeneration
526
+ try { mdxComponentWatcher.close(); } catch (e) {}
527
+
528
+ // Clear the require cache for all components and Babel
529
+ Object.keys(require.cache).forEach(key => {
530
+ if (key.includes('source/components') || key.includes('.hexo-mdx-entry') || key.includes('source\\components')) {
531
+ delete require.cache[key];
532
+ }
533
+ });
534
+
535
+ // Delete the compiled entry directory to force recreation
536
+ const fs = require('fs');
537
+ const mdxEntryDir = path.join(hexo.base_dir, '.hexo-mdx-entry');
538
+ if (fs.existsSync(mdxEntryDir)) {
539
+ try {
540
+ fs.rmSync(mdxEntryDir, { recursive: true });
541
+ } catch (err) {
542
+ // Ignore cleanup errors
543
+ }
544
+ }
545
+
546
+ // Invalidate Hexo's locals cache
547
+ if (hexo.locals) {
548
+ hexo.locals.invalidate();
549
+ }
550
+
551
+ // Read component-path JSON (prefer the copy in public) and try to rerender only affected MDX files
552
+ const mappingCandidates = [
553
+ path.join((hexo && hexo.public_dir) ? hexo.public_dir : path.join(hexo.base_dir || process.cwd(), 'public'), 'hexo-renderer-mdx.component-path.json'),
554
+ path.join(hexo.base_dir || process.cwd(), 'hexo-renderer-mdx.component-path.json')
555
+ ];
556
+ let mapping = null;
557
+ for (const mappingPath of mappingCandidates) {
558
+ try {
559
+ if (fs.existsSync(mappingPath)) {
560
+ mapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8')) || null;
561
+ break;
562
+ }
563
+ } catch (e) {
564
+ mapping = null;
565
+ }
566
+ }
567
+
568
+ const normalize = p => path.resolve(p).split(path.sep).join(path.sep);
569
+ const normalizedChanged = normalize(changedPath);
570
+
571
+ let affectedMdxFiles = [];
572
+ if (mapping) {
573
+ Object.keys(mapping).forEach(compPath => {
574
+ const normalizedComp = normalize(compPath);
575
+ if (
576
+ normalizedChanged === normalizedComp ||
577
+ normalizedChanged.startsWith(normalizedComp + path.sep) ||
578
+ normalizedComp.startsWith(normalizedChanged + path.sep)
579
+ ) {
580
+ const arr = mapping[compPath] || [];
581
+ arr.forEach(m => { if (m && affectedMdxFiles.indexOf(m) === -1) affectedMdxFiles.push(m); });
582
+ }
583
+ });
584
+ }
585
+
586
+ // If we found affected files, try to rerender them individually; otherwise fallback to full clean + generate
587
+ process.nextTick(async () => {
588
+ if (affectedMdxFiles.length > 0) {
589
+ console.log(`INFO Rerendering ${affectedMdxFiles.length} affected MDX file(s)...`);
590
+ let failed = false;
591
+ for (const mdxFile of affectedMdxFiles) {
592
+ try {
593
+ // Best-effort: try to call a targeted generate if available; fall back to full generate on failure
594
+ await hexo.call('generate', { watch: false, file: mdxFile }).catch(() => { throw new Error('per-file generate unsupported'); });
595
+ } catch (err) {
596
+ failed = true;
597
+ break;
598
+ }
599
+ }
600
+ if (!failed) {
601
+ console.log('INFO ✓ Per-file regeneration complete');
602
+ // Bundle only the affected entries by their file hash
603
+ const hashes = Array.from(new Set(affectedMdxFiles.map(f => crypto.createHash('md5').update(f).digest('hex').slice(0, 8))));
604
+ hashes.forEach(h => bundleEntryByHash(h));
605
+ // Resume watcher
606
+ recreateWatcher();
607
+ return;
608
+ }
609
+ // Fallback to full clean+generate below
610
+ }
611
+
612
+ // Fallback: full clean + generate
613
+ hexo.call('clean').then(() => {
614
+ return hexo.call('generate', {watch: false});
615
+ }).then(() => {
616
+ console.log('INFO ✓ Regeneration complete');
617
+ // Bundle only the affected entries (if any were identified)
618
+ if (affectedMdxFiles.length > 0) {
619
+ const hashes = Array.from(new Set(affectedMdxFiles.map(f => crypto.createHash('md5').update(f).digest('hex').slice(0, 8))));
620
+ hashes.forEach(h => bundleEntryByHash(h));
621
+ }
622
+ console.log('INFO ✓ Refresh your browser to see changes');
623
+ // Resume watcher
624
+ recreateWatcher();
625
+ }).catch(err => {
626
+ console.warn('Regeneration error:', err.message);
627
+ // Resume watcher even on error
628
+ recreateWatcher();
629
+ });
630
+ });
631
+ };
632
+
633
+ mdxComponentWatcher.on('change', handleComponentChange);
634
+
635
+ console.log('INFO Component file watcher initialized');
636
+ } catch (err) {
637
+ console.warn('Component file watcher setup warning:', err.message);
638
+ }
639
+
640
+ // Initialize the watcher for the first time
641
+ recreateWatcher();
642
+ });
643
+ }
644
+
645
+ // Close watcher when Hexo exits to allow process to terminate properly
646
+ if (hexo && typeof hexo.on === 'function') {
647
+ hexo.on('exit', function() {
648
+ if (mdxComponentWatcher) {
649
+ mdxComponentWatcher.close();
650
+ }
651
+ });
652
+ }
653
+
654
+ // Ensure component-path JSON is placed into public when site is generated,
655
+ // and bundle the entry if one was created during rendering.
656
+ try {
657
+ if (
658
+ hexo &&
659
+ hexo.extend &&
660
+ hexo.extend.filter &&
661
+ typeof hexo.extend.filter.register === 'function'
662
+ ) {
663
+ hexo.extend.filter.register('after_generate', function() {
664
+ try {
665
+ const projectRoot = hexo && hexo.base_dir ? hexo.base_dir : process.cwd();
666
+ const src = path.join(projectRoot, 'hexo-renderer-mdx.component-path.json');
667
+ const publicDir = (hexo && hexo.public_dir) ? hexo.public_dir : path.join(projectRoot, 'public');
668
+ const dest = path.join(publicDir, 'hexo-renderer-mdx.component-path.json');
669
+ if (fs.existsSync(src)) {
670
+ fs.mkdirSync(publicDir, { recursive: true });
671
+ fs.copyFileSync(src, dest);
672
+ }
673
+
674
+ // Bundle the entry file to produce the hydration client bundle
675
+ bundleEntryToPublic();
676
+ } catch (e) {
677
+ // ignore
678
+ }
679
+ });
680
+ }
681
+ } catch (e) {
682
+ // ignore if filter registration not available
683
+ }
684
+
685
+ // Export renderer functions for tests and direct usage outside Hexo
686
+ try {
687
+ module.exports = {
688
+ mdxRenderer,
689
+ mdxRendererWithTracking
690
+ };
691
+ } catch (e) {
692
+ // ignore export errors in unusual runtimes
693
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-renderer-mdx",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "MDX renderer plugin for Hexo with React component support",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -25,13 +25,14 @@
25
25
  },
26
26
  "homepage": "https://github.com/Bryan0324/hexo-renderer-mdx#readme",
27
27
  "dependencies": {
28
- "@babel/register": "^7.25.0",
29
28
  "@babel/core": "^7.25.0",
30
29
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
31
30
  "@babel/plugin-syntax-jsx": "^7.25.0",
32
31
  "@babel/plugin-transform-react-jsx": "^7.25.0",
32
+ "@babel/register": "^7.25.0",
33
33
  "@mdx-js/mdx": "^3.0.0",
34
- "react": "^18.2.0",
34
+ "esbuild": "^0.27.2",
35
+ "react": "^18.3.1",
35
36
  "react-dom": "^18.2.0"
36
37
  },
37
38
  "peerDependencies": {