Created
January 13, 2026 10:05
-
-
Save jmfrancois/7980b376ea2b9cd5c0ff2131b1450c08 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * # Preview what would be renamed (dry run) | |
| * node rename-jsx-files.mjs packages/components --dry-run | |
| * | |
| * # Actually rename files | |
| * node rename-jsx-files.mjs packages/components | |
| * | |
| * # Verbose mode to see all files processed | |
| * node rename-jsx-files.mjs packages/components --verbose | |
| */ | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| // Patterns that indicate JSX/React code | |
| const JSX_PATTERNS = [ | |
| /return\s*\(\s*<[A-Za-z]/m, // return (<Component | |
| /return\s*<[A-Z][A-Za-z0-9]/m, // return <Component | |
| /return\s*<>/m, // return <> | |
| /return\s*<\/>/m, // return </> | |
| /=>\s*\(\s*<[A-Za-z]/m, // => (<div or => (<Component | |
| /=>\s*<[A-Z][A-Za-z0-9]/m, // => <Component | |
| /=>\s*<>/m, // => <> | |
| /createElement\(/m, // React.createElement( | |
| ]; | |
| // Additional patterns for JSX elements | |
| const JSX_ELEMENT_PATTERNS = [ | |
| /<[A-Z][A-Za-z0-9._:-]*[\s>]/, // <Component> or <Component | |
| /<\/[A-Z][A-Za-z0-9._:-]*>/, // </Component> | |
| /<[a-z][a-z0-9]*[\s>]/, // <div> or <span | |
| /<\/[a-z][a-z0-9]*>/, // </div> or </span> | |
| /<>\s*$/m, // <> | |
| /<\/>\s*$/m, // </> | |
| ]; | |
| function hasJSXContent(content) { | |
| // Check primary JSX patterns (return statements, arrow functions) | |
| for (const pattern of JSX_PATTERNS) { | |
| if (pattern.test(content)) { | |
| return true; | |
| } | |
| } | |
| // Check for JSX elements, but be more careful to avoid false positives | |
| // Only consider it JSX if we find multiple element patterns or imports from react | |
| const hasReactImport = /from\s+['"]react['"]|require\(['"]react['"]\)/.test(content); | |
| if (hasReactImport) { | |
| for (const pattern of JSX_ELEMENT_PATTERNS) { | |
| if (pattern.test(content)) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| export function renameJSXFiles(dir, options = {}) { | |
| const { | |
| dryRun = false, | |
| verbose = false, | |
| exclude = ['node_modules', '.git', 'dist', 'build', 'lib', 'lib-esm', '.turbo'], | |
| } = options; | |
| const results = { | |
| scanned: 0, | |
| renamed: 0, | |
| errors: 0, | |
| files: [], | |
| }; | |
| function processDirectory(currentDir) { | |
| let entries; | |
| try { | |
| entries = fs.readdirSync(currentDir, { withFileTypes: true }); | |
| } catch (err) { | |
| console.error(`Error reading directory ${currentDir}:`, err.message); | |
| results.errors++; | |
| return; | |
| } | |
| for (const entry of entries) { | |
| const fullPath = path.join(currentDir, entry.name); | |
| if (entry.isDirectory()) { | |
| // Skip excluded directories | |
| if (exclude.includes(entry.name)) { | |
| if (verbose) { | |
| console.log(`Skipping excluded directory: ${fullPath}`); | |
| } | |
| continue; | |
| } | |
| processDirectory(fullPath); | |
| } else if (entry.isFile() && entry.name.endsWith('.js')) { | |
| results.scanned++; | |
| try { | |
| const content = fs.readFileSync(fullPath, 'utf8'); | |
| if (hasJSXContent(content)) { | |
| const newPath = fullPath.replace(/\.js$/, '.jsx'); | |
| results.files.push({ from: fullPath, to: newPath }); | |
| if (dryRun) { | |
| console.log(`[DRY RUN] Would rename: ${fullPath} -> ${newPath}`); | |
| } else { | |
| fs.renameSync(fullPath, newPath); | |
| console.log(`✓ Renamed: ${fullPath} -> ${newPath}`); | |
| } | |
| results.renamed++; | |
| } else if (verbose) { | |
| console.log(`Skipped (no JSX): ${fullPath}`); | |
| } | |
| } catch (err) { | |
| console.error(`Error processing ${fullPath}:`, err.message); | |
| results.errors++; | |
| } | |
| } | |
| } | |
| } | |
| processDirectory(dir); | |
| return results; | |
| } | |
| // Check if script is being run directly | |
| const isMainModule = process.argv[1] === fileURLToPath(import.meta.url); | |
| // CLI usage | |
| if (isMainModule) { | |
| const args = process.argv.slice(2); | |
| const options = { | |
| dryRun: args.includes('--dry-run') || args.includes('-d'), | |
| verbose: args.includes('--verbose') || args.includes('-v'), | |
| }; | |
| const targetDir = args.find(arg => !arg.startsWith('--') && !arg.startsWith('-')) || '.'; | |
| if (args.includes('--help') || args.includes('-h')) { | |
| console.log(` | |
| Usage: node rename-jsx-files.mjs [directory] [options] | |
| Rename .js files containing JSX/React code to .jsx | |
| Options: | |
| -d, --dry-run Show what would be renamed without actually renaming | |
| -v, --verbose Show all files being processed | |
| -h, --help Show this help message | |
| Example: | |
| node rename-jsx-files.mjs packages/components --dry-run | |
| node rename-jsx-files.mjs packages/components | |
| node rename-jsx-files.mjs . --verbose | |
| `); | |
| process.exit(0); | |
| } | |
| console.log(`\nScanning directory: ${path.resolve(targetDir)}`); | |
| console.log(`Mode: ${options.dryRun ? 'DRY RUN' : 'LIVE'}\n`); | |
| const results = renameJSXFiles(targetDir, options); | |
| console.log(`\n${'='.repeat(60)}`); | |
| console.log('Summary:'); | |
| console.log(` Files scanned: ${results.scanned}`); | |
| console.log(` Files ${options.dryRun ? 'to rename' : 'renamed'}: ${results.renamed}`); | |
| console.log(` Errors: ${results.errors}`); | |
| console.log(`${'='.repeat(60)}\n`); | |
| if (results.errors > 0) { | |
| process.exit(1); | |
| } | |
| } | |
| export { hasJSXContent }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment