diff --git a/package.json b/package.json index a52fd70..d4f987c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,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.4.2", "pathe": "^1.1.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 33e71ba..5865fff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + detect-indent: + specifier: ^7.0.1 + version: 7.0.1 jsonc-parser: specifier: ^3.2.0 version: 3.2.0 @@ -1575,7 +1578,7 @@ packages: normalize-path: 3.0.0 readdirp: 3.6.0 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /chownr@2.0.0: @@ -1741,6 +1744,11 @@ packages: resolution: {integrity: sha512-FJ9RDpf3GicEBvzI3jxc2XhHzbqD8p4ANw/1kPsFBfTvP1b7Gn/Lg1vO7R9J4IVgoMbyUmFrFGZafJ1hPZpvlg==} dev: true + /detect-indent@7.0.1: + resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} + engines: {node: '>=12.20'} + dev: false + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2446,14 +2454,6 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3706,7 +3706,7 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 dev: true /rollup@4.7.0: diff --git a/src/index.ts b/src/index.ts index 9062a63..78d2c81 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,9 @@ 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 type { PackageJson, TSConfig } from "./types"; +import detectIndent from "detect-indent"; +import { detectNewline, findFile, FindFileOptions, findNearestFile } from "./utils"; +import type { PackageJsonFile, TSConfig } from "./types"; export * from "./types"; export * from "./utils"; @@ -12,7 +13,7 @@ export type ReadOptions = { cache?: boolean | Map>; }; -export function definePackageJSON(package_: PackageJson): PackageJson { +export function definePackageJSON(package_: PackageJsonFile): PackageJsonFile { return package_; } @@ -25,26 +26,36 @@ const FileCache = new Map>(); export async function readPackageJSON( id?: string, options: ResolveOptions & ReadOptions = {} -): Promise { +): Promise { const resolvedPath = await resolvePackageJSON(id, options); const cache = options.cache && typeof options.cache !== "boolean" ? options.cache : FileCache; if (options.cache && cache.has(resolvedPath)) { - return cache.get(resolvedPath)!; + return cache.get(resolvedPath)! as PackageJsonFile & PackageJsonFile; } const blob = await fsp.readFile(resolvedPath, "utf8"); - const parsed = JSON.parse(blob) as PackageJson; - cache.set(resolvedPath, parsed); - return parsed; + const meta = { + indent: detectIndent(blob).indent, + newline: detectNewline(blob), + }; + const parsed = JSON.parse(blob) as PackageJsonFile; + const file = { ...parsed, ...meta }; + cache.set(resolvedPath, file); + return file; } export async function writePackageJSON( path: string, - package_: PackageJson + package_: PackageJsonFile & PackageJsonFile ): Promise { - await fsp.writeFile(path, JSON.stringify(package_, undefined, 2)); + const { indent, newline, ...data } = package_; + let json = JSON.stringify(data, undefined, indent); + if (newline) { + json += newline; + } + await fsp.writeFile(path, json); } export async function readTSConfig( diff --git a/src/types/packagejson.ts b/src/types/packagejson.ts index 8ac096c..2c2546d 100644 --- a/src/types/packagejson.ts +++ b/src/types/packagejson.ts @@ -157,3 +157,8 @@ export interface PackageJson { workspaces?: string[]; [key: string]: any; } + +export type PackageJsonFile = { + indent?: string; + newline?: string | undefined; +} & PackageJson; diff --git a/src/utils.ts b/src/utils.ts index 54a311c..8d3d8af 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -43,6 +43,20 @@ const defaultFindOptions: Required = { }, }; +export function detectNewline(str: string) { + if (typeof str !== 'string') { + throw new TypeError('Expected a string'); + } + const newlines = str.match(/(?:\r?\n)/g) || []; + if (newlines.length === 0) { + return; + } + const crlf = newlines.filter(newline => newline === '\r\n').length; + const lf = newlines.length - crlf; + + return crlf > lf ? '\r\n' : '\n'; +} + export async function findFile( filename: string, _options: FindFileOptions = {} diff --git a/test/index.test.ts b/test/index.test.ts index 0d3ac19..e9c4f29 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -79,6 +79,28 @@ describe("package.json", () => { ).to.equal("1.0.0"); }); + it("correctly indents package.json", async () => { + const indent = " "; + await writePackageJSON(rFixture("package.json.tmp"), { + version: "1.0.0", + indent, + }); + expect( + (await readPackageJSON(rFixture("package.json.tmp"))).indent + ).to.equal(indent); + }); + + it("correctly adds EOF newline to package.json", async () => { + const newline = "\n"; + await writePackageJSON(rFixture("package.json.tmp"), { + version: "1.0.0", + newline, + }); + expect( + (await readPackageJSON(rFixture("package.json.tmp"))).newline + ).to.equal(newline); + }); + it("correctly reads a version from absolute path", async () => { expect( await readPackageJSON(rFixture(".")).then((p) => p?.version)