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.
- package/README.md +11 -2
- package/index.js +509 -20
- 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
|
-
-
|
|
12
|
-
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
34
|
+
"esbuild": "^0.27.2",
|
|
35
|
+
"react": "^18.3.1",
|
|
35
36
|
"react-dom": "^18.2.0"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|