Skip to content

Commit eb099e4

Browse files
committed
feat: allow to "modify" non object JSON files.
For example, if root type is **array** you can: `modifyJsonFile<number[]>("path.json", arr => [...arr, 5])`
1 parent f6c2f86 commit eb099e4

File tree

6 files changed

+337
-58
lines changed

6 files changed

+337
-58
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
],
2424
"scripts": {
2525
"prepublishOnly": "tsc && yarn test",
26-
"test": "ava --serial"
26+
"test": "tsd && ava --serial"
2727
},
2828
"engines": {
2929
"node": ">=12"
@@ -41,6 +41,7 @@
4141
"load-json-file": "^6.2.0",
4242
"nanoid": "^3.1.23",
4343
"ts-node": "^10.0.0",
44+
"tsd": "^0.17.0",
4445
"typescript": "^4.3.2"
4546
},
4647
"dependencies": {

readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ We're using [detect-indent](https://www.npmjs.com/package/detect-indent) to pres
6060

6161
## TODO
6262

63+
Docs:
64+
- [ ] Fix auto generated docs
65+
- [ ] Describe all possible usage cases
66+
- [ ] Give a hint, that it doesn't perform schema checking again actual file contents when type is passed into generic function `modifyJsonFile`
67+
68+
- [ ] Performance investigation (issues welcome)
6369
- [ ] Strip bom option
6470
- [ ] Fix double tsc compilation (and test actual build/)
6571
- [ ] transformer for paths (most likely babel plugin):

src/index.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PackageJson, TsConfigJson } from "type-fest";
1+
import { PackageJson, PartialDeep, TsConfigJson } from "type-fest";
22
import detectIndent from "detect-indent";
33
import stripBom from "strip-bom";
44
import parseJson from "parse-json";
@@ -31,27 +31,30 @@ type Options = PartialDeep<{
3131
* @default "preserve"
3232
* */
3333
tabSize: null | number | "preserve" | "hard";
34-
}>
34+
}>;
3535

