1
0
mirror of synced 2025-12-23 03:44:00 -05:00
Files
docs/script/code/convert-cjs-to-esm.mjs
Kevin Heis 42e785b0a8 Migrate CommonJS to ESM (#20301)
* First run of script

* Get the app running --- ish

* Get NextJS working

* Remove `node:`

* Get more tests passing in unit directory

* Update FailBot test to use nock

* Update test.yml

* Update Dockerfile

* tests/content fixes

* Update page.js

* Update build-changelog.js

* updating tests/routing

* Update orphan-tests.js

* updating tests/rendering

* Update .eslintrc.js

* Update .eslintrc.js

* Install jest/globals

* "linting" tests

* staging update to server.mjs

* Change '.github/allowed-actions.js' to a ESM export

* Lint

* Fixes for the main package.json

* Move Jest to be last in the npm test command so we can pass args

* Just use 'npm run lint' in the npm test command

* update algolia label script

* update openapi script

* update require on openapi

* Update enterprise-algolia-label.js

* forgot JSON.parse

* Update lunr-search-index.js

* Always explicitly include process.cwd() for JSON file reads pathed from project root

* update graphql/update-files.js script

* Update other npm scripts using jest to pass ESM NODE_OPTIONS

* Update check-for-enterprise-issues-by-label.js for ESM

* Update create-enterprise-issue.js for ESM

* Import jest global for browser tests

* Convert 'script/deploy' to ESM

Co-authored-by: Grace Park <gracepark@github.com>
Co-authored-by: James M. Greene <jamesmgreene@github.com>
2021-07-14 13:49:18 -07:00

272 lines
8.0 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()