Skip to content

Commit

Permalink
refactor(sources): resolve globs and files using fp-ts
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMason committed Oct 10, 2021
1 parent a74b3c4 commit f165c1d
Show file tree
Hide file tree
Showing 20 changed files with 326 additions and 96 deletions.
8 changes: 4 additions & 4 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ module.exports = {
coverageReporters: ['html', 'lcov'],
coverageThreshold: {
global: {
branches: 90,
functions: 82,
lines: 89,
statements: 84,
branches: 89,
functions: 89,
lines: 93,
statements: 92,
},
},
moduleFileExtensions: ['ts', 'tsx', 'js'],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"commander": "8.1.0",
"cosmiconfig": "7.0.0",
"expect-more": "1.1.0",
"fp-ts": "2.11.2",
"fs-extra": "10.0.0",
"glob": "7.1.7",
"read-yaml-file": "2.1.0",
Expand Down
4 changes: 2 additions & 2 deletions src/commands/fix-mismatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chalk from 'chalk';
import { writeFileSync } from 'fs-extra';
import { EOL } from 'os';
import { relative } from 'path';
import { SyncpackConfig } from '../constants';
import { CWD, SyncpackConfig } from '../constants';
import { getHighestVersion } from './lib/get-highest-version';
import { getWrappers, SourceWrapper } from './lib/get-wrappers';
import { getMismatchedDependencies } from './lib/installations/get-mismatched-dependencies';
Expand Down Expand Up @@ -43,7 +43,7 @@ export const fixMismatchesToDisk = (options: Options): void => {
fixMismatches(wrappers, options);

wrappers.forEach((wrapper, i) => {
const shortPath = relative(process.cwd(), wrapper.filePath);
const shortPath = relative(CWD, wrapper.filePath);
const before = allBefore[i];
const after = toJson(wrapper);
if (before !== after) {
Expand Down
83 changes: 0 additions & 83 deletions src/commands/lib/get-wrappers.ts

This file was deleted.

43 changes: 43 additions & 0 deletions src/commands/lib/get-wrappers/get-file-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 { sync as globSync } from 'glob';
import { CWD } from '../../../constants';
import { getPatterns, Options } from './get-patterns';
import { removeReadonlyType } from './readonly';
import { tapNone } from './tap';
import { getErrorOrElse } from './try-catch';

type MaybeFilePaths = O.Option<string[]>;
type EitherMaybeFilePaths = E.Either<Error, MaybeFilePaths>;

/**
* Using --source options and/or config files on disk from npm/pnpm/yarn/lerna,
* return an array of absolute paths to every package.json file the user is
* working with.
*
* @returns Array of absolute file paths to package.json files
*/
export function getFilePaths(program: Options): EitherMaybeFilePaths {
return pipe(
getPatterns(program),
E.traverseArray(resolvePattern),
E.map(flow(removeReadonlyType, mergeArrayOfOptionsIntoOne, O.filter(isArrayOfStrings))),
);

function resolvePattern(pattern: string): EitherMaybeFilePaths {
return pipe(
E.tryCatch(
() => globSync(pattern, { absolute: true, cwd: CWD }),
getErrorOrElse(`npm package "glob" threw on pattern "${pattern}"`),
),
E.map(flow(O.of, O.filter(isArrayOfStrings), tapNone<string[]>(`found 0 files matching pattern "${pattern}"`))),
);
}

function mergeArrayOfOptionsIntoOne(options: MaybeFilePaths[]): MaybeFilePaths {
const unwrap = O.getOrElse<string[]>(() => []);
return O.of(options.reduce<string[]>((values, option) => values.concat(unwrap(option)), []));
}
}
20 changes: 20 additions & 0 deletions src/commands/lib/get-wrappers/get-patterns/get-lerna-patterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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 { join } from 'path';
import { MaybePatterns } from '.';
import { CWD } from '../../../../constants';
import { props } from './props';
import { readJsonSafe } from './read-json-safe';

export function getLernaPatterns(): MaybePatterns {
return pipe(
readJsonSafe(join(CWD, 'lerna.json')),
E.map(flow(props('contents.packages'), O.filter(isArrayOfStrings))),
E.match(
(): MaybePatterns => O.none,
(value) => value,
),
);
}
28 changes: 28 additions & 0 deletions src/commands/lib/get-wrappers/get-patterns/get-pnpm-patterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 { join } from 'path';
import type { MaybePatterns } from '.';
import { CWD } from '../../../../constants';
import { props } from './props';
import { readYamlSafe } from './read-yaml-safe';

interface PnpmWorkspace {
packages?: string[];
}

export function getPnpmPatterns(): MaybePatterns {
return pipe(
// packages:
// - "packages/**"
// - "components/**"
// - "!**/test/**"
readYamlSafe<PnpmWorkspace>(join(CWD, 'pnpm-workspace.yaml')),
E.map(flow(props('packages'), O.filter(isArrayOfStrings))),
E.match(
(): MaybePatterns => O.none,
(value) => value,
),
);
}
30 changes: 30 additions & 0 deletions src/commands/lib/get-wrappers/get-patterns/get-yarn-patterns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { isArrayOfStrings } from 'expect-more';
import * as E from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/lib/Option';
import { join } from 'path';
import type { MaybePatterns } from '.';
import { Source } from '..';
import { CWD } from '../../../../constants';
import { props } from './props';
import { readJsonSafe } from './read-json-safe';

export function getYarnPatterns(): MaybePatterns {
return pipe(
readJsonSafe(join(CWD, 'package.json')),
E.map((file) => pipe(findPackages(file.contents))),
O.fromEither,
O.flatten,
);

function findPackages(yarn: Source): MaybePatterns {
return pipe(
getArrayOfStrings('workspaces', yarn),
O.fold(() => getArrayOfStrings('workspaces.packages', yarn), O.some),
);
}

function getArrayOfStrings(paths: string, yarn: Source): MaybePatterns {
return pipe(yarn, props(paths), O.filter(isArrayOfStrings));
}
}
50 changes: 50 additions & 0 deletions src/commands/lib/get-wrappers/get-patterns/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { isArrayOfStrings } from 'expect-more';
import { flow, pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/lib/Option';
import { join } from 'path';
import { ALL_PATTERNS, CWD, SyncpackConfig } from '../../../../constants';
import { tapNone } from '../tap';
import { getLernaPatterns } from './get-lerna-patterns';
import { getPnpmPatterns } from './get-pnpm-patterns';
import { getYarnPatterns } from './get-yarn-patterns';

type Patterns = string[];

export type Options = Pick<SyncpackConfig, 'source'>;
export type MaybePatterns = O.Option<Patterns>;

/**
* Find every glob pattern which should be used to find package.json files for
* this monorepo.
*
* @returns `['./package.json', './packages/* /package.json']`
*/
export function getPatterns(program: Options): Patterns {
return pipe(
O.of(program.source),
O.filter(isArrayOfStrings),
tapNone<Patterns>('no --source patterns found'),
O.fold(
flow(
getYarnPatterns,
tapNone<Patterns>('no yarn workspaces found'),
O.fold(getPnpmPatterns, O.of),
tapNone<Patterns>('no pnpm workspaces found'),
O.fold(getLernaPatterns, O.of),
tapNone<Patterns>('no lerna packages found'),
O.map(flow(addRootDir, limitToPackageJson)),
),
O.of,
),
tapNone<Patterns>('no patterns found, using defaults'),
O.getOrElse(() => ALL_PATTERNS),
);

function addRootDir(patterns: Patterns): Patterns {
return [CWD, ...patterns];
}

function limitToPackageJson(patterns: Patterns): Patterns {
return patterns.map((pattern) => join(pattern, 'package.json'));
}
}
19 changes: 19 additions & 0 deletions src/commands/lib/get-wrappers/get-patterns/props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { pipe } from 'fp-ts/lib/function';
import * as O from 'fp-ts/lib/Option';
import * as C from 'fp-ts/lib/ReadonlyRecord';
import * as S from 'fp-ts/lib/State';

/**
* Safely read nested properties of any value.
* @param keys 'child.grandChild.greatGrandChild'
* @see https://gist.github.com/JamieMason/c0a3b21184cf8c43f76c77878c7c9198
*/
export function props(keys: string) {
return function getNestedProp(obj: unknown): O.Option<unknown> {
return pipe(
keys.split('.'),
S.traverseArray((key: string) => S.modify(O.chain(C.lookup(key) as never))),
S.execute(O.fromNullable<unknown>(obj)),
);
};
}
26 changes: 26 additions & 0 deletions src/commands/lib/get-wrappers/get-patterns/read-json-safe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { parse } from 'fp-ts/Json';
import * as E from 'fp-ts/lib/Either';
import { pipe } from 'fp-ts/lib/function';
import { readFileSync } from 'fs-extra';
import { SourceWrapper } from '..';
import { getErrorOrElse } from '../try-catch';

export function readJsonSafe(filePath: string): E.Either<Error | SyntaxError, SourceWrapper> {
return pipe(
readFileSafe(filePath),
E.chain((json) =>
pipe(
parse(json),
E.mapLeft(getErrorOrElse(`Failed to parse JSON file at ${filePath}`)),
E.map((contents) => ({ contents, filePath, json } as SourceWrapper)),
),
),
);
}

function readFileSafe(filePath: string): E.Either<Error, string> {
return E.tryCatch(
() => readFileSync(filePath, { encoding: 'utf8' }),
getErrorOrElse(`Failed to read JSON file at ${filePath}`),
);
}
7 changes: 7 additions & 0 deletions src/commands/lib/get-wrappers/get-patterns/read-yaml-safe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as E from 'fp-ts/lib/Either';
import { sync as readYamlSync } from 'read-yaml-file';
import { getErrorOrElse } from '../try-catch';

export function readYamlSafe<T = unknown>(filePath: string): E.Either<Error, T> {
return E.tryCatch(() => readYamlSync<T>(filePath), getErrorOrElse(`Failed to read YAML file at ${filePath}`));
}

0 comments on commit f165c1d

Please sign in to comment.