Betterer

Incremental Improvement
December 2021


December 2021


Hi, I'm Craig!

December 2021

Legacy

December 2021

Local Maxima

Desired state
Current state
December 2021

Why don't we rewrite the whole thing?!
December 2021

Revolution!

December 2021

Revolution...

December 2021

Branching!

September 2021
December 2021

Branching...
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
December 2021

Automation

December 2021

Humans

Not obsolete yet!
December 2021

Buttons
<button
class="button button--green">
Continue
</button>
<button
class="button button--red">
Cancel
</button>
<button
class="button button--success">
Continue
</button>
<button
class="button button--danger">
Cancel
</button>


December 2021

Inspiration

December 2021

Evolutionary Architecture

December 2021

Evolutionary Algorithms

December 2021

A genetic representation of the solution domain:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
---|
1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
---|
0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
---|
0
Chromosome
Population
Gene
Genetic Algorithms
December 2021

A genetic representation of the solution domain:
A fitness function to evaluate the solution:
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
---|
1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
---|
0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
---|
f(chromosome) => score
Genetic Algorithms
December 2021

Termination
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
---|
1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
---|
0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
---|
1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 |
---|
0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
---|
Crossover
1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 |
---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 |
---|
Offspring
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
---|
Mutation
Genetic Algorithms
December 2021

Tetris
December 2021

September 2021
December 2021

Evolutionary Migration?

A fitness function to evaluate the solution:
f(codebase) => score
December 2021

Code as data
import { BettererOptionsStart } from './config';
import { createGlobals } from './globals';
import { BettererRunner, BettererRunnerΩ } from './runner';
import { BettererSuiteSummary } from './suite';
// Run betterer in single-run mode:
export async function betterer(
options: BettererOptionsStart = {}
): Promise<BettererSuiteSummary> {
initDebug();
const globals = await createGlobals(options);
const runner = new BettererRunnerΩ(globals);
return runner.run(globals.config.filePaths);
}
December 2021

Code as data




ts(codebase) => nCompilerErrors
eslint(codebase) => nLintErrors
axe(codebase) => nA11yErrors
jest(codebase) => coverage%
December 2021

f(oldScore, newScore) => better | worse | same
A comparison function to track progress:
Better?
f(codebase) => oldScore
f(codebase) => newScore
Old state of the codebase
New state of the codebase
December 2021

Codebase as database?
It has become common to store extra information about a codebase in the repository:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-...
integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+...
dependencies:
"@babel/highlight" "^7.10.4"
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`betterer should not stay worse if an update is forced 1`] = `
"// BETTERER RESULTS V2.
exports[\`tsquery no raw console.log\`] = {
...
}
`
December 2021

Evolutionary Migration
A fitness function to evaluate the solution
A comparison function to track progress
A little file to store the results
December 2021

Betterer

Incremental Improvement
December 2021

December 2021

// package.json
{
"name": "@craig/my-very-stable-package",
"version": "20.2.1",
"author": "Craig Spence <craigspence0@gmail.com>",
"description": "Gee I wish this package was a little bit better.",
"scripts": {
"betterer": "betterer",
// ...
},
// ...
"devDependencies": {
"@betterer/cli": "^5.0.0",
// ...
}
}
Initialising
December 2021

// .betterer.ts
export default {
// Add tests here ☀️
};
Initialising
// .betterer.js
module.exports = {
// Add tests here ☀️
}
TypeScript by default:
JavaScript if you're into that:
December 2021

December 2021

My First Test
// .betterer.ts
import { BettererTest } from '@betterer/betterer';
export default {
'migrate JS to TS': () => new BettererTest({
// ...
})
};
December 2021

My First Test
// .betterer.ts
import { BettererTest } from '@betterer/betterer';
import glob from 'glob';
export default {
'migrate JS to TS': () => new BettererTest({
test: () => glob.sync('**/*.js').length,
// ...
})
};
December 2021

My First Test
// .betterer.ts
import { BettererTest } from '@betterer/betterer';
import { BettererConstraintResult } from '@betterer/constraints';
import glob from 'glob';
export default {
'migrate JS to TS': () => new BettererTest({
test: () => glob.sync('**/*.js').length,
constraint: (result: number, expected: number) => {
if (result < expected) {
return BettererConstraintResult.better;
}
if (result === expected) {
return BettererConstraintResult.same;
}
return BettererConstraintResult.worse;
}
})
};
December 2021

