Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
12joan committed Jun 22, 2024
0 parents commit 0cc67dd
Show file tree
Hide file tree
Showing 25 changed files with 1,775 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'prettier',
],
rules: {
'prettier/prettier': [
1,
{
trailingComma: 'es5',
singleQuote: true,
},
],
},
};
12 changes: 12 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: On push

on: push

jobs:
ci:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- run: yarn install --frozen-lockfile
- run: yarn ci
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local
tsconfig.tsbuildinfo

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.yarn-bisect-DO-NOT-COMMIT
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) Joe Anderson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# yarn-bisect

Bisect an NPM package to locate the version where an error first occurred

## Installation

```bash
git clone https://github.com/12joan/yarn-bisect
cd yarn-bisect
npm install --global
# To uninstall:
# npm uninstall --global yarn-bisect
```

## Usage

For usage instructions, see `yarn-bisect help`.

## Example

In the following example, we use `yarn-bisect` to locate the first version `typescript` that fails to build our project.

```bash
$ yarn-bisect start typescript --no-filter-versions # Include dev versions
Preparing to bisect typescript

Good version: null (Specify this with `yarn-bisect good [version]`)
Bad version: null (Specify this with `yarn-bisect bad [version]`)

Currently testing version 5.5.2. Run `yarn-bisect reset` to stop bisecting.
$ yarn-bisect bad
Preparing to bisect typescript

Good version: null (Specify this with `yarn-bisect good [version]`)
Bad version: 5.5.2

Currently testing version 5.5.2. Run `yarn-bisect reset` to stop bisecting.
$ yarn-bisect good 5.4.5
Installing typescript@5.5.0-dev.20240414

Bisecting typescript

Good version: 5.4.5
Bad version: 5.5.2

103 version(s) left to check

Currently testing version 5.5.0-dev.20240414. Run `yarn-bisect reset` to stop bisecting.
$ yarn typecheck && yarn-bisect good || yarn-bisect bad
# After several iterations
$ yarn typecheck && yarn-bisect good || yarn-bisect bad
Finised bisecting typescript

Good version: 5.5.0-dev.20240304
Bad version: 5.5.0-dev.20240305

0 version(s) left to check

First bad version is 5.5.0-dev.20240305

Currently testing version 5.5.0-dev.20240304. Run `yarn-bisect reset` to stop bisecting.
$ yarn-bisect reset
Installing typescript@^5.5.2
```

## Compatibility

Tested with the following versions of Yarn:

- 1.22.19
- 4.1.1
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "yarn-bisect",
"version": "1.0.0",
"description": "Bisect an NPM package to locate the version where an error first occurred",
"repository": "https://github.com/12joan/yarn-bisect",
"author": "Joe Anderson",
"license": "MIT",
"bin": {
"yarn-bisect": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "ts-node src/index.ts",
"typecheck": "tsc --incremental --noEmit",
"typecheck:watch": "yarn typecheck --watch",
"lint": "eslint src --max-warnings 0",
"ci": "yarn typecheck && yarn lint"
},
"devDependencies": {
"@types/node": "^20.14.8",
"ts-node": "^10.9.2",
"typescript": "^5.5.2"
},
"dependencies": {
"commander": "^12.1.0",
"regex-escape": "^3.4.10",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"prettier": "^2.8.8"
}
}
35 changes: 35 additions & 0 deletions src/commands/goodBad.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { assertBisecting } from '../utils/assertBisecting';
import { state, setState } from '../utils/state';
import { validateVersion } from '../utils/validateVersion';
import { getCurrentVersion } from '../utils/getCurrentVersion';
import { statusCommand } from './status';
import { updateVersionsToCheck } from '../utils/updateVersionsToCheck';
import { installMidpointVersion } from '../utils/installMidpointVersion';

export const goodBadCommand =
(key: 'goodVersion' | 'badVersion') => async (versionOption?: string) => {
assertBisecting(state);
const { packageName, filterVersions } = state;

const version = versionOption ?? getCurrentVersion(packageName);
validateVersion(packageName, version, filterVersions);

setState((state) => ({
...state,
[key]: version,
}));

const hasGoodAndBad = state.goodVersion && state.badVersion;

if (hasGoodAndBad) {
await updateVersionsToCheck();

const installedVersion = await installMidpointVersion();

if (installedVersion) {
console.log('');
}
}

statusCommand();
};
16 changes: 16 additions & 0 deletions src/commands/reset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { installVersion } from '../utils/installVersion';
import { state, resetState } from '../utils/state';

