mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #27958 from overleaf/ar-change-esm-codemod-to-use-vitest-and-general-refactor
[web] change esm codemod to use vitest and general refactor GitOrigin-RevId: 7f8c699b160ee0b7ff991d6284cb126165694c4f
This commit is contained in:
@@ -126,6 +126,7 @@ module.exports = {
|
||||
'chai-expect/missing-assertion': 'error',
|
||||
'chai-expect/terminating-properties': 'error',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'@overleaf/require-vi-doMock-valid-path': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('LinkedFilesController', function () {
|
||||
ctx.settings = { enabledLinkedFileTypes: [] }
|
||||
|
||||
vi.doMock(
|
||||
'.../../../../app/src/Features/Authentication/SessionManager',
|
||||
'../../../../app/src/Features/Authentication/SessionManager',
|
||||
() => ({
|
||||
default: ctx.SessionManager,
|
||||
})
|
||||
|
||||
@@ -5,30 +5,34 @@ import Runner from 'jscodeshift/src/Runner.js'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
// use minimist to get a list of files from the argv
|
||||
const argv = minimist(process.argv.slice(2), {
|
||||
boolean: ['usage'],
|
||||
const {
|
||||
dryRun,
|
||||
verbose,
|
||||
usage,
|
||||
_: files,
|
||||
} = minimist(process.argv.slice(2), {
|
||||
boolean: ['dryRun', 'usage', 'verbose'],
|
||||
})
|
||||
|
||||
function printUsage() {
|
||||
console.log(
|
||||
'node scripts/esm-migration/cjs-to-esm.mjs [files] [--format] [--lint] [--usage]'
|
||||
'node scripts/esm-migration/cjs-to-esm.mjs [files] [--dryRun] [--format] [--lint] [--usage] [--verbose]'
|
||||
)
|
||||
console.log(
|
||||
'WARNING: this will only work in local development as important dependencies will be missing in production'
|
||||
)
|
||||
console.log('Options:')
|
||||
console.log(' files: a list of files to convert')
|
||||
console.log('--dryRun: do not actually run the commands, just print them')
|
||||
console.log('--format: run prettier to fix formatting')
|
||||
console.log(' --lint: run eslint to fix linting')
|
||||
console.log(' --usage: show this help message')
|
||||
console.log('--verbose: enable verbose output')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const files = argv._
|
||||
|
||||
if (argv.usage) {
|
||||
if (usage) {
|
||||
printUsage()
|
||||
}
|
||||
|
||||
@@ -40,35 +44,46 @@ if (!Array.isArray(files) || files.length === 0) {
|
||||
|
||||
const promisifiedExec = promisify(exec)
|
||||
|
||||
const cjsTransform = fileURLToPath(
|
||||
import.meta.resolve('5to6-codemod/transforms/cjs.js')
|
||||
)
|
||||
const exportsTransform = fileURLToPath(
|
||||
import.meta.resolve('5to6-codemod/transforms/exports.js')
|
||||
)
|
||||
const overleafTransform = fileURLToPath(
|
||||
import.meta.resolve('./overleaf-es-codemod.js')
|
||||
)
|
||||
const transforms = [
|
||||
'5to6-codemod/transforms/cjs.js',
|
||||
'5to6-codemod/transforms/exports.js',
|
||||
'./codemods/esmoduleDirname.js',
|
||||
'./codemods/addExtensions.js',
|
||||
'./codemods/fixMongodbImport.js',
|
||||
]
|
||||
|
||||
const config = {
|
||||
output: __dirname,
|
||||
silent: true,
|
||||
output: import.meta.dirname,
|
||||
silent: verbose,
|
||||
print: false,
|
||||
verbose: 0,
|
||||
verbose: verbose ? 1 : 0,
|
||||
hoist: true,
|
||||
dry: dryRun,
|
||||
runInBand: true,
|
||||
}
|
||||
|
||||
await Runner.run(cjsTransform, files, config)
|
||||
await Runner.run(exportsTransform, files, config)
|
||||
await Runner.run(overleafTransform, files, config)
|
||||
if (dryRun) {
|
||||
console.log('Dry run mode enabled. No changes will be made.')
|
||||
}
|
||||
|
||||
for (const transformPath of transforms) {
|
||||
if (verbose) {
|
||||
console.log(`Running transform: ${transformPath}`)
|
||||
}
|
||||
const transform = fileURLToPath(await import.meta.resolve(transformPath))
|
||||
await Runner.run(transform, files, config)
|
||||
}
|
||||
|
||||
const webRoot = fileURLToPath(new URL('../../', import.meta.url))
|
||||
|
||||
for (const file of files) {
|
||||
// move files with git mv
|
||||
await promisifiedExec(`git mv ${file} ${file.replace('.js', '.mjs')}`)
|
||||
const relativePath = path.relative(webRoot, file)
|
||||
console.log(
|
||||
`transformed ${relativePath} and renamed it to have a .mjs extension`
|
||||
)
|
||||
if (!dryRun) {
|
||||
for (const file of files) {
|
||||
// move files with git mv
|
||||
const newFileName = file.replace('.js', '.mjs')
|
||||
await promisifiedExec(`git mv ${file} ${newFileName}`)
|
||||
const relativePath = path.relative(webRoot, file)
|
||||
console.log(
|
||||
`transformed ${relativePath} and renamed it to have a .mjs extension`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
33
services/web/transform/cjs-to-esm/codemods/addExtensions.js
Normal file
33
services/web/transform/cjs-to-esm/codemods/addExtensions.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const fs = require('node:fs')
|
||||
const Path = require('node:path')
|
||||
|
||||
module.exports = function (fileInfo, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(fileInfo.source)
|
||||
|
||||
// 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',
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = function transformer(file, api) {
|
||||
const j = api.jscodeshift
|
||||
|
||||
return j(file.source)
|
||||
.find(j.Identifier, { name: '__dirname' })
|
||||
.replaceWith(
|
||||
j.memberExpression(
|
||||
j.metaProperty(j.identifier('import'), j.identifier('meta')),
|
||||
j.identifier('dirname'),
|
||||
false
|
||||
)
|
||||
)
|
||||
.toSource()
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
|
||||
/**
|
||||
* @param {import('jscodeshift').FileInfo} file
|
||||
* @param {import('jscodeshift').API} api
|
||||
*/
|
||||
module.exports = function transformer(file, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(file.source)
|
||||
let hasChanges = false
|
||||
|
||||
const considerExtensionReplacement = nodePath => {
|
||||
const source = nodePath.value.source
|
||||
|
||||
if (
|
||||
!source ||
|
||||
typeof source.value !== 'string' ||
|
||||
!source.value.endsWith('.js')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const importPath = source.value
|
||||
const currentDirectory = path.dirname(file.path)
|
||||
|
||||
const jsPath = path.resolve(currentDirectory, importPath)
|
||||
if (fs.existsSync(jsPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const mjsImportPath = importPath.replace(/\.js$/, '.mjs')
|
||||
const mjsPath = path.resolve(currentDirectory, mjsImportPath)
|
||||
|
||||
if (fs.existsSync(mjsPath)) {
|
||||
j(nodePath).get('source').replace(j.literal(mjsImportPath))
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
const declarationTypes = [
|
||||
j.ImportDeclaration,
|
||||
j.ExportNamedDeclaration,
|
||||
j.ExportAllDeclaration,
|
||||
]
|
||||
|
||||
declarationTypes.forEach(type => {
|
||||
root
|
||||
.find(type, { source: s => s !== null })
|
||||
.forEach(considerExtensionReplacement)
|
||||
})
|
||||
|
||||
return hasChanges ? root.toSource({ quote: 'single' }) : null
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
const { getLastImport } = require('./utils')
|
||||
|
||||
module.exports = function (fileInfo, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(fileInfo.source)
|
||||
const body = root.get().value.program.body
|
||||
|
||||
// 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
|
||||
j(path).replaceWith(newImport)
|
||||
|
||||
// Insert the new constant declaration after the last import
|
||||
const lastImportIndex = getLastImport(body)
|
||||
body.splice(lastImportIndex + 1, 0, newConst)
|
||||
})
|
||||
|
||||
return root.toSource({
|
||||
quote: 'single',
|
||||
})
|
||||
}
|
||||
13
services/web/transform/cjs-to-esm/codemods/utils.js
Normal file
13
services/web/transform/cjs-to-esm/codemods/utils.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
*
|
||||
* @return {Node}
|
||||
*/
|
||||
function getLastImport(body) {
|
||||
return body.reduce((lastIndex, node, index) => {
|
||||
return node.type === 'ImportDeclaration' ? index : lastIndex
|
||||
}, -1)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLastImport,
|
||||
}
|
||||
17
services/web/transform/cjs-to-esm/fixMissingJSImports.sh
Executable file
17
services/web/transform/cjs-to-esm/fixMissingJSImports.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
script_dir=$(dirname "$0")
|
||||
FILES_TO_FIX=$(eslint . --format compact --no-color \
|
||||
| grep 'import/no-unresolved' \
|
||||
| cut -d':' -f1 \
|
||||
| sort -u)
|
||||
|
||||
if [ -z "$FILES_TO_FIX" ]; then
|
||||
echo "No files with 'import/no-unresolved' errors found. Nothing to do!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$FILES_TO_FIX" | xargs jscodeshift --parser=babel -t "$script_dir/codemods/fixMissingJsImports.mjs"
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
// 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',
|
||||
})
|
||||
}
|
||||
@@ -12,7 +12,9 @@ while true; do
|
||||
if [ -z "$FILES" ]; then
|
||||
break
|
||||
fi
|
||||
node transform/cjs-to-esm/cjs-to-esm.mjs "$FILES"
|
||||
# We want word splitting here
|
||||
# shellcheck disable=SC2086
|
||||
node transform/cjs-to-esm/cjs-to-esm.mjs $FILES
|
||||
done
|
||||
|
||||
make format_fix > /dev/null
|
||||
|
||||
88
services/web/transform/vitest/codemods/convertThisToCtx.js
Normal file
88
services/web/transform/vitest/codemods/convertThisToCtx.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @typedef {import('jscodeshift').ASTPath} ASTPath
|
||||
* @typedef {import('jscodeshift').JSCodeshift} JSCodeshift
|
||||
*/
|
||||
|
||||
const TARGET_CALLER_NAMES = new Set([
|
||||
'describe',
|
||||
'it',
|
||||
'before',
|
||||
'beforeEach',
|
||||
'after',
|
||||
'afterEach',
|
||||
])
|
||||
|
||||
/**
|
||||
* Helper function to check if a 'this' expression belongs directly to a given function scope,
|
||||
* and not to a nested traditional function defined within that scope.
|
||||
* @param {ASTPath<ThisExpression>} thisPath - The path to the 'this' expression.
|
||||
* @param {ASTPath<Function>} targetFunctionPath - The path to the target function scope.
|
||||
* @param {JSCodeshift} j - The jscodeshift instance.
|
||||
* @returns {boolean} - True if 'this' belongs to the target function scope.
|
||||
*/
|
||||
function isThisFromScope(thisPath, targetFunctionPath, j) {
|
||||
let current = thisPath.parentPath
|
||||
while (current && current.node !== targetFunctionPath.node) {
|
||||
if (
|
||||
(j.FunctionExpression.check(current.node) ||
|
||||
j.FunctionDeclaration.check(current.node)) &&
|
||||
current.node !== targetFunctionPath.node
|
||||
) {
|
||||
return false
|
||||
}
|
||||
current = current.parentPath
|
||||
}
|
||||
return !!current && current.node === targetFunctionPath.node
|
||||
}
|
||||
|
||||
module.exports = function transformer(file, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(file.source)
|
||||
|
||||
const functionsToModify = new Set()
|
||||
|
||||
root.find(j.CallExpression).forEach(callPath => {
|
||||
const callNode = callPath.node
|
||||
|
||||
if (
|
||||
j.Identifier.check(callNode.callee) &&
|
||||
TARGET_CALLER_NAMES.has(callNode.callee.name)
|
||||
) {
|
||||
callNode.arguments.forEach((arg, index) => {
|
||||
if (
|
||||
j.FunctionExpression.check(arg) ||
|
||||
j.FunctionDeclaration.check(arg)
|
||||
) {
|
||||
const functionArgumentPath = callPath.get('arguments', index)
|
||||
const containsRelevantThis = j(functionArgumentPath)
|
||||
.find(j.ThisExpression)
|
||||
.some(thisPath =>
|
||||
isThisFromScope(thisPath, functionArgumentPath, j)
|
||||
)
|
||||
|
||||
if (containsRelevantThis) {
|
||||
functionsToModify.add(functionArgumentPath)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
functionsToModify.forEach((functionPath /*: ASTPath<Function> */) => {
|
||||
const functionNode = functionPath.node
|
||||
|
||||
const hasCtxParam = functionNode.params.some(
|
||||
param => j.Identifier.check(param) && param.name === 'ctx'
|
||||
)
|
||||
if (!hasCtxParam) {
|
||||
functionNode.params.push(j.identifier('ctx'))
|
||||
}
|
||||
|
||||
j(functionPath)
|
||||
.find(j.ThisExpression)
|
||||
.filter(thisPath => isThisFromScope(thisPath, functionPath, j))
|
||||
.replaceWith(j.identifier('ctx'))
|
||||
})
|
||||
|
||||
return root.toSource({ quote: 'single' })
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {import('jscodeshift').FileInfo} file
|
||||
* @param {import('jscodeshift').API} api
|
||||
*/
|
||||
module.exports = function transformer(file, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(file.source)
|
||||
|
||||
const chaiImportCollection = root.find(j.ImportDeclaration, {
|
||||
source: {
|
||||
value: 'chai',
|
||||
},
|
||||
})
|
||||
|
||||
if (chaiImportCollection.length === 0) {
|
||||
return root.toSource()
|
||||
}
|
||||
|
||||
const chaiImport = chaiImportCollection.get(0).node
|
||||
const chaiSpecifiers = chaiImport.specifiers
|
||||
|
||||
if (!chaiSpecifiers || chaiSpecifiers.length === 0) {
|
||||
return root.toSource()
|
||||
}
|
||||
|
||||
const vitestImportCollection = root.find(j.ImportDeclaration, {
|
||||
source: {
|
||||
value: 'vitest',
|
||||
},
|
||||
})
|
||||
|
||||
if (vitestImportCollection.length > 0) {
|
||||
const vitestImport = vitestImportCollection.get(0).node
|
||||
|
||||
const existingVitestSpecifierNames = new Set(
|
||||
vitestImport.specifiers.map(specifier => specifier.imported.name)
|
||||
)
|
||||
|
||||
const newSpecifiers = chaiSpecifiers.filter(
|
||||
specifier => !existingVitestSpecifierNames.has(specifier.imported.name)
|
||||
)
|
||||
|
||||
if (newSpecifiers.length > 0) {
|
||||
vitestImport.specifiers.push(...newSpecifiers)
|
||||
}
|
||||
|
||||
chaiImportCollection.remove()
|
||||
} else {
|
||||
chaiImport.source.value = 'vitest'
|
||||
}
|
||||
|
||||
return root.toSource({ quote: 'single' })
|
||||
}
|
||||
137
services/web/transform/vitest/codemods/replaceDoneWithPromise.js
Normal file
137
services/web/transform/vitest/codemods/replaceDoneWithPromise.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @typedef {import('jscodeshift').FileInfo} FileInfo
|
||||
* @typedef {import('jscodeshift').API} API
|
||||
* @typedef {import('jscodeshift').Collection} Collection
|
||||
*/
|
||||
|
||||
module.exports = function transformer(file, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(file.source)
|
||||
|
||||
const mochaFunctionNames = new Set([
|
||||
'it',
|
||||
'specify',
|
||||
'before',
|
||||
'after',
|
||||
'beforeEach',
|
||||
'afterEach',
|
||||
])
|
||||
|
||||
root
|
||||
.find(j.CallExpression, {
|
||||
callee: {
|
||||
type: 'Identifier',
|
||||
name: name => mochaFunctionNames.has(name),
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
let callbackFunctionArg = null
|
||||
let funcArgIndex = -1
|
||||
|
||||
for (let i = path.node.arguments.length - 1; i >= 0; i--) {
|
||||
const arg = path.node.arguments[i]
|
||||
if (
|
||||
arg &&
|
||||
(arg.type === 'FunctionExpression' ||
|
||||
arg.type === 'ArrowFunctionExpression')
|
||||
) {
|
||||
callbackFunctionArg = arg
|
||||
funcArgIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!callbackFunctionArg) {
|
||||
return
|
||||
}
|
||||
|
||||
if (callbackFunctionArg.async) {
|
||||
return
|
||||
}
|
||||
|
||||
const params = callbackFunctionArg.params
|
||||
if (!params || params.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastParam = params[params.length - 1]
|
||||
if (
|
||||
!lastParam ||
|
||||
lastParam.type !== 'Identifier' ||
|
||||
lastParam.name !== 'done'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const doneParamName = lastParam.name
|
||||
|
||||
callbackFunctionArg.params.pop()
|
||||
|
||||
const originalBody = callbackFunctionArg.body
|
||||
|
||||
const bodyCollection = j(originalBody)
|
||||
bodyCollection
|
||||
.find(j.Identifier, { name: doneParamName })
|
||||
.forEach(identifierPath => {
|
||||
const parentNode = identifierPath.parentPath.node
|
||||
if (
|
||||
parentNode.type === 'MemberExpression' &&
|
||||
parentNode.property === identifierPath.node &&
|
||||
!parentNode.computed
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
parentNode.type === 'Property' &&
|
||||
parentNode.key === identifierPath.node &&
|
||||
!parentNode.shorthand
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
parentNode.type === 'LabeledStatement' &&
|
||||
parentNode.label === identifierPath.node
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
identifierPath.node.name = 'resolve'
|
||||
})
|
||||
|
||||
const resolveIdentifier = j.identifier('resolve')
|
||||
let newBodyBlock
|
||||
|
||||
if (originalBody.type === 'BlockStatement') {
|
||||
newBodyBlock = originalBody
|
||||
} else {
|
||||
newBodyBlock = j.blockStatement([j.expressionStatement(originalBody)])
|
||||
}
|
||||
|
||||
const promiseCallback = j.arrowFunctionExpression(
|
||||
[resolveIdentifier],
|
||||
newBodyBlock,
|
||||
false
|
||||
)
|
||||
promiseCallback.async = false
|
||||
|
||||
const newPromiseExpression = j.newExpression(j.identifier('Promise'), [
|
||||
promiseCallback,
|
||||
])
|
||||
|
||||
const newFunctionBody = j.expressionStatement(
|
||||
j.awaitExpression(newPromiseExpression)
|
||||
)
|
||||
|
||||
callbackFunctionArg.body = j.blockStatement([newFunctionBody])
|
||||
|
||||
callbackFunctionArg.async = true
|
||||
|
||||
console.log(
|
||||
`Transformed function in ${file.path} (argument ${funcArgIndex} of ${path.node.callee.name})`
|
||||
)
|
||||
})
|
||||
|
||||
return root.toSource({ quote: 'single' })
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
module.exports = function (fileInfo, api) {
|
||||
const j = api.jscodeshift
|
||||
const root = j(fileInfo.source)
|
||||
let shouldAddViImport = false
|
||||
let shouldRemoveSandboxedModule = false
|
||||
|
||||
root
|
||||
.find(j.CallExpression, {
|
||||
callee: {
|
||||
object: { name: 'SandboxedModule' },
|
||||
property: { name: 'require' },
|
||||
},
|
||||
})
|
||||
.forEach(path => {
|
||||
shouldRemoveSandboxedModule = true
|
||||
const args = path.node.arguments
|
||||
if (args.length > 0) {
|
||||
const assignmentStatement = path.parentPath.parentPath
|
||||
const firstArg = args[0]
|
||||
|
||||
// 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) {
|
||||
shouldAddViImport = true
|
||||
|
||||
const mocks = requiresProp.value.properties.map(mock => {
|
||||
const depPath =
|
||||
mock.key.type === 'Literal' ? mock.key.value : mock.key.name
|
||||
return j.expressionStatement(
|
||||
j.callExpression(j.identifier('vi.doMock'), [
|
||||
j.literal(depPath),
|
||||
j.arrowFunctionExpression(
|
||||
[],
|
||||
j.objectExpression([
|
||||
j.objectProperty(j.identifier('default'), mock.value),
|
||||
])
|
||||
),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
j(assignmentStatement).insertBefore(mocks)
|
||||
}
|
||||
}
|
||||
|
||||
// Create an expression to await the import of the module under test
|
||||
const awaitExpression = j.memberExpression(
|
||||
j.awaitExpression(
|
||||
j.callExpression(j.identifier('import'), [firstArg])
|
||||
),
|
||||
j.identifier('default')
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const alreadyHasViImport =
|
||||
root
|
||||
.find(j.ImportDeclaration, {
|
||||
source: { value: 'vitest' },
|
||||
})
|
||||
.size() > 0
|
||||
|
||||
if (shouldAddViImport && !alreadyHasViImport) {
|
||||
root
|
||||
.get()
|
||||
.node.program.body.unshift(
|
||||
j.importDeclaration(
|
||||
[j.importSpecifier(j.identifier('vi'))],
|
||||
j.literal('vitest')
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (shouldRemoveSandboxedModule) {
|
||||
root
|
||||
.find(j.ImportDeclaration, {
|
||||
source: { value: 'sandboxed-module' },
|
||||
})
|
||||
.remove()
|
||||
}
|
||||
|
||||
return root.toSource({
|
||||
quote: 'single',
|
||||
})
|
||||
}
|
||||
91
services/web/transform/vitest/mocha-to-vitest.mjs
Normal file
91
services/web/transform/vitest/mocha-to-vitest.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import minimist from 'minimist'
|
||||
import { exec } from 'node:child_process'
|
||||
import { promisify } from 'node:util'
|
||||
import Runner from 'jscodeshift/src/Runner.js'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
// use minimist to get a list of files from the argv
|
||||
const {
|
||||
dryRun,
|
||||
verbose,
|
||||
usage,
|
||||
_: files,
|
||||
} = minimist(process.argv.slice(2), {
|
||||
boolean: ['dryRun', 'usage', 'verbose'],
|
||||
})
|
||||
|
||||
function printUsage() {
|
||||
console.log(
|
||||
'node scripts/vitest/mocha-to-vitest.mjs [files] [--dryRun] [--format] [--lint] [--usage] [--verbose]'
|
||||
)
|
||||
console.log(
|
||||
'WARNING: this will only work in local development as important dependencies will be missing in production'
|
||||
)
|
||||
console.log('Options:')
|
||||
console.log(' files: a list of files to convert')
|
||||
console.log('--dryRun: do not actually run the commands, just print them')
|
||||
console.log('--format: run prettier to fix formatting')
|
||||
console.log(' --lint: run eslint to fix linting')
|
||||
console.log(' --usage: show this help message')
|
||||
console.log('--verbose: enable verbose output')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (usage) {
|
||||
printUsage()
|
||||
}
|
||||
|
||||
if (!Array.isArray(files) || files.length === 0) {
|
||||
console.error('You must provide a list of files to convert')
|
||||
printUsage()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const promisifiedExec = promisify(exec)
|
||||
|
||||
const transforms = [
|
||||
'./codemods/replaceDoneWithPromise.js',
|
||||
'./codemods/convertThisToCtx.js',
|
||||
'./codemods/replaceSandboxedModuleWithDoMock.js',
|
||||
'./codemods/replaceDirectChaiUsage.js',
|
||||
]
|
||||
|
||||
const config = {
|
||||
output: import.meta.dirname,
|
||||
silent: verbose,
|
||||
print: false,
|
||||
verbose: verbose ? 1 : 0,
|
||||
hoist: true,
|
||||
dry: dryRun,
|
||||
runInBand: true,
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log('Dry run mode enabled. No changes will be made.')
|
||||
}
|
||||
|
||||
for (const transformPath of transforms) {
|
||||
if (verbose) {
|
||||
console.log(`Running transform: ${transformPath}`)
|
||||
}
|
||||
const transform = fileURLToPath(await import.meta.resolve(transformPath))
|
||||
await Runner.run(transform, files, config)
|
||||
}
|
||||
|
||||
const webRoot = fileURLToPath(new URL('../../', import.meta.url))
|
||||
|
||||
if (!dryRun) {
|
||||
for (const file of files) {
|
||||
// move files with git mv
|
||||
const newFileName = file
|
||||
.replace('Tests.mjs', '.test.mjs')
|
||||
.replace('Tests.js', '.test.js')
|
||||
.replace('Test.js', '.test.js')
|
||||
.replace('Test.mjs', '.test.mjs')
|
||||
|
||||
await promisifiedExec(`git mv ${file} ${newFileName}`)
|
||||
const relativePath = path.relative(webRoot, file)
|
||||
console.log(`transformed ${relativePath} and renamed it for vitest`)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user