Skip to content

chore: remove babel dependency #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/vscode-wdio-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@
"clean": "shx rm -rf out dist coverage"
},
"dependencies": {
"@babel/parser": "^7.27.0",
"@babel/types": "^7.27.0",
"@babel/parser": "^7.27.5",
"@cucumber/gherkin": "^32.1.0",
"@cucumber/messages": "^27.2.0",
"@vscode-wdio/constants": "workspace:*",
Expand Down
122 changes: 71 additions & 51 deletions packages/vscode-wdio-worker/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,72 +2,93 @@ import fs from 'node:fs/promises'
import path from 'node:path'
import { pathToFileURL } from 'node:url'

import * as parser from '@babel/parser'
import * as t from '@babel/types'
import recast from 'recast'
import { parse, print, visit, types as t } from 'recast'
// @ts-ignore
import typescriptParser from 'recast/parsers/typescript'

const reporterIdentifierName = 'VscodeJsonReporter'

// This file is bundle as parser/ats.js at the package of vscode-webdriverio
// So, the correct reporter path is parent directory
const VSCODE_REPORTER_PATH = path.resolve(__dirname, '../reporter.cjs')

/**
* Create AST nodes using ast-types builders
*/
const b = t.builders

/**
* Since Windows cannot import by reporter file path due to issues with
* the `initializePlugin` method of wdio-utils, the policy is to create a temporary configuration file.
*/
export async function createTempConfigFile(filename: string, outDir: string) {
const source = await fs.readFile(filename, { encoding: 'utf8' })
const ast = recast.parse(source, {
parser: {
parse(source: string) {
return parser.parse(source, {
sourceType: 'unambiguous',
plugins: ['typescript', 'jsx', 'topLevelAwait'],
})
},
},
const ast = parse(source, {
parser: typescriptParser,
})

const reporterIdentifier = t.identifier(reporterIdentifierName)
const reporterConfigIdentifier = t.identifier(`${reporterIdentifierName}.default || ${reporterIdentifierName}`)
const reporterElement = t.arrayExpression([
const reporterIdentifier = b.identifier(reporterIdentifierName)
const reporterConfigIdentifier = b.identifier(`${reporterIdentifierName}.default || ${reporterIdentifierName}`)
const reporterElement = b.arrayExpression([
reporterConfigIdentifier,
t.objectExpression([
t.objectProperty(t.identifier('stdout'), t.booleanLiteral(true)),
t.objectProperty(t.identifier('outputDir'), t.stringLiteral(outDir)),
b.objectExpression([
b.property('init', b.identifier('stdout'), b.literal(true)),
b.property('init', b.identifier('outputDir'), b.literal(outDir)),
]),
])
let hasReporterImport = false

function addOrUpdateReporters(configObject: t.Node) {
if (!t.isObjectExpression(configObject)) {
function addOrUpdateReporters(configObject: any) {
if (!t.namedTypes.ObjectExpression.check(configObject)) {
return
}

const reportersProp = configObject.properties.find(
(prop) =>
t.isObjectProperty(prop) &&
((t.isIdentifier(prop.key) && prop.key.name === 'reporters') ||
(t.isStringLiteral(prop.key) && prop.key.value === 'reporters'))
)
// Find existing reporters property
let reportersProp = null

for (let i = 0; i < configObject.properties.length; i++) {
const prop = configObject.properties[i]

// Check for both Property and ObjectProperty nodes
if (t.namedTypes.Property.check(prop) || t.namedTypes.ObjectProperty?.check?.(prop)) {
const isReportersKey =
(t.namedTypes.Identifier.check(prop.key) && prop.key.name === 'reporters') ||
(t.namedTypes.Literal.check(prop.key) && prop.key.value === 'reporters')

if (isReportersKey) {
reportersProp = prop
break
}
}
}

if (reportersProp && t.isObjectProperty(reportersProp) && t.isArrayExpression(reportersProp.value)) {
if (reportersProp && t.namedTypes.ArrayExpression.check(reportersProp.value)) {
// Add to existing reporters array
reportersProp.value.elements.push(reporterElement)
} else if (reportersProp) {
// Replace existing non-array reporters with array including existing value
const existingValue = reportersProp.value
//@ts-ignore
reportersProp.value = b.arrayExpression([existingValue, reporterElement])
} else {
// Add new reporters property
configObject.properties.push(
t.objectProperty(t.identifier('reporters'), t.arrayExpression([reporterElement]))
b.property('init', b.identifier('reporters'), b.arrayExpression([reporterElement]))
)
}
}

recast.types.visit(ast, {
visit(ast, {
visitImportDeclaration(path) {
const { source, specifiers } = path.node
if (
source.value === pathToFileURL(VSCODE_REPORTER_PATH).href &&
specifiers &&
//@ts-ignore
specifiers.some((s) => t.isImportDefaultSpecifier(s) && s.local.name === reporterIdentifierName)
specifiers.some(
//@ts-ignore
(s: any) => t.namedTypes.ImportDefaultSpecifier.check(s) && s.local.name === reporterIdentifierName
)
) {
hasReporterImport = true
}
Expand All @@ -77,21 +98,20 @@ export async function createTempConfigFile(filename: string, outDir: string) {
visitExportNamedDeclaration(path) {
const decl = path.node.declaration

// @ts-ignore
if (t.isVariableDeclaration(decl)) {
if (t.namedTypes.VariableDeclaration.check(decl)) {
const first = decl.declarations[0]

if (t.isVariableDeclarator(first)) {
if (t.namedTypes.VariableDeclarator.check(first)) {
const id = first.id
const init = first.init

if (t.isIdentifier(id) && id.name === 'config') {
if (t.isObjectExpression(init)) {
if (t.namedTypes.Identifier.check(id) && id.name === 'config') {
if (t.namedTypes.ObjectExpression.check(init)) {
addOrUpdateReporters(init)
} else if (
t.isCallExpression(init) &&
t.namedTypes.CallExpression.check(init) &&
init.arguments.length > 0 &&
t.isObjectExpression(init.arguments[0])
t.namedTypes.ObjectExpression.check(init.arguments[0])
) {
const configObject = init.arguments[0]
addOrUpdateReporters(configObject)
Expand All @@ -111,12 +131,10 @@ export async function createTempConfigFile(filename: string, outDir: string) {
}

if (
// @ts-ignore
t.isMemberExpression(left) &&
t.isIdentifier(left.object) &&
t.isIdentifier(left.property) &&
// @ts-ignore
t.isObjectExpression(right)
t.namedTypes.MemberExpression.check(left) &&
t.namedTypes.Identifier.check(left.object) &&
t.namedTypes.Identifier.check(left.property) &&
t.namedTypes.ObjectExpression.check(right)
) {
const leftName = `${left.object.name}.${left.property.name}`
if (['module.exports', 'exports.config'].includes(leftName)) {
Expand All @@ -128,13 +146,15 @@ export async function createTempConfigFile(filename: string, outDir: string) {
},

visitCallExpression(path) {
const node = path.node as t.Node
const node = path.node

if (
t.isCallExpression(node) &&
t.isIdentifier(node.callee, { name: 'require' }) &&
t.namedTypes.CallExpression.check(node) &&
t.namedTypes.Identifier.check(node.callee) &&
node.callee.name === 'require' &&
node.arguments.length === 1 &&
t.isStringLiteral(node.arguments[0]) &&
t.namedTypes.Literal.check(node.arguments[0]) &&
typeof node.arguments[0].value === 'string' &&
node.arguments[0].value === pathToFileURL(VSCODE_REPORTER_PATH).href
) {
hasReporterImport = true
Expand All @@ -145,15 +165,15 @@ export async function createTempConfigFile(filename: string, outDir: string) {
})

if (!hasReporterImport) {
const importedModule = t.importDeclaration(
[t.importDefaultSpecifier(reporterIdentifier)],
t.stringLiteral(pathToFileURL(VSCODE_REPORTER_PATH).href)
const importedModule = b.importDeclaration(
[b.importDefaultSpecifier(reporterIdentifier)],
b.literal(pathToFileURL(VSCODE_REPORTER_PATH).href)
)

ast.program.body.unshift(importedModule)
}

const output = recast.print(ast).code
const output = print(ast).code
const ext = path.extname(filename)
const _filename = path.join(path.dirname(filename), `wdio-vscode-${new Date().getTime()}${ext}`)
await fs.writeFile(_filename, output)
Expand Down
70 changes: 22 additions & 48 deletions packages/vscode-wdio-worker/src/parsers/js.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as babelParser from '@babel/parser'
import * as t from '@babel/types'
import { parse, types } from 'recast'
import { parse, visit, types as t } from 'recast'
// @ts-ignore
import typescriptParser from 'recast/parsers/typescript'
import type { TestData, SourceRange, WorkerMetaContext } from '@vscode-wdio/types/worker'

/**
* Parse WebdriverIO test files and extract test cases using Recast and Babel parser
* Parse WebdriverIO test files and extract test cases using Recast with TypeScript parser
*
* @param fileContent Content of the test file
* @param uri File URI for error reporting
Expand All @@ -20,35 +20,9 @@ export function parseTestCases(this: WorkerMetaContext, fileContent: string, uri
const testBlocksMap = new Map<string, TestData>()

try {
// Parse the file content with Recast and Babel parser to handle TypeScript
// Parse the file content with Recast TypeScript parser
const ast = parse(fileContent, {
parser: {
parse: (source: string) => {
try {
return babelParser.parse(source, {
sourceType: 'module',
plugins: [
'typescript',
'jsx',
'decorators-legacy',
'classProperties',
'exportDefaultFrom',
'exportNamespaceFrom',
'dynamicImport',
'objectRestSpread',
'optionalChaining',
'nullishCoalescingOperator',
],
tokens: true,
ranges: true,
})
} catch (parseError) {
// Provide more detailed error information
const errorMessage = (parseError as Error).message
throw new Error(`Babel parser error: ${errorMessage}`)
}
},
},
parser: typescriptParser,
})

// Process the AST to extract test blocks
Expand All @@ -65,15 +39,14 @@ export function parseTestCases(this: WorkerMetaContext, fileContent: string, uri
* @param ast The parsed AST
* @param testCases Array to store top-level test cases
* @param testBlocksMap Map to track all test blocks for hierarchy building
* @param fileContent Original file content for line calculations
*/
function processAst(ast: any, testCases: TestData[], testBlocksMap: Map<string, TestData>): void {
// Stack to track current describe block context
const blockStack: TestData[] = []
const blockIdSet = new Set<string>()

// Traverse the AST
types.visit(ast, {
// Traverse the AST using recast's visit function
visit(ast, {
// Visit call expressions to find describe, it, and test blocks
visitCallExpression(path) {
const node = path.node
Expand Down Expand Up @@ -125,13 +98,14 @@ function processAst(ast: any, testCases: TestData[], testBlocksMap: Map<string,
if (callbackArg) {
// Handle both regular and async functions
if (
t.isArrowFunctionExpression(callbackArg as t.Node) ||
t.isFunctionExpression(callbackArg as t.Node)
t.namedTypes.ArrowFunctionExpression.check(callbackArg) ||
t.namedTypes.FunctionExpression.check(callbackArg)
) {
const body = (callbackArg as t.ArrowFunctionExpression | t.FunctionExpression).body
// @ts-ignore
const body = callbackArg.body

// For arrow functions with expression body, we don't traverse
if (t.isBlockStatement(body)) {
if (t.namedTypes.BlockStatement.check(body)) {
// For block statements, traverse the body
this.traverse(path.get('arguments', 1))
}
Expand Down Expand Up @@ -190,14 +164,14 @@ function createSourceRangeFromLocation(loc: any): SourceRange {
*/
function isTestBlockCall(node: any): boolean {
// Direct call (describe, it, test)
if (t.isIdentifier(node.callee) && ['describe', 'it', 'test'].includes(node.callee.name)) {
if (t.namedTypes.Identifier.check(node.callee) && ['describe', 'it', 'test'].includes(node.callee.name)) {
return true
}

// Method chain call (describe.skip, it.only, etc.)
if (
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object) &&
t.namedTypes.MemberExpression.check(node.callee) &&
t.namedTypes.Identifier.check(node.callee.object) &&
['describe', 'it', 'test'].includes(node.callee.object.name)
) {
return true
Expand All @@ -215,7 +189,7 @@ function isTestBlockCall(node: any): boolean {
*/
function getTestBlockType(node: any): 'describe' | 'it' | 'test' | null {
// Direct call
if (t.isIdentifier(node.callee)) {
if (t.namedTypes.Identifier.check(node.callee)) {
if (node.callee.name === 'describe') {
return 'describe'
}
Expand All @@ -225,7 +199,7 @@ function getTestBlockType(node: any): 'describe' | 'it' | 'test' | null {
if (node.callee.name === 'test') {
return 'test'
}
} else if (t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object)) {
} else if (t.namedTypes.MemberExpression.check(node.callee) && t.namedTypes.Identifier.check(node.callee.object)) {
// Method chain call (e.g., describe.skip, it.only)

if (node.callee.object.name === 'describe') {
Expand Down Expand Up @@ -255,17 +229,17 @@ function extractTestName(node: any): string | null {
}

// String literal
if (t.isStringLiteral(node)) {
if (t.namedTypes.Literal.check(node) && typeof node.value === 'string') {
return node.value
}

// Template literal
if (t.isTemplateLiteral(node)) {
return node.quasis.map((q) => q.value.cooked).join('${...}')
if (t.namedTypes.TemplateLiteral.check(node)) {
return node.quasis.map((q: any) => q.value.cooked).join('${...}')
}

// Binary expression (string concatenation)
if (t.isBinaryExpression(node) && node.operator === '+') {
if (t.namedTypes.BinaryExpression.check(node) && node.operator === '+') {
const left = extractTestName(node.left)
const right = extractTestName(node.right)

Expand Down
5 changes: 1 addition & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.