Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(betterer 🔧): start migrating away from console to Ink #310

Merged
merged 2 commits into from
Oct 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"lint": "eslint \"./packages/**/src/**/*.{js,ts}\" \"./test/**/*.{js,ts}\" \"./*.{js,ts}\" --fix",
"format": "prettier \"**/!(*.snap)\" --write --loglevel=silent",
"test": "jest",
"test:api": "ts-node ./test/api-test.ts",
"test:api": "ts-node -P ./tsconfig.test.json ./test/api ",
"test:debug": "jest --runInBand --collectCoverage=false",
"test:extension": "lerna run test:extension",
"prepublishOnly": "yarn run build"
Expand All @@ -42,6 +42,7 @@
"@types/eslint": "^7.2.2",
"@types/jest": "^26.0.0",
"@types/node": "^14.6.1",
"@types/react": "^16.9.51",
"@typescript-eslint/eslint-plugin": "^4.1.1",
"@typescript-eslint/parser": "^4.1.1",
"ansi-regex": "^5.0.0",
Expand All @@ -51,9 +52,11 @@
"eslint-plugin-jest": "^24.0.1",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.3.0",
"ink": "^3.0.7",
"jest": "^26.1.0",
"lerna": "^3.22.1",
"npm-run-all": "^4.1.5",
"react": "^16.13.1",
"prettier": "^2.1.1",
"ts-api-guardian": "^0.5.0",
"ts-jest": "^26.3.0",
Expand Down
97 changes: 0 additions & 97 deletions test/api-test.ts

This file was deleted.

32 changes: 32 additions & 0 deletions test/api/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { FC, useEffect, useState } from 'react';
import { Box, render } from 'ink';

import { PackageAPITest, PackageAPITestProps } from './package-api-test';
import { getPackages, testPackage } from './test';

export const APITest: FC = function APITest() {
const [packageTestProps, setPackageTestProps] = useState<Array<PackageAPITestProps>>([]);

useEffect(() => {
(async () => {
const packageNames = await getPackages();
const packageTests = packageNames.map((packageName) => {
return {
name: packageName,
running: testPackage(packageName)
};
});
setPackageTestProps(packageTests);
})();
}, []);

return (
<Box flexDirection="column">
{packageTestProps.map(({ name, running }) => (
<PackageAPITest key={name} name={name} running={running} />
))}
</Box>
);
};

render(<APITest />);
66 changes: 66 additions & 0 deletions test/api/package-api-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { FC, useEffect, useState } from 'react';
import { Box, Text } from 'ink';
import { BettererPackageAPITestState } from './state';

export type PackageAPITestProps = {
name: string;
running: Promise<BettererPackageAPITestState>;
};

export const PackageAPITest: FC<PackageAPITestProps> = function PackageAPITest({ name, running }) {
const [[indicator, colour, message], setState] = useState([getIndicator(), getColour(), getMessage(name)]);

useEffect(() => {
(async () => {
const result = await running;
setState([getIndicator(result), getColour(result), getMessage(name, result)]);
})();
}, []);

return (
<Box flexDirection="row">
<Text>{indicator}</Text>
<Text color={colour}>{name}: </Text>
<Text>{message}</Text>
</Box>
);
};

function getMessage(packageName: string, state?: BettererPackageAPITestState): string {
if (state && state.valid) {
return `No Breaking API changes found in "@betterer/${packageName}".`;
}
if (state && state.exposedInternals) {
return `Found "${state.exposedInternals}" in the API for "@betterer/${packageName}. This means internal code has been exposed.`;
}
if (state && state.valid === false) {
return `API changes found in "@betterer/${packageName}" 🚨`;
}
return `Validating API for "@betterer/${packageName}" ...`;
}

function getIndicator(state?: BettererPackageAPITestState): string {
if (state && state.valid) {
return state.isDefinitelyValid ? '✅' : '🤷‍♂️';
}
if (state && state.exposedInternals) {
return '🔥';
}
if (state && state.valid === false) {
return '🚨';
}
return '🤔';
}

