From ff14d94c049dc7c5382673fd052540b02b89408d Mon Sep 17 00:00:00 2001 From: Andrew Rumble Date: Thu, 13 Aug 2026 11:15:20 +0100 Subject: [PATCH] Merge pull request #33538 from overleaf/ar-remove-web-transform-vitest [web] remove mocha-to-vitest transform script GitOrigin-RevId: b33f1587846640a8823ff963e789cde4aaae081f --- .../vitest/codemods/convertThisToCtx.js | 88 ----------- .../vitest/codemods/replaceDirectChaiUsage.js | 55 ------- .../vitest/codemods/replaceDoneWithPromise.js | 137 ------------------ .../replaceSandboxedModuleWithDoMock.js | 105 -------------- .../web/transform/vitest/mocha-to-vitest.mjs | 91 ------------ 5 files changed, 476 deletions(-) delete mode 100644 services/web/transform/vitest/codemods/convertThisToCtx.js delete mode 100644 services/web/transform/vitest/codemods/replaceDirectChaiUsage.js delete mode 100644 services/web/transform/vitest/codemods/replaceDoneWithPromise.js delete mode 100644 services/web/transform/vitest/codemods/replaceSandboxedModuleWithDoMock.js delete mode 100644 services/web/transform/vitest/mocha-to-vitest.mjs diff --git a/services/web/transform/vitest/codemods/convertThisToCtx.js b/services/web/transform/vitest/codemods/convertThisToCtx.js deleted file mode 100644 index cf733510ea..0000000000 --- a/services/web/transform/vitest/codemods/convertThisToCtx.js +++ /dev/null @@ -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} thisPath - The path to the 'this' expression. - * @param {ASTPath} 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 */) => { - 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' }) -} diff --git a/services/web/transform/vitest/codemods/replaceDirectChaiUsage.js b/services/web/transform/vitest/codemods/replaceDirectChaiUsage.js deleted file mode 100644 index 350c76e70d..0000000000 --- a/services/web/transform/vitest/codemods/replaceDirectChaiUsage.js +++ /dev/null @@ -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' }) -} diff --git a/services/web/transform/vitest/codemods/replaceDoneWithPromise.js b/services/web/transform/vitest/codemods/replaceDoneWithPromise.js deleted file mode 100644 index 282cba7e0a..0000000000 --- a/services/web/transform/vitest/codemods/replaceDoneWithPromise.js +++ /dev/null @@ -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' }) -} diff --git a/services/web/transform/vitest/codemods/replaceSandboxedModuleWithDoMock.js b/services/web/transform/vitest/codemods/replaceSandboxedModuleWithDoMock.js deleted file mode 100644 index 172ab934a2..0000000000 --- a/services/web/transform/vitest/codemods/replaceSandboxedModuleWithDoMock.js +++ /dev/null @@ -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', - }) -} diff --git a/services/web/transform/vitest/mocha-to-vitest.mjs b/services/web/transform/vitest/mocha-to-vitest.mjs deleted file mode 100644 index 6b846c964e..0000000000 --- a/services/web/transform/vitest/mocha-to-vitest.mjs +++ /dev/null @@ -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`) - } -}