1
0
mirror of synced 2025-12-23 03:44:00 -05:00
Files
docs/script/code/convert-cjs-to-esm.mjs
Kevin Heis 8a56437c93 Pretty format (#20352)
* Update prettier flow to include JS

* Run prettier

* ...run prettier
2021-07-14 14:35:01 -07:00

257 lines
7.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
/*
This script does most of the work to convert our *.js files to use ESM
instead of CommonJS.
This is based on https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
This file assumes the two ESLint rules have already been installed and enforced:
- https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/global-require.md
- https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-dynamic-require.md
(for reference) https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-module.md
*/
import fs from 'fs/promises'
import semver from 'semver'
import walkSync from 'walk-sync'
// https://stackoverflow.com/a/31102605
function orderKeys(unordered) {
return Object.keys(unordered)
.sort()
.reduce((obj, key) => {
obj[key] =
unordered[key].constructor === {}.constructor ? orderKeys(unordered[key]) : unordered[key]
return obj
}, {})
}
async function readPackageFile() {
return JSON.parse(await fs.readFile('./package.json', 'utf8'))
}
async function writePackageFile(packageFile) {
return fs.writeFile('./package.json', JSON.stringify(orderKeys(packageFile), ' ', 2) + '\n')
}
async function readAllJsFiles() {
const paths = walkSync('./', {
directories: false,
includeBasePath: true,
globs: ['**/*.js'],
ignore: ['node_modules', 'dist'],
})
return await Promise.all(paths.map(async (path) => [path, await fs.readFile(path, 'utf8')]))
}
function listJsonPaths() {
const paths = walkSync('./', {
directories: false,
includeBasePath: true,
globs: ['**/*.json'],
ignore: ['node_modules', 'dist'],
})
return paths.map((p) => p.replace('.json', ''))
}
function withAllFiles(jsFiles, fn) {
return jsFiles.map(([path, file]) => [path, fn(path, file)])
}
async function writeAllJsFiles(jsFiles) {
return await Promise.all(jsFiles.map(async ([path, file]) => await fs.writeFile(path, file)))
}
// Converts a path to an import name
/* Example:
import xLunrJa from 'lunr-languages/lunr.ja'
xLunrJa(lunr)
*/
function nameImport(p2) {
const myString = p2.split('/').pop()
const string = myString.replace(/[-.]([a-z])/g, (g) => g[1].toUpperCase())
return `x${string.charAt(0).toUpperCase()}${string.slice(1)}`
}
// Add "type": "module" to your package.json.
async function addTypeModule() {
const packageFile = await readPackageFile()
packageFile.type = 'module'
return writePackageFile(packageFile)
}
// Replace "main": "index.js" with "exports": "./index.js" in your package.json.
async function updateMainExport() {
const packageFile = await readPackageFile()
const main = packageFile.main
if (!main) return
delete packageFile.main
packageFile.exports = './' + main
return writePackageFile(packageFile)
}
// Update the "engines" field in package.json to Node.js 12: "node": "^12.20.0 || ^14.13.1 || >=16.0.0".
// If 12 is already required, we will skip this change.
async function checkEngines() {
const packageFile = await readPackageFile()
const nodeVersion = packageFile.engines.node
if (semver.gt(semver.minVersion(nodeVersion), '12.0.0')) return
packageFile.engines.node = '^12.20.0 || ^14.13.1 || >=16.0.0'
await writePackageFile(packageFile)
}
// Remove 'use strict'; from all JavaScript files.
function noStrict(path, file) {
if (file.includes('use strict')) {
throw new Error(`Cannot use strict in ${path}. Please remove and run this script again.`)
}
return file
}
// Read JSON requires separately
/*
import { promises as fs } from 'fs'
const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'))
*/
function noJsonReads(jsonPaths) {
return (path, file) => {
const found = [...file.matchAll(/require\('[./]+(.*?)'\)/gm)]
if (!found) return file
const matchesJsonPath = found
.map((f) => f[1])
.filter((f) => jsonPaths.some((p) => p.endsWith(f)))
if (matchesJsonPath.length) {
throw new Error(
`${path} has possible JSON requires: ${matchesJsonPath}. Please fix this manually then run the script again.`
)
}
return file
}
}
// Replace all require()/module.export with import/export.
// Use only full relative file paths for imports: import x from '.'; → import x from './index.js';.
// Add `.js` if starts with `./`
// Replace `:` to as
// Fix up standard const x = require('x') statements
function updateStandardImport(path, file) {
return file.replaceAll(/^const\s(.*?)\s*?=\s*?require\('(.*?)'\)$/gm, (_, p1, p2) => {
// Replace `:` to as
p1 = p1.replace(/\s*:\s*/g, ' as ')
// Add `.js` if path starts with `.`
if (p2.startsWith('.') && !p2.endsWith('.js')) p2 = p2 + '.js'
return `import ${p1} from '${p2}'`
})
}
// Fix up inlined requires that are still "top-level"
function updateInlineImport(path, file) {
return file.replaceAll(/^(.*?)require\('(.*?)'\)(.*)$/gm, (_, p1, p2, p3) => {
// Generate a new import name based on the path
const name = nameImport(p2)
// Add `.js` if starts with `.`
if (p2.startsWith('.') && !p2.endsWith('.js')) p2 = p2 + '.js'
// Fix up unused require('x') statements
if (!p1 && !p3) return `import '${p2}'`
return `import ${name} from '${p2}'\n${p1}${name}${p3}`
})
}
// Handle module.exports =
function updateDefaultExport(path, file) {
return file.replaceAll(/^module.exports\s*?=\s*?(\S.*)/gm, 'export default $1')
}
// Handle exports.x =
function updateNamedExport(path, file) {
return file.replaceAll(/^exports\.(\S+)\s*?=\s*?(\S.*)/gm, 'export const $1 = $2')
}
// Replace __filename and __dirname
function updateFileAndDir(path, file) {
if (!file.includes('__filename') && !file.includes('__dirname')) return file
return [
"import { fileURLToPath } from 'url'",
"import path from 'path'",
file.includes('__filename') && 'const __filename = fileURLToPath(import.meta.url)',
file.includes('__dirname') && 'const __dirname = path.dirname(fileURLToPath(import.meta.url))',
file,
]
.filter(Boolean)
.join('\n')
}
// lodash => lodash-es
function useEsLodash(path, file) {
return file.replace("'lodash'", "'lodash-es'")
}
// Pull all imports to the top of the file to avoid syntax issues
function moveImportsToTop(path, file) {
if (!file.includes('import')) return file
const isTop = (line) => /^import/gm.test(line)
const lineEnd = /\r?\n|\r/g
return (
file.split(lineEnd).filter(isTop).join('\n') +
'\n' +
file
.split(lineEnd)
.filter((line) => !isTop(line))
.join('\n')
)
}
// Make sure script declarations on the top of the file before imports
function updateScriptDeclaration(path, file) {
if (!path.startsWith('./script')) return file
file = file.replace('#!/usr/bin/env node\n', '')
return '#!/usr/bin/env node\n' + file
}
// Check there's no `require(` ... anywhere
function checkRequire(path, file) {
if (/require\s*\(/.test(file)) {
throw new Error(`"require(" still in ${path}`)
}
return file
}
// Check there's no `exports` ... anywhere
function checkExports(path, file) {
if (file.includes('exports')) {
throw new Error(`"exports" still in ${path}`)
}
return file
}
async function main() {
await addTypeModule()
await updateMainExport()
await checkEngines()
const jsonPaths = listJsonPaths()
let jsFiles = await readAllJsFiles()
jsFiles = withAllFiles(jsFiles, noStrict)
jsFiles = withAllFiles(jsFiles, noJsonReads(jsonPaths))
jsFiles = withAllFiles(jsFiles, updateStandardImport)
jsFiles = withAllFiles(jsFiles, updateInlineImport)
jsFiles = withAllFiles(jsFiles, updateDefaultExport)
jsFiles = withAllFiles(jsFiles, updateNamedExport)
jsFiles = withAllFiles(jsFiles, updateFileAndDir)
jsFiles = withAllFiles(jsFiles, useEsLodash)
jsFiles = withAllFiles(jsFiles, moveImportsToTop)
jsFiles = withAllFiles(jsFiles, updateScriptDeclaration)
jsFiles = withAllFiles(jsFiles, checkRequire)
jsFiles = withAllFiles(jsFiles, checkExports)
await writeAllJsFiles(jsFiles)
}
main()