#!/usr/bin/env node
import fs from 'node:fs'
import url from 'node:url'
import path from 'node:path'
import camelCase from 'camelcase'
import { type Plugin } from 'esbuild'
import { paramTypeMap, returnTypeMap } from './constants.js'
import { PROTOCOLS } from '../../../../scripts/protocols.js'
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
const TYPINGS_PATH = path.join(__dirname, '..', '..', '..', '..', 'packages', 'wdio-protocols', 'src', 'commands')
const INDENTATION = ' '.repeat(4)
const EXAMPLE_INDENTATION = `${INDENTATION} * `
const DEPRECATED_INDENTATION = `\n${INDENTATION} * @deprecated`
const jsDocTemplate = `
${INDENTATION}/**
${INDENTATION} * {PROTOCOL} Protocol Command
${INDENTATION} *
${INDENTATION} * {DESCRIPTION}
${INDENTATION} * @ref {REF}
${INDENTATION} *{EXAMPLE}
${INDENTATION} */`
export function generateTypes (): Plugin {
return {
name: 'generate-types',
setup (build) {
build.onStart(() => generate())
}
}
}
function generate () {
* create directory if not existing
*/
if (!fs.existsSync(TYPINGS_PATH)) {
fs.mkdirSync(TYPINGS_PATH)
}
for (const [protocolName, definition] of Object.entries(PROTOCOLS)) {
if (protocolName === 'webdriverBidi') {
continue
}
const interfaceName = protocolName.slice(0, 1).toUpperCase() + protocolName.slice(1)
const customTypes = new Set()
const lines = ['']
lines.push(`// ${protocolName} types`)
lines.push(`export default interface ${interfaceName}Commands {`)
for (const methods of Object.values(definition)) {
for (const description of Object.values(methods)) {
const { command, parameters = [], variables = [], returns, ref, deprecated, examples } = description
if (!ref) {
throw new Error(`missing ref for command ${command} in ${protocolName}`)
}
const vars = variables
.filter((v) => v.name !== 'sessionId')
.map((v) => `${v.name}: string`)
const params = parameters.map((p, idx) => {
const hasCustomType = (
paramTypeMap[command as keyof typeof paramTypeMap] &&
paramTypeMap[command as keyof typeof paramTypeMap][idx] &&
paramTypeMap[command as keyof typeof paramTypeMap][idx].name === p.name
)
const paramType = hasCustomType
? paramTypeMap[command as keyof typeof paramTypeMap][idx].type
: p.type.toLowerCase()
if (hasCustomType && Boolean(paramTypeMap[command as keyof typeof paramTypeMap][idx].requiresImport)) {
customTypes.add(paramType)
}
return `${camelCase(p.name)}${p.required !== true ? '?' : ''}: ${paramType}`
})
const varsAndParams = vars.concat(params)
let returnValue = returns ? returns.type.toLowerCase() : 'void'
returnValue = returnValue === '*' ? 'any' : returnValue
if (returnTypeMap[command as keyof typeof returnTypeMap]) {
returnValue = returnTypeMap[command as keyof typeof returnTypeMap]
customTypes.add(returnTypeMap[command as keyof typeof returnTypeMap].replace('[]', ''))
}
if (returnValue === 'object') {
returnValue = 'ProtocolCommandResponse'
customTypes.add(returnValue)
}
const jsDoc = jsDocTemplate
.replace('{PROTOCOL}', interfaceName)
.replace('{DESCRIPTION}', description.description || 'No description available, please see reference link.')
.replace('{EXAMPLE}', (
(examples || [])
.map((example) => (
`\n${EXAMPLE_INDENTATION}@example\n` +
`${EXAMPLE_INDENTATION}\`\`\`js\n` +
EXAMPLE_INDENTATION +
`${example.map((l, i) => (i === 0
? `${l}`
: `${EXAMPLE_INDENTATION}${l.replace(/(\/\*\*|\s\*\s|\s\*\/)/, '// ')}`.trimEnd())
).join('\n')}\n` +
`${EXAMPLE_INDENTATION}` + '```'
))
.join(`\n${EXAMPLE_INDENTATION}`.trim())
)
)
.replace('{REF}', deprecated ? `{REF}${DEPRECATED_INDENTATION} ${deprecated}` : '{REF}')
.replace('{REF}', ref)
lines.push(jsDoc)
lines.push(`${INDENTATION}${command}(${varsAndParams.join(', ')}): Promise<${returnValue}>;`)
}
}
* import missing protocol types
*/
if (customTypes.size) {
lines.unshift(`import type { ${[...customTypes].join(', ')} } from '../types.js'`)
}
lines.push('}')
fs.writeFileSync(path.join(TYPINGS_PATH, `${protocolName}.ts`), lines.join('\n'), 'utf8')
console.log(` 🧩 Generated typings file for ${protocolName}`)
}
}