-
Notifications
You must be signed in to change notification settings - Fork 2
⚡ perf: optimize migrate-mdx.js with async I/O #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,44 +1,40 @@ | ||||||||||||
| const fs = require('fs'); | ||||||||||||
| const path = require('path'); | ||||||||||||
|
|
||||||||||||
| function walk(dir) { | ||||||||||||
| let results = []; | ||||||||||||
| const list = fs.readdirSync(dir); | ||||||||||||
| list.forEach(file => { | ||||||||||||
| const fullPath = path.join(dir, file); | ||||||||||||
| const stat = fs.statSync(fullPath); | ||||||||||||
| if (stat && stat.isDirectory()) results = results.concat(walk(fullPath)); | ||||||||||||
| else results.push(fullPath); | ||||||||||||
| }); | ||||||||||||
| return results; | ||||||||||||
| import fs from 'node:fs/promises'; | ||||||||||||
| import path from 'node:path'; | ||||||||||||
|
|
||||||||||||
| async function main() { | ||||||||||||
| const dirents = await fs.readdir('./pages', { recursive: true, withFileTypes: true }); | ||||||||||||
| const files = dirents | ||||||||||||
| .filter(dirent => !dirent.isDirectory() && (dirent.name.endsWith('.mdx') || dirent.name.endsWith('.md'))) | ||||||||||||
| .map(dirent => path.join(dirent.parentPath || dirent.path, dirent.name)); | ||||||||||||
|
|
||||||||||||
| await Promise.all(files.map(async file => { | ||||||||||||
| let content = await fs.readFile(file, 'utf8'); | ||||||||||||
|
|
||||||||||||
| // replace <Callout type="warning"> ... </Callout> with :::warning ... ::: | ||||||||||||
| content = content.replace(/<Callout[^>]*type="([^"]+)"[^>]*>([\s\S]*?)<\/Callout>/g, (_, type, body) => { | ||||||||||||
| return `:::${type}\n${body.trim()}\n:::`; | ||||||||||||
| }); | ||||||||||||
| content = content.replace(/<Callout>([\s\S]*?)<\/Callout>/g, (_, body) => { | ||||||||||||
| return `:::info\n${body.trim()}\n:::`; | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| // Replace imports | ||||||||||||
| content = content.replace(/import\s*\{([^}]*)\}\s*from\s*"nextra\/components";?/g, (match, imports) => { | ||||||||||||
| const list = imports.split(',').map(i => i.trim()).filter(i => i !== 'Callout' && i !== ''); | ||||||||||||
| if (list.length === 0) return ''; | ||||||||||||
| if (list.includes('Tabs') && !list.includes('Tab')) { | ||||||||||||
| list.push('Tab'); | ||||||||||||
| } | ||||||||||||
| return `import { ${list.join(', ')} } from "rspress/theme";`; | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| // Replace <Tabs.Tab> with <Tab> | ||||||||||||
| content = content.replace(/Tabs\.Tab/g, 'Tab'); | ||||||||||||
|
|
||||||||||||
| await fs.writeFile(file, content, 'utf8'); | ||||||||||||
| })); | ||||||||||||
|
|
||||||||||||
| console.log('Done migrating MDX'); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const files = walk('./pages').filter(f => f.endsWith('.mdx') || f.endsWith('.md')); | ||||||||||||
|
|
||||||||||||
| files.forEach(file => { | ||||||||||||
| let content = fs.readFileSync(file, 'utf8'); | ||||||||||||
|
|
||||||||||||
| // replace <Callout type="warning"> ... </Callout> with :::warning ... ::: | ||||||||||||
| content = content.replace(/<Callout[^>]*type="([^"]+)"[^>]*>([\s\S]*?)<\/Callout>/g, (_, type, body) => { | ||||||||||||
| return `:::${type}\n${body.trim()}\n:::`; | ||||||||||||
| }); | ||||||||||||
| content = content.replace(/<Callout>([\s\S]*?)<\/Callout>/g, (_, body) => { | ||||||||||||
| return `:::info\n${body.trim()}\n:::`; | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| // Replace imports | ||||||||||||
| content = content.replace(/import\s*\{([^}]*)\}\s*from\s*"nextra\/components";?/g, (match, imports) => { | ||||||||||||
| const list = imports.split(',').map(i => i.trim()).filter(i => i !== 'Callout' && i !== ''); | ||||||||||||
| if (list.length === 0) return ''; | ||||||||||||
| if (list.includes('Tabs') && !list.includes('Tab')) { | ||||||||||||
| list.push('Tab'); | ||||||||||||
| } | ||||||||||||
| return `import { ${list.join(', ')} } from "rspress/theme";`; | ||||||||||||
| }); | ||||||||||||
|
|
||||||||||||
| // Replace <Tabs.Tab> with <Tab> | ||||||||||||
| content = content.replace(/Tabs\.Tab/g, 'Tab'); | ||||||||||||
|
|
||||||||||||
| fs.writeFileSync(file, content, 'utf8'); | ||||||||||||
| }); | ||||||||||||
| console.log('Done migrating MDX'); | ||||||||||||
| main().catch(console.error); | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return non-zero exit status on migration failure.
Suggested fix-main().catch(console.error);
+main().catch(err => {
+ console.error(err);
+ process.exitCode = 1;
+});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bound parallel I/O to avoid
EMFILE/resource spikes at scale.Running all file reads/writes in one
Promise.allis unbounded; large trees can overwhelm file descriptors and memory. Use a fixed concurrency worker pool.Suggested fix
async function main() { const dirents = await fs.readdir('./pages', { recursive: true, withFileTypes: true }); const files = dirents .filter(dirent => !dirent.isDirectory() && (dirent.name.endsWith('.mdx') || dirent.name.endsWith('.md'))) .map(dirent => path.join(dirent.parentPath || dirent.path, dirent.name)); - await Promise.all(files.map(async file => { + const CONCURRENCY = 32; + let idx = 0; + + async function worker() { + while (idx < files.length) { + const file = files[idx++]; + await processFile(file); + } + } + + async function processFile(file) { let content = await fs.readFile(file, 'utf8'); // replace <Callout type="warning"> ... </Callout> with :::warning ... ::: content = content.replace(/<Callout[^>]*type="([^"]+)"[^>]*>([\s\S]*?)<\/Callout>/g, (_, type, body) => { return `:::${type}\n${body.trim()}\n:::`; @@ // Replace <Tabs.Tab> with <Tab> content = content.replace(/Tabs\.Tab/g, 'Tab'); await fs.writeFile(file, content, 'utf8'); - })); + } + + await Promise.all(Array.from({ length: Math.min(CONCURRENCY, files.length) }, () => worker()));🤖 Prompt for AI Agents