Skip to content

Commit

Permalink
refactor(lib): replace fp-ts with @mobily/ts-belt
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMason committed Feb 9, 2023
1 parent c72fdd3 commit 5a226fa
Show file tree
Hide file tree
Showing 29 changed files with 565 additions and 217 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
"Tom Fletcher (https://github.com/tom-fletcher)"
],
"dependencies": {
"@mobily/ts-belt": "3.13.1",
"chalk": "4.1.2",
"commander": "10.0.0",
"cosmiconfig": "8.0.0",
"expect-more": "1.3.0",
"fp-ts": "2.13.1",
"fs-extra": "11.1.0",
"glob": "8.1.0",
"minimatch": "6.1.6",
Expand Down
2 changes: 1 addition & 1 deletion src/bin-list-mismatches/list-mismatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function listMismatches(ctx: Context): Context {
invalidGroups.forEach((instanceGroup) => {
const name = instanceGroup.name;
const workspaceInstance = instanceGroup.getWorkspaceInstance();
const expected = instanceGroup.getExpectedVersion() || 'nice b';
const expected = instanceGroup.getExpectedVersion() || '';
const isBanned = instanceGroup.versionGroup.isBanned;
const isUnpinned = instanceGroup.isUnpinned;

Expand Down
2 changes: 1 addition & 1 deletion src/bin-set-semver-ranges/set-semver-ranges-cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe('setSemverRanges', () => {
jest.restoreAllMocks();
});

it('sets all versions to use the supplied range', () => {
it.only('sets all versions to use the supplied range', () => {
const scenario = scenarios.semverRangesDoNotMatchConfig();
setSemverRangesCli(scenario.config, scenario.disk);
expect(scenario.disk.writeFileSync.mock.calls).toEqual([
Expand Down
16 changes: 13 additions & 3 deletions src/lib/disk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@ import { CWD } from '../constants';
import type { Config } from './get-context/get-config/config';
import { verbose } from './log';

export type Disk = typeof disk;
export type Disk = {
process: {
exit: (code: number) => void;
};
globSync: (pattern: string) => string[];
readConfigFileSync: (configPath?: string) => Partial<Config.RcFile>;
readFileSync: (filePath: string) => string;
readYamlFileSync: <T = unknown>(filePath: string) => T;
removeSync: (filePath: string) => void;
writeFileSync: (filePath: string, contents: string) => void;
};

const client = cosmiconfigSync('syncpack');

export const disk = {
export const disk: Disk = {
process: {
exit(code: number): void {
verbose('exit(', code, ')');
Expand Down Expand Up @@ -70,4 +80,4 @@ export const disk = {
verbose('writeFileSync(', filePath, contents, ')');
writeFileSync(filePath, contents);
},
} as const;
};
29 changes: 29 additions & 0 deletions src/lib/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export class BaseError extends Error {
name: 'SyncpackError';
cause?: BaseError | Error | null;

constructor(
message: string,
options?: {
cause?: unknown;
props?: { args: any[] };
},
) {
super(message);
this.name = 'SyncpackError';
this.cause = BaseError.normalize(options?.cause);
}

static normalize(value: unknown): BaseError | Error | null {
if (value instanceof BaseError) return value;
if (value instanceof Error) return value;
if (typeof value === 'string') return new Error(value);
return null;
}

static map(message: string) {
return (cause: unknown): BaseError => {
return new BaseError(message, { cause });
};
}
}
25 changes: 25 additions & 0 deletions src/lib/get-context/$R.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { R } from '@mobily/ts-belt';
import { BaseError } from '../error';

/** Additional helpers for https://mobily.github.io/ts-belt/api/result */
export const $R = {
/**
* Return an R.Ok<output[]> for every R.Result which succeeded, or an
* R.Error<BaseError> if none succeeded.
*/
onlyOk<Input, Output = Input>(
getResult: (value: Input) => R.Result<Output, BaseError>,
) {
return (inputs: Input[]): R.Result<Output[], BaseError> => {
const outputs: Output[] = [];
for (const value of inputs) {
const result = getResult(value);
if (R.isError(result)) continue;
outputs.push(R.getExn(result));
}
return outputs.length > 0
? (R.Ok<Output[]>(outputs) as R.Result<Output[], BaseError>)
: R.Error(new BaseError('No R.Ok() returned by $R.onlyOk'));
};
},
};
2 changes: 1 addition & 1 deletion src/lib/get-context/get-context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('getContext', () => {
workspace: 'workspace',
};

it('includes all if none are set', () => {
it.only('includes all if none are set', () => {
expect(getContext({}, disk)).toHaveProperty(
'dependencyTypes',
expect.toBeArrayIncludingOnly(allTypes),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/get-context/get-groups/get-semver-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function getSemverGroups(
if (!semverGroup.instancesByName[name]) {
semverGroup.instancesByName[name] = [];
}
semverGroup.instancesByName[name].push(instance);
semverGroup.instancesByName[name]?.push(instance);
semverGroup.instances.push(instance);
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/get-context/get-groups/get-version-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function getVersionGroups(
if (!versionGroup.instancesByName[name]) {
versionGroup.instancesByName[name] = [];
}
versionGroup.instancesByName[name].push(instance);
versionGroup.instancesByName[name]?.push(instance);
versionGroup.instances.push(instance);
return;
}
Expand Down
30 changes: 30 additions & 0 deletions src/lib/get-context/get-package-json-files/get-file-paths.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { R } from '@mobily/ts-belt';
import { mockDisk } from '../../../../test/mock-disk';
import { BaseError } from '../../error';
import { getConfig } from '../get-config';
import { getFilePaths } from './get-file-paths';

it('returns R.Error when patterns return no files', () => {
const disk = mockDisk();
const program = getConfig(disk, {});
disk.globSync.mockReturnValue([]);
const message =
'No package.json files matched the patterns: "package.json", "packages/*/package.json"';
expect(getFilePaths(disk, program)).toEqual(R.Error(new BaseError(message)));
});

it('returns R.Ok when patterns return files', () => {
const disk = mockDisk();
const program = getConfig(disk, {});
const root = ['/fake/dir/package.json'];
const packages = [
'/fake/dir/packages/a/package.json',
'/fake/dir/packages/b/package.json',
];
disk.globSync.mockImplementation((pattern) => {
if (pattern === 'package.json') return root;
if (pattern === 'packages/*/package.json') return packages;
throw new Error('Unexpected pattern in test');
});
expect(getFilePaths(disk, program)).toEqual(R.Ok([...root, ...packages]));
});
71 changes: 35 additions & 36 deletions src/lib/get-context/get-package-json-files/get-file-paths.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { isArrayOfStrings } from 'expect-more';
import * as A from 'fp-ts/lib/Array';
import * as E from 'fp-ts/lib/Either';
import { flow, pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/lib/Option';
import * as S from 'fp-ts/lib/string';
import { A, flow, pipe, R } from '@mobily/ts-belt';
import { isArrayOfStrings, isEmptyArray } from 'expect-more';
import { $R } from '../$R';
import type { Disk } from '../../disk';
import { BaseError } from '../../error';
import type { Config } from '../get-config/config';
import { getPatterns } from './get-patterns';
import { tapOption } from './tap';
import { getErrorOrElse } from './try-catch';

type SafeFilePaths = R.Result<string[], BaseError>;

/**
* Using --source options and/or config files on disk from npm/pnpm/yarn/lerna,
Expand All @@ -20,38 +18,39 @@ import { getErrorOrElse } from './try-catch';
export function getFilePaths(
disk: Disk,
program: Config.RcFile,
): E.Either<Error, O.Option<string[]>> {
return pipe(
program,
getPatterns(disk),
O.getOrElse<string[]>(() => []),
E.traverseArray(resolvePattern),
E.map(removeReadonlyType),
E.map(flow(A.flatten, A.uniq(S.Eq))),
E.map(O.fromPredicate(isArrayOfStrings)),
E.map(tapOption<string[]>('package.json files found')),
);
): SafeFilePaths {
return pipe(program, getPatterns(disk), R.flatMap(resolvePatterns));

function resolvePattern(pattern: string): E.Either<Error, string[]> {
function resolvePatterns(patterns: string[]): SafeFilePaths {
const quoted = patterns.map((p) => `"${p}"`).join(', ');
const ERR_NO_MATCH = `No package.json files matched the patterns: ${quoted}`;
return pipe(
E.tryCatch(
() => disk.globSync(pattern),
getErrorOrElse(`npm package "glob" threw on pattern "${pattern}"`),
),
E.map(
flow(
O.fromPredicate(isArrayOfStrings),
tapOption(`files found matching pattern "${pattern}"`),
O.getOrElse<string[]>(() => []),
),
patterns,
$R.onlyOk<string, string[]>(resolvePattern),
R.mapError(BaseError.map(ERR_NO_MATCH)),
R.map(flow(A.flat, A.uniq, removeReadonlyType)),
);
}

function resolvePattern(pattern: string): SafeFilePaths {
const ERR_GLOB_MISS = `No package.json files match pattern "${pattern}"`;
const ERR_INVALID = `"glob" returned unexpected data on pattern "${pattern}"`;
const ERR_GLOB_THROW = `"glob" threw on pattern "${pattern}"`;
return pipe(
R.fromExecution(() => disk.globSync(pattern)),
R.mapError(BaseError.map(ERR_GLOB_THROW)),
R.flatMap((filePaths) =>
isEmptyArray(filePaths)
? R.Error(new BaseError(ERR_GLOB_MISS))
: isArrayOfStrings(filePaths)
? R.Ok(pipe(filePaths, A.flat, A.uniq, removeReadonlyType))
: R.Error(new BaseError(ERR_INVALID)),
),
);
}
}

/**
* Remove unwanted readonly type added by TaskEither.traverseArray
*/
function removeReadonlyType<T>(value: readonly T[]): T[] {
return value as T[];
/** Remove unwanted readonly type */
function removeReadonlyType<T>(value: readonly T[]): T[] {
return value as T[];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { R } from '@mobily/ts-belt';
import { mockDisk } from '../../../../../test/mock-disk';
import { BaseError } from '../../../error';
import { getLernaPatterns } from './get-lerna-patterns';

it('returns an R.Ok of strings when found', () => {
const disk = mockDisk();
disk.readFileSync.mockReturnValue(JSON.stringify({ packages: ['a', 'b'] }));
expect(getLernaPatterns(disk)()).toEqual(R.Ok(['a', 'b']));
});

it('returns an R.Error when disk throws', () => {
const disk = mockDisk();
const thrownError = new BaseError(
'Failed to read JSON file at /fake/dir/lerna.json',
);
disk.readFileSync.mockImplementation(() => {
throw thrownError;
});
expect(getLernaPatterns(disk)()).toEqual(R.Error(thrownError));
});

it('returns an R.Error when data is not valid JSON', () => {
const disk = mockDisk();
const thrownError = new BaseError(
'Failed to parse JSON file at /fake/dir/lerna.json',
);
disk.readFileSync.mockReturnValue('wut?');
expect(getLernaPatterns(disk)()).toEqual(R.Error(thrownError));
});

it('returns an R.Error when data is valid JSON but the wrong shape', () => {
const disk = mockDisk();
disk.readFileSync.mockReturnValue(JSON.stringify({ packages: [1, 2] }));
expect(getLernaPatterns(disk)()).toEqual(
R.Error(new BaseError('no lerna patterns found')),
);
});
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { isArrayOfStrings } from 'expect-more';
import * as E from 'fp-ts/lib/Either';
import { flow, pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/lib/Option';
import { O, pipe, R } from '@mobily/ts-belt';
import { join } from 'path';
import { CWD } from '../../../../constants';
import type { Disk } from '../../../disk';
import { props } from './props';
import { BaseError } from '../../../error';
import { getArrayOfStrings } from './lib/get-array-of-strings';
import { readJsonSafe } from './read-json-safe';

export function getLernaPatterns(disk: Disk): () => O.Option<string[]> {
return () =>
pipe(
readJsonSafe(disk)(join(CWD, 'lerna.json')),
E.map(flow(props('contents.packages'), O.filter(isArrayOfStrings))),
E.match(
(): O.Option<string[]> => O.none,
(value) => value,
export function getLernaPatterns(
disk: Disk,
): () => R.Result<string[], BaseError> {
const getPackages = getArrayOfStrings('packages');

return function getLernaPatterns() {
return pipe(
join(CWD, 'lerna.json'),
readJsonSafe(disk),
R.flatMap(({ contents }) =>
pipe(
getPackages(contents),
O.toResult(new BaseError('no lerna patterns found')),
),
),
);
};
}

0 comments on commit 5a226fa

Please sign in to comment.