Skip to content

Commit ed674cd

Browse files
committed
initial version
1 parent 17c6184 commit ed674cd

File tree

6 files changed

+479
-0
lines changed

6 files changed

+479
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
build

package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "modify-json-file",
3+
"version": "0.0.1",
4+
"main": "build/index.js",
5+
"types": "build/index.d.ts",
6+
"description": "Simple and modern way to modify JSON files",
7+
"repository": "https://github.com/zardoy/modify-json-file",
8+
"keywords": [
9+
"json",
10+
"files",
11+
"modify",
12+
"change",
13+
"update",
14+
"replace",
15+
"fields",
16+
"package.json",
17+
"tsconfig.json"
18+
],
19+
"author": "Vitaly <vital2580@icloud.com>",
20+
"license": "MIT",
21+
"scripts": {
22+
"prepublishOnly": "yarn tsc",
23+
"test": "echo they're definitely needed"
24+
},
25+
"devDependencies": {
26+
"@types/node": "^15.12.2",
27+
"@zardoy/tsconfig": "^1.0.2",
28+
"ts-node": "^10.0.0",
29+
"typescript": "^4.3.2"
30+
},
31+
"dependencies": {
32+
"detect-indent": "^6.1.0",
33+
"load-json-file": "^6.2.0",
34+
"type-fest": "^1.2.0"
35+
}
36+
}

