Merge pull request #33538 from overleaf/ar-remove-web-transform-vitest

[web] remove mocha-to-vitest transform script

GitOrigin-RevId: b33f1587846640a8823ff963e789cde4aaae081f
This commit is contained in:
Andrew Rumble
2026-09-17 11:54:28 +00:00
committed by Copybot
parent 3edb312a35
commit ff14d94c04
5 changed files with 0 additions and 476 deletions
@@ -1,88 +0,0 @@
/**
* @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' })
}
@@ -1,55 +0,0 @@
// @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' })
}
@@ -1,137 +0,0 @@
/**
* @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' })
}
@@ -1,105 +0,0 @@
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',
})
}
@@ -1,91 +0,0 @@
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`)
}
}