Skip to content

Commit cf3ad94

Browse files
committed
fix(project): improve fault tolerance when reading package.json
1 parent cc76626 commit cf3ad94

File tree

6 files changed

+138
-63
lines changed

6 files changed

+138
-63
lines changed

cli/update.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@ import {exit} from "process";
22
import chain from "@softwareventures/chain";
33
import {forEachFn, mapFn} from "@softwareventures/array";
44
import {readProject} from "../project/read";
5-
import {bindFailureFn} from "../result/result";
6-
import {updateProject} from "../project/update";
5+
import {bindAsyncResultFn, bindFailureFn} from "../result/result";
6+
import {UpdateFailureReason, updateProject} from "../project/update";
7+
import {ReadJsonFailureReason} from "../project/read-json";
8+
import {Project} from "../project/project";
79

810
export function cliUpdate(path: string, options: object): void {
911
readProject(path)
10-
.then(updateProject)
12+
.then(bindAsyncResultFn<ReadJsonFailureReason, UpdateFailureReason, Project>(updateProject))
1113
.then(
1214
bindFailureFn(reasons => {
1315
chain(reasons)
1416
.map(
1517
mapFn(reason => {
1618
switch (reason.type) {
19+
case "file-not-found":
20+
return `File Not Found: ${reason.path}`;
1721
case "not-a-directory":
1822
return `Not a Directory: ${reason.path}`;
1923
case "file-exists":
2024
return `File Exists: ${reason.path}`;
25+
case "invalid-json":
26+
return `Invalid JSON: ${reason.path}`;
2127
case "git-not-clean":
2228
return `Git working copy not clean: ${reason.path}`;
2329
case "yarn-fix-failed":

prettier/fix.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import {resolve} from "path";
22
import {mapFn} from "@softwareventures/array";
3+
import {mapNullFn} from "@softwareventures/nullable";
34
import {ProjectSource} from "../project/project";
45
import {readProjectJson} from "../project/read-json";
5-
import {combineAsyncResults, mapFailureFn, Result, success} from "../result/result";
6+
import {
7+
combineAsyncResults,
8+
mapFailureFn,
9+
mapResultFn,
10+
Result,
11+
success,
12+
toNullable
13+
} from "../result/result";
614
import {yarn} from "../yarn/yarn";
715

816
export type PrettierFixResult = Result<PrettierFixFailureReason>;
@@ -43,17 +51,14 @@ export async function prettierFixFilesIfAvailable(
4351

4452
export async function isPrettierAvailable(project: ProjectSource): Promise<boolean> {
4553
return readProjectJson(project, "package.json")
46-
.catch(reason => {
47-
if (reason instanceof SyntaxError || reason.code === "ENOENT") {
48-
return false;
49-
} else {
50-
throw reason;
51-
}
52-
})
5354
.then(
54-
packageJson =>
55-
packageJsonDependsOnPrettier(packageJson) && yarnPrettierCanRun(packageJson)
56-
);
55+
mapResultFn(
56+
packageJson =>
57+
packageJsonDependsOnPrettier(packageJson) && yarnPrettierCanRun(packageJson)
58+
)
59+
)
60+
.then(toNullable)
61+
.then(mapNullFn(() => false));
5762
}
5863

5964
function packageJsonDependsOnPrettier(packageJson: any): boolean {

project/read-json.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,33 @@
1+
import {failure, Result, success} from "../result/result";
2+
import {FileNotFound} from "../fs-stage/file-not-found";
13
import {ProjectSource} from "./project";
24
import {readProjectText} from "./read-text";
35

4-
export async function readProjectJson(project: ProjectSource, path: string): Promise<any> {
5-
return readProjectText(project, path).then(JSON.parse);
6+
export type ReadJsonResult = Result<ReadJsonFailureReason, any>;
7+
8+
export type ReadJsonFailureReason = FileNotFound | InvalidJson;
9+
10+
export interface InvalidJson {
11+
readonly type: "invalid-json";
12+
readonly path: string;
13+
}
14+
15+
export async function readProjectJson(
16+
project: ProjectSource,
17+
path: string
18+
): Promise<ReadJsonResult> {
19+
return readProjectText(project, path)
20+
.then(JSON.parse)
21+
.then(
22+
json => success(json),
23+
reason => {
24+
if (reason.code === "ENOENT") {
25+
return failure([{type: "file-not-found", path}]);
26+
} else if (reason instanceof SyntaxError) {
27+
return failure([{type: "invalid-json", path}]);
28+
} else {
29+
throw reason;
30+
}
31+
}
32+
);
633
}

project/read.ts

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,28 @@ import {mapNullableFn, mapNullFn} from "@softwareventures/nullable";
55
import {gitHostFromUrl} from "../git/git-host";
66
import {createNodeVersions} from "../node/create";
77
import {parseAndCorrectSpdxExpression} from "../license/spdx/correct";
8+
import {allAsyncResults, mapResultFn, Result} from "../result/result";
89
import {statProjectFile} from "./stat-file";
910
import {Project} from "./project";
10-
import {readProjectJson} from "./read-json";
11+
import {ReadJsonFailureReason, readProjectJson} from "./read-json";
1112

12-
export async function readProject(path: string): Promise<Project> {
13+
export type ReadProjectResult = Result<ReadJsonFailureReason, Project>;
14+
15+
export async function readProject(path: string): Promise<ReadProjectResult> {
1316
path = resolve(path);
1417

1518
const project = {path};
1619

1720
const packageJson = readProjectJson(project, "package.json");
1821

1922
const npmPackage = packageJson
20-
.then(packageJson => packageJson.name ?? "")
21-
.then(name => /^(?:(@.*?)\/)?(.*)$/.exec(name) ?? ["", "", ""])
22-
.then(([_, scope, name]) => ({scope, name}));
23+
.then(mapResultFn(packageJson => packageJson?.name ?? ""))
24+
.then(mapResultFn(name => /^(?:(@.*?)\/)?(.*)$/.exec(name) ?? ["", "", ""]))
25+
.then(mapResultFn(([_, scope, name]) => ({scope, name})));
2326

24-
const gitHost = packageJson.then(packageJson => packageJson.repository).then(gitHostFromUrl);
27+
const gitHost = packageJson
28+
.then(mapResultFn(packageJson => packageJson?.repository))
29+
.then(mapResultFn(gitHostFromUrl));
2530

2631
const target = statProjectFile(project, "webpack.config.js")
2732
.catch(reason => {
@@ -33,36 +38,45 @@ export async function readProject(path: string): Promise<Project> {
3338
})
3439
.then(stats => (stats?.isFile() ? "webapp" : "npm"));
3540

36-
const author = packageJson.then(({author}) =>
37-
typeof author === "object"
38-
? {name: author.name, email: author.email}
39-
: typeof author === "string"
40-
? chain(/^\s*(.*?)(?:\s+<\s*(.*)\s*>)?\s*$/.exec(author) ?? []).map(
41-
([_, name, email]) => ({name, email})
42-
).value
43-
: {}
44-
);
41+
const author = packageJson
42+
.then(mapResultFn(packageJson => packageJson?.author))
43+
.then(
44+
mapResultFn(author =>
45+
typeof author === "object"
46+
? {name: author?.name, email: author?.email}
47+
: typeof author === "string"
48+
? chain(/^\s*(.*?)(?:\s+<\s*(.*)\s*>)?\s*$/.exec(author) ?? []).map(
49+
([_, name, email]) => ({name, email})
50+
).value
51+
: {}
52+
)
53+
);
4554

4655
const spdxLicense = packageJson
47-
.then(packageJson => packageJson.license)
48-
.then(mapNullableFn(parseAndCorrectSpdxExpression))
49-
.catch(() => undefined)
50-
.then(mapNullFn(() => undefined));
56+
.then(
57+
mapResultFn(packageJson =>
58+
typeof packageJson?.license === "string" ? packageJson?.license : null
59+
)
60+
)
61+
.then(mapResultFn(mapNullableFn(parseAndCorrectSpdxExpression)))
62+
.then(mapResultFn(mapNullFn(() => undefined)));
5163

5264
const today = todayUtc();
5365

54-
return Promise.all([npmPackage, gitHost, target, author, spdxLicense]).then(
55-
([npmPackage, gitHost, target, author, spdxLicense]) => ({
56-
path,
57-
npmPackage,
58-
gitHost,
59-
node: createNodeVersions(today),
60-
target,
61-
author,
62-
license: {
63-
spdxLicense,
64-
year: today.year
65-
}
66-
})
66+
return target.then(async target =>
67+
allAsyncResults([npmPackage, gitHost, author, spdxLicense]).then(
68+
mapResultFn(([npmPackage, gitHost, author, spdxLicense]) => ({
69+
path,
70+
npmPackage,
71+
gitHost,
72+
node: createNodeVersions(today),
73+
target,
74+
author,
75+
license: {
76+
spdxLicense,
77+
year: today.year
78+
}
79+
}))
80+
)
6781
);
6882
}

result/result.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,33 @@ export function chainAsyncResultsFn<TReason, TValue>(
220220
return async initial => chainAsyncResults(initial, actions);
221221
}
222222

223+
type InferReasons<T> = T extends ReadonlyArray<Result<infer Reasons, unknown>> ? Reasons : never;
224+
type InferValue<T> = T extends Success<infer Value> ? Value : never;
225+
226+
export function allResults<T extends Array<Result<unknown, unknown>>>(
227+
results: readonly [...T]
228+
): Result<InferReasons<T>, {[K in keyof T]: InferValue<T[K]>}> {
229+
return fold(
230+
results,
231+
(acc, result) =>
232+
bindResult(acc, accValues => mapResult(result, value => [...accValues, value])),
233+
success([]) as Result<any, any[]>
234+
) as any;
235+
}
236+
237+
type InferAwaited<T> = T extends Promise<infer Value> ? Value : T;
238+
type InferAwaiteds<T extends unknown[]> = {[K in keyof T]: InferAwaited<T[K]>};
239+
240+
export async function allAsyncResults<
241+
T extends Array<Promise<Result<unknown, unknown>> | Result<unknown, unknown>>
242+
>(
243+
results: readonly [...T]
244+
): Promise<
245+
Result<InferReasons<InferAwaiteds<T>>, {[K in keyof T]: InferValue<InferAwaited<T[K]>>}>
246+
> {
247+
return Promise.all(results).then(results => allResults(results) as any);
248+
}
249+
223250
export function toNullable<TValue>(result: Result<unknown, TValue>): TValue | null {
224251
return result.type === "success" ? result.value : null;
225252
}

yarn/fix.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {mapFailureFn, Result, success} from "../result/result";
1+
import {mapNullFn} from "@softwareventures/nullable";
2+
import {mapFailureFn, mapResultFn, Result, success, toNullable} from "../result/result";
23
import {ProjectSource} from "../project/project";
34
import {readProjectJson} from "../project/read-json";
45
import {yarn} from "./yarn";
@@ -24,19 +25,14 @@ export async function yarnFixIfAvailable(project: ProjectSource): Promise<YarnFi
2425

2526
export async function isYarnFixAvailable(project: ProjectSource): Promise<boolean> {
2627
return readProjectJson(project, "package.json")
27-
.catch(reason => {
28-
if (reason instanceof SyntaxError) {
29-
return false;
30-
} else if (reason.code === "ENOENT") {
31-
return false;
32-
} else {
33-
throw reason;
34-
}
35-
})
3628
.then(
37-
packageJson =>
38-
typeof packageJson === "object" &&
39-
typeof packageJson?.scripts === "object" &&
40-
typeof packageJson?.scripts?.fix === "string"
41-
);
29+
mapResultFn(
30+
packageJson =>
31+
typeof packageJson === "object" &&
32+
typeof packageJson?.scripts === "object" &&
33+
typeof packageJson?.scripts?.fix === "string"
34+
)
35+
)
36+
.then(toNullable)
37+
.then(mapNullFn(() => false));
4238
}

0 commit comments

Comments
 (0)