readme.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Modify JSON
2+
3+
Simplest way to modify JSON files
4+
5+
[API](https://paka.dev/npm/@zardoy/modify-json)
6+
7+
## Usage
8+
9+
- Only async use
10+
11+
Let's say we *package.json* file:
12+
```json
13+
// 📁package.json
14+
{
15+
"name": "package",
16+
"main": "index.js",
17+
"author": "eldar",
18+
"dependencies": {
19+
"type-fest": "*",
20+
"fdir": ">=2"
21+
}
22+
}
23+
```
24+
25+
And this code:
26+
27+
```ts
28+
import { modifyJsonFile } from "@zardoy/modify-json";
29+
30+
// of course, you should use path module here
31+
await modifyJsonFile("package.json", {
32+
name: s => `super ${s}`,
33+
main: "build/electron.js",
34+
dependencies: {
35+
"type-fest": "^1.0.0"
36+
}
37+
})
38+
```
39+
After running this code, we'll get this package.json:
40+
41+
```json
42+
{
43+
"name": "super package",
44+
"main": "build/electron.js",
45+
"author": "eldar",
46+
"dependencies": {
47+
"type-fest": "^1.0.0"
48+
}
49+
}
50+
```
51+
52+
As you can see above, `modifyJsonFile` only merges fields on 1 level depth. Currently, we don't support merging in nested fields.
53+
54+
We're using [detect-indent](https://www.npmjs.com/package/detect-indent) to preserve the tab size in `.json` files.
55+
56+
57+
## TODO
58+
59+
- [ ] Fix. Currently, to preserve the tab size, json file readed twice
60+
- [ ] tests. should be easy but I'm extremely lazy
61+
- [ ] transformer for paths (most likely babel plugin):
62+
63+
```ts
64+
await fs.promises.readFile(./package.json, "utf8");
65+
```
66+
67+
Into this:
68+
69+
```ts
70+
await fs.promises.readFile(path.join(__dirname, "package.json"), "utf8");
71+
```
72+
73+
- [ ] find a way to use FS in builder-way (like [fdir](https://www.npmjs.com/package/fdir) does) and deprecate this module
74+
75+
## Related
76+
77+
<!-- With *jsonfile*, you need to read / write objects. 1 function is simpler. That's super important for me, because I need to work with JSON files a lot. -->
78+
79+
- [jsonfile](https://npmjs.com/jsonfile): simple reading / writing for json files
80+
- immer ?
81+
82+
[Other cool modules for working with JSON](https://github.com/search?q=user%3Asindresorhus+json)

src/index.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import fs from "fs";
2+
import { PackageJson, TsConfigJson } from "type-fest";
3+
import detectIndent from "detect-indent"
4+
import loadJsonFile from "load-json-file"
5+
6+
interface Options {
7+
/** @default utf-8 */
8+
encoding: BufferEncoding
9+
/**
10+
* Will throw in case of FS error or invalid JSON
11+
* @default true
12+
* */
13+
throws: boolean
14+
// ideally this lib should integrate with json validator
15+
/** @default "throw" (even if throws is false) */
16+
ifFieldIsMissing: "throw" | "skip" | "add"
17+
/**
18+
* - throw - throws (even if throws param is false)
19+
*- skip - won't call the function
20+
* - pass - pass the `undefined` value
21+
* @default "throw"
22+
* */
23+
ifFieldIsMissingForSetter: "throw" | "skip" | "pass"
24+
/**
25+
* - null - disable formatting
26+
* - hard - one hard tab \t
27+
* - number - number of spaces
28+
* @default "preserve"
29+
* */
30+
tabSize: null | number | "preserve" | "hard"
31+
}
32+
33+
type GettersDeep<T extends object> = {
34+
[K in keyof T]: (oldValue: T[K], json: T) => unknown
35+
//T[K] extends object ? ((oldValue: T[K]) => unknown)/* | GettersDeep<T[K]> */ : (oldValue: T[K]) => unknown
36+
}
37+
38+
export type ModifyJsonFileFunction<T extends object> = (
39+
path: string,
40+
fields: Partial<T | GettersDeep<T>>,
41+
options?: Options
42+
) => Promise<void>;
43+
44+
type ModifyJsonFileGenericFunction = <T extends object>(
45+
path: string,
46+
fields: Partial<T | GettersDeep<T>>,
47+
options?: Partial<Options>
48+
) => Promise<void>;
49+
50+
/** It's just Error */
51+
class InnerError extends Error {
52+
innerError = true;
53+
}
54+
55+
/**
56+
* modifies **original** JSON file
57+
* You can pass generic, that reflects the structure of original JSON file
58+
*
59+
* Fields, that are functions will be skipped if they're not preset in original JSON file
60+
*/
61+
export const modifyJsonFile: ModifyJsonFileGenericFunction = async (
62+
path,
63+
fields,
64+
options
65+
) => {
66+
const {
67+
encoding = "utf-8",
68+
throws = true ,
69+
ifFieldIsMissing = "throw",
70+
ifFieldIsMissingForSetter = "throw",
71+
tabSize: tabSizeOption = "preserve"
72+
} = options || {};
73+
try {
74+
const json = await loadJsonFile(path);
75+
// todo remove restriction or not?
76+
if (!json || typeof json !== "object" || Array.isArray(json)) throw new TypeError(`${path}: JSON root type must be object`);
77+
// todo we don't need to read the file twice
78+
const rawText = await fs.promises.readFile(path, encoding);
79+
const indent = tabSizeOption === "preserve" ? detectIndent(rawText).indent : tabSizeOption === "hard" ? "\t" : tabSizeOption === null ? undefined : " ".repeat(tabSizeOption);
80+
81+
for (const [name, value] of Object.entries(fields)) {
82+
if (!(name in json)) {
83+
const isSetter = typeof value === "function";
84+
const generalAction = isSetter ? ifFieldIsMissingForSetter : ifFieldIsMissing;
85+
if (generalAction === "throw") throw new InnerError(`Property to modify "${name}" is missing in ${path}`);
86+
if (generalAction === "skip") continue;
87+
}
88+
json[name as string] = typeof value === "function" ? value(json[name as string], json) : value;
89+
}
90+
91+
await fs.promises.writeFile(
92+
path,
93+
JSON.stringify(json, undefined, indent)
94+
);
95+
} catch(err) {
96+
if (err.innerError) throw new Error(err.message);
97+
if (throws) throw err;
98+
}
99+
}
100+
101+
// todo: use read-pkg / write-pkg for normalization
102+
103+
/**
104+
* Almost the same is sindresorhus/write-pkg, but with proper typing support and setters for fields
105+
*/
106+
export const modifyPackageJson: ModifyJsonFileFunction<PackageJson> = modifyJsonFile;
107+
108+
export const modifyTsConfigJson: ModifyJsonFileFunction<TsConfigJson> = modifyJsonFile;
109+

tsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "@zardoy/tsconfig/node",
3+
"compilerOptions": {
4+
"module": "CommonJS",
5+
"esModuleInterop": true,
6+
"declaration": true
7+
}
8+
}

0 commit comments

Comments
 (0)