function getColour(state?: BettererPackageAPITestState): string {
if (state && state.valid) {
return state.isDefinitelyValid ? 'greenBright' : 'green';
}
if (state && state.exposedInternals) {
return 'redBright';
}
if (state && state.valid === false) {
return 'orangeBright';
}
return 'whiteBright';
}
18 changes: 18 additions & 0 deletions test/api/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function getState(state: Partial<BettererPackageAPITestState>): BettererPackageAPITestState {
return {
running: false,
valid: false,
exposedInternals: null,
isDefinitelyValid: false,
diff: null,
...state
};
}

export type BettererPackageAPITestState = {
running: boolean;
valid: boolean;
exposedInternals: string | null;
isDefinitelyValid: boolean;
diff: string | null;
};
76 changes: 76 additions & 0 deletions test/api/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { promises as fs } from 'fs';
import * as path from 'path';
import { publicApi, verifyAgainstGoldenFile } from 'ts-api-guardian';
import { BettererPackageAPITestState, getState } from './state';

const EXCLUDED_PACKAGES = ['extension'];
const DECLARATION_EXTENSION = '.d.ts';
const BUILT_DECLARATION = `dist/index${DECLARATION_EXTENSION}`;
const PACKAGES_DIR = path.resolve(__dirname, '../../packages');
const GOLDENS_DIR = path.resolve(__dirname, '../../goldens/api/@betterer');

const API_OPTIONS = { allowModuleIdentifiers: ['ts'] };

const CRLF = '\r\n';
const CHUNK_SPLIT = '\n\n';

const INTERNAL_TOKENS = ['Ω'];

export async function getPackages(): Promise<Array<string>> {
const items = await fs.readdir(PACKAGES_DIR);

const testDirectory = await Promise.all(
items.map(async (item) => {
const stat = await fs.lstat(path.join(PACKAGES_DIR, item));
return stat.isDirectory();
})
);

const packages = items.filter((_, index) => testDirectory[index]);
return packages.filter((packageName) => {
return packageName && !EXCLUDED_PACKAGES.includes(packageName);
});
}

export async function testPackage(packageName: string): Promise<BettererPackageAPITestState> {
const packageDeclarationPath = path.join(PACKAGES_DIR, packageName, BUILT_DECLARATION);
const packageGoldenPath = path.join(GOLDENS_DIR, `${packageName}${DECLARATION_EXTENSION}`);

const packageGoldenRaw = await fs.readFile(packageGoldenPath, 'utf-8');
const packageGeneratedRaw = publicApi(packageDeclarationPath, API_OPTIONS);
const packageGolden = normaliseFile(packageGoldenRaw);
const packageGenerated = normaliseFile(packageGeneratedRaw);

const foundToken = INTERNAL_TOKENS.find((token) => {
return checkForBannedTokens(packageGolden, token) || checkForBannedTokens(packageGenerated, token);
});

if (foundToken) {
return getState({ exposedInternals: foundToken });
}

const isDefinitelyValid = packageGolden === packageGenerated;
const isProbablyValid = isDefinitelyValid || checkForOutOfOrder(packageGenerated, packageGolden);
if (isProbablyValid) {
return getState({ valid: true, isDefinitelyValid });
}

const diff = verifyAgainstGoldenFile(packageDeclarationPath, packageGoldenPath, API_OPTIONS);
return getState({ valid: false, diff });
}

function checkForBannedTokens(types: string, token: string): boolean {
return types.includes(token);
}

function checkForOutOfOrder(generated: string, golden: string): boolean {
const generatedChunks = generated.split(CHUNK_SPLIT);
const goldenChunks = golden.split(CHUNK_SPLIT);
const newChunks = generatedChunks.filter((chunk) => !golden.includes(chunk));
const missingChunks = goldenChunks.filter((chunk) => !generated.includes(chunk));
return newChunks.length === 0 && missingChunks.length === 0;
}

function normaliseFile(str: string): string {
return str.replace(new RegExp(CRLF, 'g'), '\n').trim();
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"typeRoots": ["./node_modules/@types/"],
"resolveJsonModule": true,
"composite": true,
"incremental": true
"incremental": true,
"jsx": "react"
}
}
1 change: 1 addition & 0 deletions tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"jsx": "react",
"esModuleInterop": true
}
}
Loading