257 lines
7.9 KiB
JavaScript
Executable File
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()
|