Betterer
Incremental Improvement
August 2021
August 2021
Hi, I'm Craig!
August 2021
August 2021
Legacy
August 2021
Local Maxima
Desired state
Current state
August 2021
Revolution!
August 2021
Branching!
August 2021
Automation
August 2021
Evolutionary Algorithms
August 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
August 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
August 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
August 2021
August 2021
Humans
Not obsolete yet!
August 2021
Evolutionary Architecture
August 2021
Evolutionary Migration?
A fitness function to evaluate the solution:
f(codebase) => score
August 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);
}
August 2021
Code as data
August 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
August 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\`] = {
...
}
`
August 2021
Evolutionary Migration
A fitness function to evaluate the solution
A comparison function to track progress
A little file to store the results
Betterer
Incremental Improvement
August 2021
Betterer
August 2021
August 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
August 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:
August 2021
August 2021
My First Test
// .betterer.ts
import { BettererTest } from '@betterer/betterer';
export default {
'migrate JS to TS': () => new BettererTest({
// ...
})
};
August 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,
// ...
})
};
August 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;
}
})
};
August 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
})
};
August 2021
August 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!
August 2021
August 2021
Workflows
Add to CI pipeline
Add to pre-commit hooks for changed files
Take their changes when merging
August 2021
// cool.js
export function ohBoyILoveJavaScript () {
console.log(`I'll never change!!!`);
return [1, 2, 3] + [4, 5, 6];
}
August 2021
August 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
})
};
August 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')
};
August 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')
};
August 2021
August 2021
August 2021
August 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:
}`
};
August 2021
August 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
August 2021
August 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:
}`
};
August 2021
More tests!
August 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';
Betterer
Help?!
August 2021
Thanks!
August 2021
Betterer: Incremental Improvement
By Craig Spence
Betterer: Incremental Improvement
- 2,876