My First Test
// .betterer.ts
import { BettererTest } from '@betterer/betterer';
import { smaller } from '@betterer/constraints';
import glob from 'glob';
export default {
'migrate JS to TS': () => new BettererTest({
test: () => glob.sync('**/*.js').length,
constraint: smaller
})
};
December 2021

December 2021

My First Result
// .betterer.results
// // BETTERER RESULTS V2.
exports[`migrate JS to TS`] = {
value: `9`
};
This is basically the same things as a Jest snapshot file!
December 2021

December 2021

// cool.js
export function ohBoyILoveJavaScript () {
console.log(`I'll never change!!!`);
return [1, 2, 3] + [4, 5, 6];
}



December 2021

December 2021

My First File Test
// .betterer.ts
import { BettererTest } from '@betterer/betterer';
import { smaller } from '@betterer/constraints';
import glob from 'glob';
export default {
'migrate JS to TS': () => new BettererTest({
test: () => glob.sync('**/*.js').length,
constraint: smaller
})
};
December 2021

My First File Test
// .betterer.ts
import { BettererFileTest } from '@betterer/betterer';
export default {
'migrate JS to TS': () =>
new BettererFileTest(async (filePaths, fileTestResult) => {
// ...
//
}).include('**/*.js')
};
December 2021

My First File Test
// .betterer.ts
import { BettererFileTest } from '@betterer/betterer';
import { promises as fs } from 'fs';
export default {
'migrate JS to TS': () =>
new BettererFileTest(async (filePaths, fileTestResult) => {
await Promise.all(
filePaths.map(async (filePath) => {
const fileContents = await fs.readFile(filePath, 'utf8');
const file = fileTestResult.addFile(filePath, fileContents);
file.addIssue(0, 1, 'Please use TypeScript!');
})
);
}).include('**/**/*.js')
};
December 2021

December 2021

// BETTERER RESULTS V2.
exports[`migrate JS to TS`] = {
value: `{
"index.js:2978447364": [
[0, 0, 1, "Please use TypeScript!", "177600"]
],
"cool.js:3542870298": [
[0, 0, 1, "Please use TypeScript!", "287364"]
],
"amazing.js:8762519887": [
[0, 0, 1, "Please use TypeScript!", "726351"]
],
//...
// 9 total issues:
}`
};
December 2021


December 2021

December 2021

// BETTERER RESULTS V2.
exports[`migrate JS to TS`] = {
value: `{
"index.js:2978447364": [
[0, 0, 1, "Please use TypeScript!", "177600"]
],
"cool.js:3542870298": [
[0, 0, 1, "Please use TypeScript!", "287364"]
],
"amazing.js:8762519887": [
[0, 0, 1, "Please use TypeScript!", "726351"]
],
//...
// 10 total issues:
}`
};
December 2021

December 2021

// .betterer.ts
import { BettererFileTest } from '@betterer/betterer';
import { promises as fs } from 'fs';
export default {
'migrate JS to TS': () =>
new BettererFileTest(async (filePaths, fileTestResult) => {
await Promise.all(
filePaths.map(async (filePath) => {
const fileContents = await fs.readFile(filePath, 'utf8');
const file = fileTestResult.addFile(filePath, fileContents);
file.addIssue(0, 1, 'Please use TypeScript!');
})
);
})
.include('**/**/*.js')
.deadline('2021/12/31')
};
My First Deadline
December 2021

December 2021

// BETTERER RESULTS V2.
exports[`migrate JS to TS`] = {
value: `{
"index.js:2978447364": [
[0, 0, 1, "Please use TypeScript!", "177600"]
],
"boo.js:1827649983": [
[0, 0, 1, "Please use TypeScript!", "298736"]
],
"gross.js:1872646688": [
[0, 0, 1, "Please use TypeScript!", "009726"]
],
//...
// 8 total issues:
}`
};
December 2021

More tests!








December 2021

Built in tests!
// .betterer.ts
import { eslint } from '@betterer/eslint';
import { regexp } from '@betterer/regexp';
import { stylelint } from '@betterer/stylelint';
import { tsquery } from '@betterer/tsquery';
import { typescript } from '@betterer/typescript';
December 2021

Betterer

Help?!
December 2021

Thank you!

December 2021

Betterer: Incremental Improvement - React Next
By Craig Spence
Betterer: Incremental Improvement - React Next
- 2,969