Betterer

Incremental Improvement

September 2021


September 2021

Hi, I'm Craig!

September 2021

Legacy


September 2021
Local Maxima

Desired state
Current state

September 2021

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


September 2021
Revolution...

September 2021

Branching!


September 2021
Branching...

September 2021
// 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
Automation






September 2021
Humans

Not obsolete yet!

September 2021
Buttons

September 2021
<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>



September 2021
Inspiration

Evolutionary Architecture


September 2021
Evolutionary Algorithms


September 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

September 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

September 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

September 2021
Tetris

September 2021

September 2021
Evolutionary Migration?

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

September 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); }
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); }
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); }
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); }
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); }

September 2021
Code as data





September 2021
ts(codebase) => nCompilerErrors
eslint(codebase) => nLintErrors
axe(codebase) => nA11yErrors
jest(codebase) => coverage%
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

September 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\`] = {
...
}
`

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

September 2021
Betterer

Incremental Improvement

September 2021

September 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", // ... } }
// 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", // ... } }
// 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

September 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:

September 2021

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

September 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, // ... }) };

September 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; } }) };

September 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 }) };

September 2021

September 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!

September 2021

September 2021
Workflows
Add to CI pipeline
Add to pre-commit hooks for changed files
Take their changes when merging

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


September 2021



September 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 }) };

September 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') };
// .betterer.ts import { BettererFileTest } from '@betterer/betterer'; export default { 'migrate JS to TS': () => new BettererFileTest(async (filePaths, fileTestResult) => { // ... // }).include('**/*.js') };

September 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') };
// .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') };
// .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') };
// .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') };
// .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') };
// .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') };

September 2021

September 2021

September 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: }` };


September 2021

September 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: }` };

September 2021

September 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

September 2021

September 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: }` };

September 2021
More tests!









September 2021

September 2021
// axe.ts export function axe () { }
Axe Test

September 2021
// axe.ts import { BettererTest } from "@betterer/betterer"; export function axe () { return new BettererTest({ // ... }); }
Axe Test

September 2021
// axe.ts import { BettererTest } from "@betterer/betterer"; import { AxePuppeteer } from "axe-puppeteer"; import puppeteer from "puppeteer"; export function axe (uri: string) { return new BettererTest({ async test() { // ... }, }); }
Axe Test

September 2021
// axe.ts import { BettererTest } from "@betterer/betterer"; import { AxePuppeteer } from "axe-puppeteer"; import puppeteer from "puppeteer"; export function axe (uri: string) { return new BettererTest({ async test() { const browser = await puppeteer.launch(); const [page] = await browser.pages(); await page.goto(uri); const results = await new AxePuppeteer(page).analyze(); await page.close(); await browser.close(); return results.violations.length; }, }); }
// axe.ts import { BettererTest } from "@betterer/betterer"; import { AxePuppeteer } from "axe-puppeteer"; import puppeteer from "puppeteer"; export function axe (uri: string) { return new BettererTest({ async test() { const browser = await puppeteer.launch(); const [page] = await browser.pages(); await page.goto(uri); const results = await new AxePuppeteer(page).analyze(); await page.close(); await browser.close(); return results.violations.length; }, }); }
Axe Test

September 2021
// axe.ts import { BettererTest } from "@betterer/betterer"; import { smaller } from "@betterer/constraints"; import { AxePuppeteer } from "axe-puppeteer"; import puppeteer from "puppeteer"; export function axe (uri: string) { return new BettererTest({ async test() { // ... return results.violations.length; }, constraint: smaller, }); }
Axe Test

September 2021
// .betterer.ts
import { axe } from './axe';
export default {
'documentation homepage a11y': () =>
axe('https://phenomnomnominal.github.io/betterer'),
'documentation api a11y': () =>
axe('https://phenomnomnominal.github.io/betterer/docs/api')
};
Axe Test

September 2021
Axe Test
// BETTERER RESULTS V2.
exports[`documentation homepage a11y`] = {
value: `7`
};
exports[`documentation api a11y`] = {
value: `12`
};

September 2021
Axe Test
// axe.ts import { BettererTest } from "@betterer/betterer"; import { AxePuppeteer } from "axe-puppeteer"; import puppeteer from "puppeteer"; export function axe (uri: string) { return new BettererTest<BettererAxeResult>({ async test() { // ... return new BettererAxeResult(results.violations); } }); }

September 2021
Axe Test
// axe-result.ts type BettererAxeSelectors = Array<string>; type BettererAxeViolations = Record<string, Array<string>>; export class BettererAxeResult { public violations: BettererAxeViolations; constructor (violations) { this.violations = this._process(violations); } private _process (): BettererAxeViolations { // ... } }
// axe-result.ts type BettererAxeSelectors = Array<string>; type BettererAxeViolations = Record<string, Array<string>>; export class BettererAxeResult { public violations: BettererAxeViolations; constructor (violations) { this.violations = this._process(violations); } private _process (): BettererAxeViolations { // ... } }
// axe-result.ts type BettererAxeSelectors = Array<string>; type BettererAxeViolations = Record<string, Array<string>>; export class BettererAxeResult { public violations: BettererAxeViolations; constructor (violations) { this.violations = this._process(violations); } private _process (): BettererAxeViolations { // ... } }

September 2021
Axe Test
// axe.ts import { BettererConstraintResult } from '@betterer/constraints'; function constraint( expected: BettererAxeResult, result: BettererAxeResult ): BettererConstraintResult { const diff = differ(expected, result); if (diff.new.length) { return BettererConstraintResult.worse; } if (diff.fixed.length) { return BettererConstraintResult.better; } return BettererConstraintResult.same; }
// axe.ts import { BettererConstraintResult } from '@betterer/constraints'; function constraint( expected: BettererAxeResult, result: BettererAxeResult ): BettererConstraintResult { const diff = differ(expected, result); if (diff.new.length) { return BettererConstraintResult.worse; } if (diff.fixed.length) { return BettererConstraintResult.better; } return BettererConstraintResult.same; }
// axe.ts import { BettererConstraintResult } from '@betterer/constraints'; function constraint( expected: BettererAxeResult, result: BettererAxeResult ): BettererConstraintResult { const diff = differ(expected, result); if (diff.new.length) { return BettererConstraintResult.worse; } if (diff.fixed.length) { return BettererConstraintResult.better; } return BettererConstraintResult.same; }

September 2021
Axe Test
// axe.ts import { BettererTest } from "@betterer/betterer"; import { AxePuppeteer } from "axe-puppeteer"; import puppeteer from "puppeteer"; export function axe (uri: string) { return new BettererTest<BettererAxeResult>({ async test() { // ... return new BettererAxeResult(results.violations); }, constraint, serialiser: { deserialise, serialise } }); }

September 2021
Axe Test
function serialise (deserialised: BettererAxeResult): BettererAxeViolations { return deserialised.violations; } function deserialised (serialised: BettererAxeViolations): BettererAxeResult { return BettererAxeResult.from(serialised); }

September 2021
Axe Test
// BETTERER RESULTS V2.
exports[`documentation homepage a11y`] = {
value: {
"All page content must be contained by landmarks": [
"#__docusaurus > div:nth-child(2)",
".hero"
],
// ... More results
}
};
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';

September 2021
Betterer

Help?!

September 2021
Questions?


September 2021
Betterer Incremental Improvement @phenomnominal September 2021
Betterer: Incremental Improvement - Code Europe
By Craig Spence
Betterer: Incremental Improvement - Code Europe
- 9,122