| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- // Performs a few useful codemod transformations for Overleaf's esm migration.
- // The transformations mostly address specific issues faced commonly in Overleaf's `web` service.
- // * Replaces `sandboxed-module` imports with `esmock` imports.
- // * Replaces `sandboxed-module` invocation with `esmock` invocation (Assumes `SandboxedModule.require` is used for the invocation).
- // * Fixes `mongodb-legacy` import to use `mongodb` import and extract `ObjectId` from the import.
- // * Replaces `require('path').join` with `path.join` (importing the path module if not already imported).
- // * Adds `const __dirname = fileURLToPath(new URL('.', import.meta.url))` if `__dirname` is used in the file.
- // * Adds `.js` or `.mjs` extension (as appropriate) to relative path imports.
- // call this with `jscodeshift -t overleaf-es-codemod.js <file>` or using the `cjs-to-esm.js` script (which does this as the final step before formatting).
- const fs = require('node:fs')
- const Path = require('node:path')
- module.exports = function (fileInfo, api) {
- const j = api.jscodeshift
- const root = j(fileInfo.source)
- const body = root.get().value.program.body
- /**
- * Conditionally adds an import statement to the top of the file if it doesn't already exist.
- * @param moduleName A plain text name for the module to import (e.g. 'node:path').
- * @param specifier A jscodeshift specifier for the import statement (provides e.g. `{ promises }` from `import { promises } from 'fs'`.
- * @param existingImportCheck A function that checks if a specific import statement is the one we're looking for.
- */
- function addImport(moduleName, specifier, existingImportCheck) {
- // Add import path from 'path' at the top if not already present
- const importDeclaration = j.importDeclaration(
- specifier,
- j.literal(moduleName)
- )
- if (!existingImportCheck) {
- existingImportCheck = node => node.source.value === moduleName
- }
- const existingImport = body.find(
- node => node.type === 'ImportDeclaration' && existingImportCheck(node)
- )
- if (!existingImport) {
- const lastImportIndex = body.reduce((lastIndex, node, index) => {
- return node.type === 'ImportDeclaration' ? index : lastIndex
- }, -1)
- body.splice(lastImportIndex, 0, importDeclaration)
- }
- }
- // Replace sandboxed-module imports
- root
- .find(j.ImportDeclaration, {
- source: { value: 'sandboxed-module' },
- })
- .forEach(path => {
- path.node.source.value = 'esmock'
- if (path.node.specifiers.length > 0 && path.node.specifiers[0].local) {
- path.node.specifiers[0].local.name = 'esmock'
- }
- })
- // Replace sandboxedModule.require calls with awaited esmock calls
- root
- .find(j.CallExpression, {
- callee: {
- object: { name: 'SandboxedModule' },
- property: { name: 'require' },
- },
- })
- .forEach(path => {
- const args = path.node.arguments
- if (args.length > 0) {
- const firstArg = args[0]
- const esmockArgs = [firstArg]
- // Check if there's a second argument with a 'requires' property
- if (args.length > 1 && args[1].type === 'ObjectExpression') {
- const requiresProp = args[1].properties.find(
- prop =>
- prop.key.name === 'requires' || prop.key.value === 'requires'
- )
- if (requiresProp) {
- // Move contents of 'requires' to top level
- esmockArgs.push(requiresProp.value)
- }
- }
- // Create the await expression with restructured arguments
- const awaitExpression = j.awaitExpression(
- j.callExpression(
- j.memberExpression(j.identifier('esmock'), j.identifier('strict')),
- esmockArgs
- )
- )
- // Replace the original call with the await expression
- j(path).replaceWith(awaitExpression)
- // Find the closest function and make it async
- let functionPath = path
- while ((functionPath = functionPath.parent)) {
- if (
- functionPath.node.type === 'FunctionDeclaration' ||
- functionPath.node.type === 'FunctionExpression' ||
- functionPath.node.type === 'ArrowFunctionExpression'
- ) {
- functionPath.node.async = true
- break
- }
- }
- }
- })
- // Fix mongodb-legacy import
- root
- .find(j.ImportDeclaration, {
- source: { value: 'mongodb-legacy' },
- specifiers: [{ imported: { name: 'ObjectId' } }],
- })
- .forEach(path => {
- // Create new import declaration
- const newImport = j.importDeclaration(
- [j.importDefaultSpecifier(j.identifier('mongodb'))],
- j.literal('mongodb-legacy')
- )
- // Create new constant declaration
- const newConst = j.variableDeclaration('const', [
- j.variableDeclarator(
- j.objectPattern([
- j.property(
- 'init',
- j.identifier('ObjectId'),
- j.identifier('ObjectId')
- ),
- ]),
- j.identifier('mongodb')
- ),
- ])
- // Replace the old import with the new import and constant declaration
- j(path).replaceWith(newImport)
- path.insertAfter(newConst)
- })
- root
- .find(j.CallExpression, {
- callee: {
- object: { callee: { name: 'require' }, arguments: [{ value: 'path' }] },
- property: { name: 'join' },
- },
- })
- .forEach(path => {
- // Replace with path.join
- j(path).replaceWith(
- j.callExpression(
- j.memberExpression(j.identifier('path'), j.identifier('join')),
- path.node.arguments
- )
- )
- // Add import path from 'path' at the top if not already presen
- addImport(
- 'node:path',
- [j.importDefaultSpecifier(j.identifier('path'))],
- node =>
- node.source.value === 'path' || node.source.value === 'node:path'
- )
- })
- // Add const __dirname = fileURLToPath(new URL('.', import.meta.url)) if there is a usage of __dirname
- const dirnameDeclaration = j.variableDeclaration('const', [
- j.variableDeclarator(
- j.identifier('__dirname'),
- j.callExpression(j.identifier('fileURLToPath'), [
- j.newExpression(j.identifier('URL'), [
- j.literal('.'),
- j.memberExpression(j.identifier('import'), j.identifier('meta.url')),
- ]),
- ])
- ),
- ])
- const existingDirnameDeclaration = body.find(
- node =>
- node.type === 'VariableDeclaration' &&
- node.declarations[0].id.name === '__dirname'
- )
- const firstDirnameUsage = root.find(j.Identifier, { name: '__dirname' }).at(0)
- if (firstDirnameUsage.size() > 0 && !existingDirnameDeclaration) {
- // Add import path from 'path' at the top if not already present
- addImport(
- 'node:url',
- [j.importSpecifier(j.identifier('fileURLToPath'))],
- node => node.source.value === 'url' || node.source.value === 'node:url'
- )
- const lastImportIndex = body.reduce((lastIndex, node, index) => {
- return node.type === 'ImportDeclaration' ? index : lastIndex
- }, -1)
- body.splice(lastImportIndex + 1, 0, dirnameDeclaration)
- }
- // Add extension to relative path imports
- root
- .find(j.ImportDeclaration)
- .filter(path => path.node.source.value.startsWith('.'))
- .forEach(path => {
- const importPath = path.node.source.value
- const fullPathJs = Path.resolve(
- Path.dirname(fileInfo.path),
- `${importPath}.js`
- )
- const fullPathMjs = Path.resolve(
- Path.dirname(fileInfo.path),
- `${importPath}.mjs`
- )
- if (fs.existsSync(fullPathJs)) {
- path.node.source.value = `${importPath}.js`
- } else if (fs.existsSync(fullPathMjs)) {
- path.node.source.value = `${importPath}.mjs`
- }
- })
- return root.toSource({
- quote: 'single',
- })
- }
|