36-
type GettersDeep<T extends object> = {
37-
[K in keyof T]: (oldValue: T[K], json: T) => T[K]
36+
type ModifyFields<T extends object> = {
37+
[K in keyof T]?: T[K] | ((oldValue: T[K], json: T) => T[K])
3838
//T[K] extends object ? ((oldValue: T[K]) => unknown)/* | GettersDeep<T[K]> */ : (oldValue: T[K]) => unknown
3939
};
4040

41-
export type ModifyJsonFileFunction<T extends object> = (
41+
type ModifyFunction<T> = (oldJson: T) => MaybePromise<T>;
42+
43+
export type ModifyJsonFileFunction<T> = (
4244
path: string,
43-
modifyFields: Partial<T | GettersDeep<T>> | ((oldJson: T) => MaybePromise<T>),
44-
options?: Options
45+
modifyFields: T extends object ? ModifyFields<T> | ModifyFunction<T> : ModifyFunction<T>,
46+
options?: Partial<Options>
4547
) => Promise<void>;
4648

47-
type ModifyJsonFileGenericFunction = <T extends object>(
49+
type ModifyJsonFileGenericFunction = <T extends any = object>(
4850
path: string,
49-
modifyFields: Partial<T | GettersDeep<T>> | ((oldJson: T) => MaybePromise<T>),
51+
modifyFields: T extends object ? ModifyFields<T> | ModifyFunction<T> : ModifyFunction<T>,
5052
options?: Partial<Options>
5153
) => Promise<void>;
5254

55+
type LoadJsonFileOptions = Required<Pick<Options, "encoding" | "tabSize">>;
5356
/** returns additional info, not only JSON */
54-
const loadJsonFile = async (filePath: string, { encoding, tabSize }: Pick<Options, "encoding" | "tabSize">) => {
57+
const loadJsonFile = async (filePath: string, { encoding, tabSize }: LoadJsonFileOptions) => {
5558
const contents = stripBom(
5659
await fs.promises.readFile(filePath, encoding)
5760
);
@@ -73,7 +76,7 @@ const loadJsonFile = async (filePath: string, { encoding, tabSize }: Pick<Option
7376
* modifies **original** JSON file
7477
* You can pass generic, that reflects the structure of original JSON file
7578
*
76-
* @param modifyFields Fields to merge or callback (can be async). If callback is passed, JSON fields won't be merged.
79+
* @param modifyFields If file contents is object, you can pass fields to merge or callback (can be async). If callback is passed, JSON fields won't be merged. In case if file contents is not an object, you must pass callback.
7780
*/
7881
export const modifyJsonFile: ModifyJsonFileGenericFunction = async (
7982
path,
@@ -89,19 +92,22 @@ export const modifyJsonFile: ModifyJsonFileGenericFunction = async (
8992
} = options || {};
9093
try {
9194
let { json, indent } = await loadJsonFile(path, { encoding, tabSize });
92-
// todo remove restriction or not?
93-
if (!json || typeof json !== "object" || Array.isArray(json)) throw new TypeError(`${path}: JSON root type must be object`);
9495
if (typeof modifyFields === "function") {
9596
json = await modifyFields(json);
9697
} else {
97-
for (const [name, value] of Object.entries(modifyFields)) {
98+
if (typeof json !== "object" || Array.isArray(json)) throw new TypeError(`${path}: Root type is not object. Only callback can be used`);
99+
for (const parts of Object.entries(modifyFields)) {
100+
// todo fix typescript types workaround
101+
const name = parts[0] as string;
102+
const value = parts[1] as any;
103+
104+
const isSetter = typeof value === "function";
98105
if (!(name in json)) {
99-
const isSetter = typeof value === "function";
100106
const generalAction = isSetter ? ifFieldIsMissingForSetter : ifFieldIsMissing;
101107
if (generalAction === "throw") throw new TypeError(`Property to modify "${name}" is missing in ${path}`);
102108
if (generalAction === "skip") continue;
103109
}
104-
json[name as string] = typeof value === "function" ? value(json[name as string], json) : value;
110+
json[name as string] = isSetter ? value(json[name as string], json) : value;
105111
}
106112
}
107113

@@ -110,7 +116,6 @@ export const modifyJsonFile: ModifyJsonFileGenericFunction = async (
110116
JSON.stringify(json, undefined, indent)
111117
);
112118
} catch (err) {
113-
// if (err.innerError) throw new Error(err.message);
114119
if (throws) throw err;
115120
}
116121
};

tests/index.test-d.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expectType } from "tsd";
2+
3+
// todo change to ../build
4+
import { modifyJsonFile, modifyPackageJsonFile } from "../src/";
5+
6+
modifyJsonFile("path.json", {
7+
value: 5,
8+
extremeValue: n => n + 1,
9+
// //@ts-expect-error
10+
notAllowed: {
11+
test: () => 10
12+
}
13+
});
14+
15+
modifyJsonFile<{ someNumber: number; anotherProp: { someString: string; }; }>("path.json", {
16+
someNumber: 5
17+
});
18+
19+
//@ts-expect-error
20+
modifyJsonFile<number>("path.json", {});
21+
22+
modifyJsonFile<number>("path.json", n => {
23+
expectType<number>(n);
24+
return n + 5;
25+
});
26+
27+
modifyPackageJsonFile("someDirWithPackageJson", {
28+
name: name => `@supertf/${name}`,
29+
dependencies: {
30+
string: "string"
31+
},
32+
author: {
33+
//@ts-expect-error
34+
name: name => `super ${name}`
35+
}
36+
});

tests/index.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import fs from "fs/promises";
44
import jsonfile from "jsonfile";
55
import path from "path";
66

7+
// todo convert paths
78
import { modifyJsonFile, modifyPackageJsonFile } from "../build/";
89

910
const jsonFilePath = path.join(__dirname, "testing-file.json");
@@ -47,3 +48,9 @@ test("modifies package.json file with async function", async t => {
4748
t.snapshot(modifiedJsonFle);
4849
});
4950

51+
test("modifies package.json file which has numeric type", async t => {
52+
dataToWrite = 50;
53+
await modifyPackageJsonFile(jsonFilePath, n => n + 5);
54+
const modifiedJsonFle = await fs.readFile(jsonFilePath, "utf8");
55+
t.snapshot(modifiedJsonFle);
56+
});

0 commit comments

Comments
 (0)