Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f5c9610
move to rollup
cacieprins Dec 18, 2025
d445a7c
no longer need to move some subdirs in start-build, special bin dir h…
cacieprins Dec 29, 2025
b7de399
Merge branch 'develop' into refactor-cli-rollup
cacieprins Dec 29, 2025
1950dd5
preserve modules so __dirname is properly resolved in spawn.ts
cacieprins Dec 30, 2025
cdb35ac
ensure lib/exec/xvfb exports default
cacieprins Dec 30, 2025
fa75f41
make build processes more intuitive
cacieprins Dec 30, 2025
1424659
additional
cacieprins Dec 30, 2025
513ef75
and
cacieprins Dec 30, 2025
157f780
fix clean
cacieprins Dec 30, 2025
e9b35c3
fix tests
cacieprins Dec 30, 2025
a8dcefb
revert
cacieprins Dec 30, 2025
3401af4
rename clean cmd
cacieprins Dec 30, 2025
d7965dd
fix cjs exports
cacieprins Dec 31, 2025
d6c80d8
fix test import
cacieprins Dec 31, 2025
7cbb83f
fix tertiary dep bundling
cacieprins Dec 31, 2025
aa86fd6
maybe..?
cacieprins Dec 31, 2025
e40698e
ensure cli is built before tests are run
cacieprins Dec 31, 2025
c644984
exports= again
cacieprins Jan 5, 2026
dbefeff
discrete exports
cacieprins Jan 5, 2026
5a66643
maybe if tslib is bundled
cacieprins Jan 5, 2026
645fb6e
add tslib as dependency for rollup
cacieprins Jan 5, 2026
1fe7edf
lockfile, no longer bundle
cacieprins Jan 5, 2026
b2a42ef
bundle tslib?
cacieprins Jan 5, 2026
0c4c1f5
cache
cacieprins Jan 6, 2026
cc93887
preserve entry file paths in dist dir
cacieprins Jan 7, 2026
dc9f262
relative path change
cacieprins Jan 9, 2026
5994305
ensure the cjs entrypoint for the esm build of cli is available in dist
cacieprins Jan 12, 2026
15b83ee
properly import json so rollup picks it up
cacieprins Jan 12, 2026
890d30e
ensure bin entrypoint has no file extension
cacieprins Jan 12, 2026
8bb047a
add exec/run.ts ? to entry files
cacieprins Jan 13, 2026
f0d6fc8
Revert "add exec/run.ts ? to entry files"
cacieprins Jan 14, 2026
604aefb
simplify rollup; make bin script +x on postbuild
cacieprins Jan 14, 2026
969a34b
Merge branch 'develop' into refactor-cli-rollup
cacieprins Jan 15, 2026
98d9c45
cleanup
cacieprins Jan 15, 2026
7c79b6b
updates build docs
cacieprins Jan 20, 2026
cb4034c
fix ./bin export path
cacieprins Jan 20, 2026
86e4932
Merge branch 'develop' into refactor-cli-rollup
cacieprins Jan 20, 2026
f63f935
comment to clarify why relative path resolution changed in spawn.ts
cacieprins Jan 20, 2026
a14f399
make script start path (for dev mode) less brittle vis a vis build ar…
cacieprins Jan 20, 2026
32543ec
cleanup
cacieprins Jan 21, 2026
c34d13b
since we dont need monorepo packages yet, adding it here is premature
cacieprins Jan 21, 2026
64c80a6
externalize package.json to fix install script
cacieprins Jan 21, 2026
9805c44
Merge branch 'develop' into refactor-cli-rollup
cacieprins Jan 21, 2026
5c59239
add entrypoints to cli knip config
cacieprins Jan 22, 2026
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
2 changes: 1 addition & 1 deletion cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ angular
angular-zoneless
svelte
index.js
lib/**/*.js
lib/**/*.js
65 changes: 64 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,70 @@ The CLI is used to build the [cypress npm module](https://www.npmjs.com/package/

## Building

See `scripts/build.js`. Note that the built npm package will include [NPM_README.md](NPM_README.md) as its public README file.
For ease of development, `yarn build` will clean and prepare the artifact directories, build the package with rollup, and post-process the build. `yarn build-cli` is used in CI to prevent the clean step from wiping out work from prior steps.

The built npm package includes [NPM_README.md](NPM_README.md) as its public README file, rather than this readme file.

### Build Hooks

When you run `yarn build`, the following hooks execute in order:

#### `prebuild`
Runs before the main build step:
- **Cleans previous build artifacts**: Removes `build/`, `dist/`, and type definition directories
- **Runs postinstall**: Applies patch-package patches and syncs TypeScript type definitions from `@types/*` packages
- **Prepares build directory**: Creates `build/` folder and copies static files (README, .release.json, bin, types)

#### `build`
The main build step:
- **Runs Rollup**: Compiles TypeScript source files from `lib/` to JavaScript in `dist/`
- Creates both CommonJS (`.js`) and ESM (`.mjs`) outputs
- Preserves directory structure for internal entrypoint modules
- Bundles `tslib` but externalizes other `node_modules` dependencies

#### `postbuild`
Runs after the main build step:
- **Makes binary executable**: Sets execute permissions on `dist/bin/cypress`
- **Copies to build directory**: Copies `dist/` contents to `build/`
- **Prepares package.json**: Generates npm-ready `package.json` in `build/` with:
- Version and metadata from root package
- Build info (commit branch, SHA, date)
- Removed devDependencies and internal-only fields
- **Bundles component testing frameworks**: Copies mount-utils, react, vue, angular, angular-zoneless, and svelte packages to `build/`

**Note**: `yarn build-cli` explicitly includes `postbuild` in its command, but `yarn build` relies on npm's automatic lifecycle hooks. Both produce the same result.

### Why `dist/` and `build/`?

The build process uses two separate directories:

- **`dist/`**: Raw build output from Rollup. This is the compiled JavaScript that:
- Is referenced by the source `package.json` (`"main": "dist/index.js"`) for development
- May be imported directly by other packages in the monorepo during development
- Contains only the compiled code, no static files or npm packaging artifacts

- **`build/`**: Complete npm package artifact. This directory:
- Contains static files added in `prebuild` (README, .release.json, bin, types)
- Contains the compiled code copied from `dist/` in `postbuild`
- Contains component testing frameworks bundled in `postbuild`
- Contains a prepared `package.json` for npm release (with version, build info, etc.)

We can't build directly to `build/` because `prebuild` adds static files there first. The separation ensures:
1. `dist/` remains available for development imports
2. `build/` contains the complete, ready-to-publish npm package
3. The build process doesn't overwrite static files added in `prebuild`

### Build Configuration

**`rollup.config.mjs`**: Defines two Rollup builds:
- **CommonJS build**: Compiles multiple entry points (`lib/index.ts`, `lib/cli.ts`, `lib/cypress.ts`, `lib/exec/xvfb.ts`, `lib/exec/spawn.ts`, `lib/bin/cypress.ts`) to `dist/` with preserved directory structure. Bundles `tslib` and `@packages/*` monorepo packages, but externalizes other `node_modules` dependencies. `tslib` is bundled to preserve Typescript polyfill helpers, and may eventually be removed as a bundled package.
- **ESM build**: Compiles single entry point (`lib/index.mts`) to `dist/index.mjs`. This is a thin wrapper that uses Node's `module.createRequire()` to dynamically require the CJS build output (`./cypress`), then re-exports its API. While `external: false` is set, there are no npm dependencies to bundle, as only Node's built-in `module` package is imported.

**`tsconfig.json`**: Base TypeScript configuration for eslint and type checking (ES2022 target, CommonJS module, strict mode, no emit).

**`tsconfig.build.json`**: Extends base config for CJS build (ES2016 target, enables emit, rootDir: `lib`, outDir: `dist`).

**`tsconfig.esm.json`**: Extends build config for ESM build (ES2022 target, ES2022 module, includes only `lib/**/*.mts` files).

## Testing

Expand Down
4 changes: 3 additions & 1 deletion cli/lib/bin/cypress.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#!/usr/bin/env node
// declared here in order to avoid consumers who are looking for the binary to be available relative to the dist directory
import '../../bin/cypress'
import CLI from '../cli'

CLI.init()
144 changes: 70 additions & 74 deletions cli/lib/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,99 +4,95 @@ import fs from 'fs-extra'
import openModule from './exec/open'
import runModule from './exec/run'
import util from './util'
import cli from './cli'
import cliImport from './cli'

const cypressModuleApi = {
/**
* Opens Cypress GUI
* @see https://on.cypress.io/module-api#cypress-open
*/
open (options: any = {}): any {
options = util.normalizeModuleOptions(options)
export function open (options: any = {}): any {
options = util.normalizeModuleOptions(options)

return openModule.start(options)
},
return openModule.start(options)
}

/**
* Runs Cypress tests in the current project
* @see https://on.cypress.io/module-api#cypress-run
*/
async run (options: any = {}): Promise<any> {
if (!runModule.isValidProject(options.project)) {
throw new Error(`Invalid project path parameter: ${options.project}`)
}
/**
* Runs Cypress tests in the current project
* @see https://on.cypress.io/module-api#cypress-run
*/
export async function run (options: any = {}): Promise<any> {
if (!runModule.isValidProject(options.project)) {
throw new Error(`Invalid project path parameter: ${options.project}`)
}

options = util.normalizeModuleOptions(options)
tmp.setGracefulCleanup()
options = util.normalizeModuleOptions(options)
tmp.setGracefulCleanup()

const outputPath: string = tmp.fileSync().name
const outputPath: string = tmp.fileSync().name

options.outputPath = outputPath
options.outputPath = outputPath

const failedTests = await runModule.start(options)
const output = await fs.readJson(outputPath, { throws: false })
const failedTests = await runModule.start(options)
const output = await fs.readJson(outputPath, { throws: false })

if (!output) {
return {
status: 'failed',
failures: failedTests,
message: 'Could not find Cypress test run results',
}
if (!output) {
return {
status: 'failed',
failures: failedTests,
message: 'Could not find Cypress test run results',
}
}

return output
},

cli: {
/**
* Parses CLI arguments into an object that you can pass to "cypress.run"
* @example
* const cypress = require('cypress')
* const cli = ['cypress', 'run', '--browser', 'firefox']
* const options = await cypress.cli.parseRunArguments(cli)
* // options is {browser: 'firefox'}
* await cypress.run(options)
* @see https://on.cypress.io/module-api
*/
parseRunArguments (args: string[]): any {
return cli.parseRunCommand(args)
},
},
return output
}

export const cli = {
/**
* Provides automatic code completion for configuration in many popular code editors.
* While it's not strictly necessary for Cypress to parse your configuration, we
* recommend wrapping your config object with `defineConfig()`
* Parses CLI arguments into an object that you can pass to "cypress.run"
* @example
* module.exports = defineConfig({
* viewportWith: 400
* })
*
* @see ../types/cypress-npm-api.d.ts
* @param {Cypress.ConfigOptions} config
* @returns {Cypress.ConfigOptions} the configuration passed in parameter
* const cypress = require('cypress')
* const cli = ['cypress', 'run', '--browser', 'firefox']
* const options = await cypress.cli.parseRunArguments(cli)
* // options is {browser: 'firefox'}
* await cypress.run(options)
* @see https://on.cypress.io/module-api
*/
defineConfig (config: any): any {
return config
parseRunArguments (args: string[]): any {
return cliImport.parseRunCommand(args)
},
}

/**
* Provides automatic code completion for Component Frameworks Definitions.
* While it's not strictly necessary for Cypress to parse your configuration, we
* recommend wrapping your Component Framework Definition object with `defineComponentFramework()`
* @example
* module.exports = defineComponentFramework({
* type: 'cypress-ct-solid-js'
* // ...
* })
*
* @see ../types/cypress-npm-api.d.ts
* @param {Cypress.ThirdPartyComponentFrameworkDefinition} config
* @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter
*/
defineComponentFramework (config: any): any {
return config
},
/**
* Provides automatic code completion for configuration in many popular code editors.
* While it's not strictly necessary for Cypress to parse your configuration, we
* recommend wrapping your config object with `defineConfig()`
* @example
* module.exports = defineConfig({
* viewportWith: 400
* })
*
* @see ../types/cypress-npm-api.d.ts
* @param {Cypress.ConfigOptions} config
* @returns {Cypress.ConfigOptions} the configuration passed in parameter
*/
export function defineConfig (config: any): any {
return config
}

export = cypressModuleApi
/**
* Provides automatic code completion for Component Frameworks Definitions.
* While it's not strictly necessary for Cypress to parse your configuration, we
* recommend wrapping your Component Framework Definition object with `defineComponentFramework()`
* @example
* module.exports = defineComponentFramework({
* type: 'cypress-ct-solid-js'
* // ...
* })
*
* @see ../types/cypress-npm-api.d.ts
* @param {Cypress.ThirdPartyComponentFrameworkDefinition} config
* @returns {Cypress.ThirdPartyComponentFrameworkDefinition} the configuration passed in parameter
*/
export function defineComponentFramework (config: any): any {
return config
}
11 changes: 7 additions & 4 deletions cli/lib/exec/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { needsSandbox } from '../tasks/verify'
import { throwFormErrorText, getError, errors } from '../errors'
import readline from 'readline'
import { stdin, stdout, stderr } from 'process'

import { relativeToRepoRoot } from '../relative-to-repo-root'
const debug = Debug('cypress:cli')

const DBUS_ERROR_PATTERN = /ERROR:dbus\/(bus|object_proxy)\.cc/
Expand Down Expand Up @@ -73,9 +73,12 @@ function createSpawnFunction (
executable = 'node'
// if we're in dev then reset
// the launch cmd to be 'npm run dev'
startScriptPath = path.resolve(__dirname, '..', '..', '..', 'scripts', 'start.js')

debug('in dev mode the args became %o', args)
// This path is correct in the build output, but not the source code. This file gets bundled into
// `dist/spawn-<hash>.js`, which makes this resolution appear incorrect at first glance.
startScriptPath = relativeToRepoRoot('scripts/start.js')
if (!startScriptPath) {
throw new Error(`Cypress start script (scripts/start.js) not found in parent directory of ${__dirname}`)
}
}

if (!options.dev && needsSandbox()) {
Expand Down
Loading
Loading