Skip to content

Commit

Permalink
improved CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
christian-bromann committed Feb 26, 2024
1 parent b9244be commit 3db8556
Show file tree
Hide file tree
Showing 16 changed files with 310 additions and 86 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,34 @@ Running this with `bx` results in:
```sh
> npx bx ./html.html
Hello World!
```

## Session Management

If you like to speed up your execution, you can create browser sessions on your system and run scripts through them immediately without having to spin up the browser. You can create a session via:

```sh
# create a session with random session name, e.g. "chrome-1"
npx bx session --browserName chrome
# create a session with custom name
npx bx session --browserName chrome --name chrome
```

You can now run scripts faster by providing a session name:

```sh
npx bx ./script.ts --sessionName chrome
```

To view all opened sessions, run:

```sh
npx bx session
```

Kill specific or all sessions via:

```sh
npx bx session --kill chrome
npx bx session --killAll
```
File renamed without changes.
19 changes: 18 additions & 1 deletion package-lock.json

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

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"homepage": "https://github.com/webdriverio/bx#readme",
"devDependencies": {
"@types/node": "^20.11.19",
"@types/yargs": "^17.0.32",
"@vitest/coverage-v8": "^1.2.2",
"npm-run-all": "^4.1.5",
"rimraf": "^5.0.5",
Expand All @@ -38,6 +39,7 @@
},
"dependencies": {
"vite": "^5.1.3",
"webdriverio": "^8.32.0"
"webdriverio": "^8.32.0",
"yargs": "^17.7.2"
}
}
4 changes: 4 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import * as runCmd from './run.js'
import * as sessionCmd from './session.js'

export default [runCmd, sessionCmd]
71 changes: 71 additions & 0 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import type { Argv } from 'yargs'

import { ViteServer } from '../server.js'
import { run } from '../runner.js'
import { CLI_EPILOGUE } from '../constants.js'

export const command = '<target> [options]'
export const desc = 'Run script, html file or URL.'
export const cmdArgs = {
browserName: {
type: 'string',
alias: 'b',
description: 'Name of the browser to use. Defaults to "chrome".'
},
browserVersion: {
type: 'string',
alias: 'v',
description: 'Version of the browser to use.'
},
headless: {
type: 'boolean',
alias: 'h',
description: 'Run the browser in headless mode. Defaults to true in CI environments.'
},
rootDir: {
type: 'string',
alias: 'r',
description: 'Root directory of the project. Defaults to the current working directory.'
}
} as const

const yargsInstance = yargs(hideBin(process.argv)).options(cmdArgs)

export const builder = (yargs: Argv) => {
return yargs
.example('$0 ./script.js --browserName chrome', 'Run a JavaScript script with Chrome')
.example('$0 ./script.ts --browserName chrome', 'Run a TypeScript file with Chrome')
.example('$0 ./site.html --browserName chrome', 'Run a HTML file with Chrome')
.example('$0 http://localhost:8080 --browserName chrome', 'Run a website with Chrome')
.epilogue(CLI_EPILOGUE)
.help()
}

export const handler = async () => {
const params = await yargsInstance.parse()
const rootDir = params.rootDir || process.cwd()
const server = new ViteServer({ root: rootDir })
const target = params._[0] as string | undefined

if (!target) {
console.error('Error: No target provided')
process.exit(1)
}

if (target.startsWith('http')) {
console.error('Error: Running URLs is not supported yet')
process.exit(1)
}

try {
const env = await server.start(target)
await run(env, params)
} catch (err) {
console.error('Error:', (err as Error).message)
process.exit(1)
} finally {
await server.stop()
}
}
88 changes: 88 additions & 0 deletions src/cli/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import type { Argv } from 'yargs'

import { cmdArgs as runCmdArgs } from './run.js'
import { CLI_EPILOGUE } from '../constants.js'
import { initBrowserSession } from '../utils.js'
import { SessionManager } from '../session.js'

export const command = 'session [options]'
export const desc = 'Manage `bx` sessions.'
export const cmdArgs = {
name: {
type: 'string',
alias: 's',
description: 'Name of the session.'
},
kill: {
type: 'string',
description: 'Kill a session by name.'
},
killAll: {
type: 'boolean',
description: 'Kill all sessions.'
},
...runCmdArgs
} as const