export const resetCommand = async ({ install }: { install: boolean }) => {
if (state.status === 'null') {
console.log('Nothing to do');
return;
}

if (install) {
const { initialVersionSpec } = state;
await installVersion(initialVersionSpec);
}

resetState();
};
24 changes: 24 additions & 0 deletions src/commands/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getAllVersions } from '../utils/getAllVersions';
import { getCurrentVersionSpec } from '../utils/getCurrentVersionSpec';
import { setState } from '../utils/state';
import { statusCommand } from './status';

export const startCommand = async (
packageName: string,
{ filterVersions }: { filterVersions: boolean }
) => {
const versions = await getAllVersions(packageName, filterVersions);
const { versionSpec: initialVersionSpec, isDev } =
getCurrentVersionSpec(packageName);

setState({
status: 'bisecting',
packageName,
filterVersions,
isDev,
initialVersionSpec,
versionsToCheck: versions,
});

statusCommand();
};
55 changes: 55 additions & 0 deletions src/commands/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { getCurrentVersion } from '../utils/getCurrentVersion';
import { state } from '../utils/state';

export const statusCommand = () => {
if (state.status === 'null') {
console.log(
'Not bisecting. Run `yarn-bisect start <package>` to get started.'
);
return;
}

const { packageName, versionsToCheck, goodVersion, badVersion } = state;
const hasGoodAndBad = goodVersion && badVersion;
const finished = hasGoodAndBad && versionsToCheck.length === 0;
const currentVersion = getCurrentVersion(packageName);

const lines: string[] = [];

const statusDescriptor = (() => {
if (finished) return 'Finised bisecting';
if (hasGoodAndBad) return 'Bisecting';
return 'Preparing to bisect';
})();

lines.push(`${statusDescriptor} ${packageName}`);

lines.push('');

[
['Good version', 'good', goodVersion],
['Bad version', 'bad', badVersion],
].forEach(([label, command, value]) => {
const valueDescription =
value ?? `null (Specify this with \`yarn-bisect ${command} [version]\`)`;
lines.push(`${label}: ${valueDescription}`);
});

lines.push('');

if (hasGoodAndBad) {
lines.push(`${versionsToCheck.length} version(s) left to check`);
lines.push('');
}

if (finished) {
lines.push(`First bad version is ${badVersion}`);
lines.push('');
}

lines.push(
`Currently testing version ${currentVersion}. Run \`yarn-bisect reset\` to stop bisecting.`
);

console.log(lines.join('\n'));
};
55 changes: 55 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env node

import { program } from 'commander';
import packageJson from '../package.json';
import { startCommand } from './commands/start';
import { statusCommand } from './commands/status';
import { resetCommand } from './commands/reset';
import { goodBadCommand } from './commands/goodBad';

program
.name(packageJson.name)
.description(packageJson.description)
.version(packageJson.version);

program
.command('status')
.description('Show the current state of the bisection')
.action(statusCommand);

program
.command('start')
.option(
'--no-filter-versions',
'Do not filter versions. By default, only versions matching the format x.y.z[.w] are included in the bisection.'
)
.argument('<package>', 'The package to bisect')
.description('Start bisecting a package')
.action(startCommand);

program
.command('good')
.argument('[version]', 'The version to mark as good')
.description(
'Mark a version as good. If no version is specified, uses the currently installed version.'
)
.action(goodBadCommand('goodVersion'));

program
.command('bad')
.argument('[version]', 'The version to mark as bad')
.description(
'Mark a version as bad. If no version is specified, uses the currently installed version.'
)
.action(goodBadCommand('badVersion'));

program
.command('reset')
.option(
'--no-install',
'Do not reinstall the version that was used prior to bisecting.'
)
.description('Reset the bisection')
.action(resetCommand);

program.parse();
3 changes: 3 additions & 0 deletions src/regex-escape.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'regex-escape' {
export default function regexEscape(input: string): string;
}
11 changes: 11 additions & 0 deletions src/utils/assertBisecting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { State, BisectingState } from './state';
import { fail } from './fail';

export function assertBisecting(state: State): asserts state is BisectingState {
if (state.status !== 'bisecting') {
fail(
'not currently bisecting',
'Run `yarn-bisect start <package>` to get started.'
);
}
}
Loading

0 comments on commit 0cc67dd

Please sign in to comment.