diff --git a/package.json b/package.json index af8e08d..44517df 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:types": "tsc --noEmit --module esnext --skipLibCheck --moduleResolution node ./test/*.test.ts" }, "dependencies": { + "detect-indent": "^7.0.1", "jsonc-parser": "^3.2.0", "mlly": "^1.1.1", "pathe": "^1.1.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5684292..c6983aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ specifiers: '@types/node': ^18.13.0 '@vitest/coverage-c8': ^0.28.5 changelogen: ^0.4.1 + detect-indent: ^7.0.1 eslint: ^8.34.0 eslint-config-unjs: ^0.1.0 expect-type: ^0.15.0 @@ -17,6 +18,7 @@ specifiers: vitest: ^0.28.5 dependencies: + detect-indent: 7.0.1 jsonc-parser: 3.2.0 mlly: 1.1.1 pathe: 1.1.0 @@ -1499,6 +1501,11 @@ packages: resolution: {integrity: sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA==} dev: true + /detect-indent/7.0.1: + resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} + engines: {node: '>=12.20'} + dev: false + /diff/5.1.0: resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} engines: {node: '>=0.3.1'} diff --git a/src/index.ts b/src/index.ts index 9062a63..9633f3d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,13 @@ import { promises as fsp } from "node:fs"; import { dirname, resolve, isAbsolute } from "pathe"; import { ResolveOptions as _ResolveOptions, resolvePath } from "mlly"; -import { findFile, FindFileOptions, findNearestFile } from "./utils"; +import { + findFile, + FindFileOptions, + findNearestFile, + writeJsonFile, + WriteOptions, +} from "./utils"; import type { PackageJson, TSConfig } from "./types"; export * from "./types"; @@ -42,9 +48,10 @@ export async function readPackageJSON( export async function writePackageJSON( path: string, - package_: PackageJson + package_: PackageJson, + options: WriteOptions = {} ): Promise { - await fsp.writeFile(path, JSON.stringify(package_, undefined, 2)); + await writeJsonFile(path, package_, options); } export async function readTSConfig( @@ -68,9 +75,10 @@ export async function readTSConfig( export async function writeTSConfig( path: string, - tsconfig: TSConfig + tsconfig: TSConfig, + options: WriteOptions = {} ): Promise { - await fsp.writeFile(path, JSON.stringify(tsconfig, undefined, 2)); + await writeJsonFile(path, tsconfig, options); } export async function resolvePackageJSON( diff --git a/src/utils.ts b/src/utils.ts index 93e989e..bfaaf84 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ -import { statSync } from "node:fs"; +import { statSync, existsSync } from "node:fs"; +import { readFile, writeFile } from "node:fs/promises"; import { join, resolve } from "pathe"; +import detectIndent from "detect-indent"; export interface FindFileOptions { /** @@ -30,6 +32,16 @@ export interface FindFileOptions { /** @deprecated */ export type FindNearestFileOptions = FindFileOptions; +export type WriteOptions = { + indent?: number | string; + newline?: boolean | string; +}; + +export type ResolvedWriteOptions = { + indent: number | string; + newline: string; +}; + const defaultFindOptions: Required = { startingFrom: ".", rootPattern: /^node_modules$/, @@ -97,3 +109,35 @@ export function findFarthestFile( ): Promise { return findFile(filename, { ..._options, reverse: true }); } + +export async function resolveWriteOptions( + path: string, + options: WriteOptions +): Promise { + const file = existsSync(path) ? await readFile(path, "utf8") : undefined; + const indent = options.indent ?? (file ? detectIndent(file).indent : 2); + const newline = + options.newline === true + ? "\n" + : options.newline === false + ? "" + : options.newline ?? (file ? (file.endsWith("\n") ? "\n" : "") : "\n"); + + return { + indent, + newline, + }; +} + +export async function writeJsonFile( + path: string, + value: any, + options: WriteOptions = {} +): Promise { + const resolvedOpts = await resolveWriteOptions(path, options); + + let content = JSON.stringify(value, undefined, resolvedOpts.indent); + content += resolvedOpts.newline; + + await writeFile(path, content); +} diff --git a/test/index.test.ts b/test/index.test.ts index 0d3ac19..3a20026 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,9 @@ import { fileURLToPath } from "node:url"; +import { readFile } from "node:fs/promises"; import { dirname, resolve } from "pathe"; import { describe, expect, it } from "vitest"; import { expectTypeOf } from "expect-type"; +import detectIndent from "detect-indent"; import { readPackageJSON, readTSConfig, @@ -90,6 +92,36 @@ describe("package.json", () => { "string" ); }); + + it("write package.json with tab indent", async () => { + const path = rFixture("package.json.tab.tmp"); + const indent = "\t"; + await writePackageJSON(path, { version: "1.0.0" }, { indent }); + const file = await readFile(path, "utf8"); + expect(detectIndent(file).indent).toBe(indent); + }); + + it("write package.json with 4 space indent", async () => { + const path = rFixture("package.json.3space.tmp"); + const indent = 4; + await writePackageJSON(path, { version: "1.0.0" }, { indent }); + const file = await readFile(path, "utf8"); + expect(detectIndent(file).indent).toBe(" "); + }); + + it("write package.json with newline", async () => { + const path = rFixture("package.json.newline.tmp"); + await writePackageJSON(path, { version: "1.0.0" }, { newline: true }); + const file = await readFile(path, "utf8"); + expect(file.endsWith("\n")).toBe(true); + }); + + it("write package.json with no newline", async () => { + const path = rFixture("package.json.no-newline.tmp"); + await writePackageJSON(path, { version: "1.0.0" }, { newline: false }); + const file = await readFile(path, "utf8"); + expect(file.endsWith("\n")).toBe(false); + }); }); describe("tsconfig.json", () => {