const yargsInstance = yargs(hideBin(process.argv)).options(cmdArgs)

export const builder = (yargs: Argv) => {
return yargs
.example('$0 session --browserName chrome', 'Create a new session with Chrome')
.example('$0 session --name myChromeSession --browserName chrome', 'Create a named session with Chrome')
.example('$0 session --killAll', 'Kill all local sessions')
.example('$0 session --kill myChromeSession ', 'Kill a local session by name')
.epilogue(CLI_EPILOGUE)
.help()
}

export const handler = async () => {
const params = await yargsInstance.parse()

if (typeof params.kill === 'string') {
await SessionManager.deleteSession(params.kill)
return console.log(`Session "${params.kill}" stopped`)
}

if (params.killAll) {
await SessionManager.deleteAllSessions()
return console.log('All sessions stopped')
}

const browserName = params.browserName

/**
* if browserName is not provided, list all sessions
*/
if (!browserName) {
const sessions = await SessionManager.listSessions()
if (sessions.length === 0) {
return console.log('No sessions found!')
}

console.log('Available sessions:')
for (const session of sessions) {
console.log(`- ${session.name} (${session.capabilities.browserName} ${session.capabilities.browserVersion})`)
}
return
}

let sessionName = params.name
/**
* if no session name is provided, generate a random one
*/
if (!sessionName) {
const sessions = await SessionManager.listSessions()
const browserNameSessions = sessions.filter((session) => session.requestedCapabilities.browserName === browserName)
sessionName = `${browserName}-${browserNameSessions.length}`
}

const headless = Boolean(params.headless)
const rootDir = params.rootDir || process.cwd()
const browser = await initBrowserSession({ ...params, rootDir, headless, browserName })
await SessionManager.saveSession(browser, sessionName)
console.log(`Session "${sessionName}" started, you can now run scripts faster e.g. \`npx bx ./script.js --sessionName ${sessionName}\``)
process.exit(0)
}
19 changes: 0 additions & 19 deletions src/commands/index.ts

This file was deleted.

28 changes: 0 additions & 28 deletions src/commands/session.ts

This file was deleted.

9 changes: 8 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createRequire } from 'node:module'

export const SUPPORTED_FILE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.html']
export const IS_CI = Boolean(process.env.HEADLESS || process.env.CI)
export const CLI_OPTIONS = {
Expand All @@ -12,4 +14,9 @@ export const PARSE_OPTIONS = {
options: CLI_OPTIONS,
tokens: true,
allowPositionals: true
} as const
} as const

const require = createRequire(import.meta.url)
export const pkg = require('../package.json')

export const CLI_EPILOGUE = `bx (v${pkg.version})`
40 changes: 13 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,18 @@
import { parseArgs } from 'node:util'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'

import { parseFileName } from './utils.js'
import { CLI_OPTIONS, PARSE_OPTIONS } from './constants.js'
import { runCommand, startSession, stopSession } from './commands/index.js'
import type { RunnerArgs } from './types.js'
import commands from './cli/index.js'
import { handler } from './cli/run.js'

export default async function cli () {
const { values, tokens, positionals } = parseArgs(PARSE_OPTIONS)

;(tokens || []).filter((token) => token.kind === 'option').forEach((token: any) => {
if (token.name.startsWith('no-')) {
// Store foo:false for --no-foo
const positiveName = token.name.slice(3) as 'headless'
values[positiveName] = false
delete values[token.name as 'headless']
} else {
// Re-save value so last one wins if both --foo and --no-foo.
values[token.name as keyof typeof CLI_OPTIONS] = token.value ?? true
}
})

if (positionals.includes('session')) {
if (positionals.includes('stop')) {
return stopSession(values as RunnerArgs)
}

return startSession(values as RunnerArgs)
const argv = yargs(hideBin(process.argv))
.command(commands)
.help()
.argv

const cmdNames = commands.map((command: { command: string }) => command.command.split(' ')[0])
const params = await argv
if (!cmdNames.includes(`${params['_'][0]}`)) {
await handler()
}

return runCommand(parseFileName(positionals[0]), values as RunnerArgs)
}
Loading

0 comments on commit 3db8556

Please sign in to comment.