diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 441975c..1ed55d5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,12 @@ jobs: fail-fast: false matrix: node-version: + - 20 + - 18 - 16 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/index.d.ts b/index.d.ts index d4787cc..7fdd2a7 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,6 +1,6 @@ -import {JsonObject} from 'type-fest'; +import type {PackageJson, JsonObject} from 'type-fest'; -export interface Options { +export type Options = { /** The indentation to use for new files. @@ -18,13 +18,20 @@ export interface Options { @default true */ readonly normalize?: boolean; -} +}; + +/** +A JSON object with suggested fields for [npm's `package.json` file](https://docs.npmjs.com/creating-a-package-json-file). +*/ +type PackageJsonData = PackageJson | JsonObject; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents /** Write a `package.json` file. Writes atomically and creates directories for you as needed. Sorts dependencies when writing. Preserves the indentation if the file already exists. +To write to a `package.json` file while preserving unchanged fields, see {@link updatePackage}. + @param path - The path to where the `package.json` file should be written or its directory. @example @@ -39,14 +46,16 @@ await writePackage(path.join('unicorn', 'package.json'), {foo: true}); console.log('done'); ``` */ -export function writePackage(path: string, data: JsonObject, options?: Options): Promise; -export function writePackage(data: JsonObject, options?: Options): Promise; +export function writePackage(path: string, data: PackageJsonData, options?: Options): Promise; +export function writePackage(data: PackageJsonData, options?: Options): Promise; /** Synchronously write a `package.json` file. Writes atomically and creates directories for you as needed. Sorts dependencies when writing. Preserves the indentation if the file already exists. +To write to a `package.json` file while preserving unchanged fields, see {@link updatePackageSync}. + @param path - The path to where the `package.json` file should be written or its directory. @example @@ -61,5 +70,153 @@ writePackageSync(path.join('unicorn', 'package.json'), {foo: true}); console.log('done'); ``` */ -export function writePackageSync(path: string, data: JsonObject, options?: Options): void; -export function writePackageSync(data: JsonObject, options?: Options): void; +export function writePackageSync(path: string, data: PackageJsonData, options?: Options): void; +export function writePackageSync(data: PackageJsonData, options?: Options): void; + +/** +Update a `package.json` file. + +Writes atomically and creates directories for you as needed. Sorts dependencies when writing. Preserves the indentation if the file already exists. + +@param path - The path to where the `package.json` file should be written or its directory. + +@example +``` +import {updatePackage} from 'write-pkg'; + +await updatePackage({foo: true}); +//=> { "foo": true } + +await updatePackage({foo: false, bar: true}); +//=> { "foo": false, "bar": true } +``` +*/ +export function updatePackage(path: string, data: PackageJsonData, options?: Options): Promise; +export function updatePackage(data: PackageJsonData, options?: Options): Promise; + +/** +Update a `package.json` file. + +Writes atomically and creates directories for you as needed. Sorts dependencies when writing. Preserves the indentation if the file already exists. + +@param path - The path to where the `package.json` file should be written or its directory. + +@example +``` +import {updatePackageSync} from 'write-pkg'; + +updatePackageSync({foo: true}); +//=> { "foo": true } + +updatePackageSync({foo: false, bar: true}); +//=> { "foo": false, "bar": true } +``` +*/ +export function updatePackageSync(path: string, data: PackageJsonData, options?: Options): void; +export function updatePackageSync(data: PackageJsonData, options?: Options): void; + +type DependencyKeys = + | 'dependencies' + | 'devDependencies' + | 'optionalDependencies' + | 'peerDependencies'; + +type Dependencies = Partial> | Pick; + +/** +Add dependencies to a `package.json` file. + +Sorts dependencies when writing. Preserves indentation, or creates the file if it does not exist. + +@param path - The path to where the `package.json` file should be written or its directory. + +@example +``` +import {writePackage, addPackageDependencies} from 'write-pkg'; + +await writePackage({foo: true}); +//=> { "foo": true } + +await addPackageDependencies({foo: '1.0.0'}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" } } + +await addPackageDependencies({dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" }, "devDependencies": { "bar": "1.0.0" } } +``` +*/ +export function addPackageDependencies(path: string, dependencies: Dependencies, options?: Options): Promise; +export function addPackageDependencies(dependencies: Dependencies, options?: Options): Promise; + +/** +Add dependencies to a `package.json` file. + +Sorts dependencies when writing. Preserves indentation, or creates the file if it does not exist. + +@param path - The path to where the `package.json` file should be written or its directory. + +@example +``` +import {writePackageSync, addPackageDependenciesSync} from 'write-pkg'; + +writePackageSync({foo: true}); +//=> { "foo": true } + +addPackageDependenciesSync({foo: '1.0.0'}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" } } + +addPackageDependenciesSync({dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" }, "devDependencies": { "bar": "1.0.0" } } +``` +*/ +export function addPackageDependenciesSync(path: string, dependencies: Dependencies, options?: Options): void; +export function addPackageDependenciesSync(dependencies: Dependencies, options?: Options): void; + +type DependenciesToRemove = string[] | Partial>; + +/** +Remove dependencies from a `package.json` file. + +Sorts dependencies when writing. Preserves indentation. Does not throw if the file does not exist. + +@param path - The path to where the `package.json` file should be written or its directory. + +@example +``` +import {writePackage, removePackageDependencies} from 'write-pkg'; + +await writePackage({foo: true, dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" }, "devDependencies": { "bar": "1.0.0" } } + +await removePackageDependencies(['foo']); +//=> { "foo": true, "devDependencies": { "bar": "1.0.0" } } + +await removePackageDependencies({devDependencies: ['bar']}); +//=> { "foo": true } +``` +*/ +export function removePackageDependencies(path: string, dependencies: DependenciesToRemove, options?: Options): Promise; +export function removePackageDependencies(dependencies: DependenciesToRemove, options?: Options): Promise; + +/** +Remove dependencies from a `package.json` file. + +Sorts dependencies when writing. Preserves indentation. Does not throw if the file does not exist. + +@param path - The path to where the `package.json` file should be written or its directory. + +@example +``` +import {writePackageSync, removePackageDependenciesSync} from 'write-pkg'; + +writePackageSync({foo: true, dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" }, "devDependencies": { "bar": "1.0.0" } } + +removePackageDependenciesSync(['foo']); +//=> { "foo": true, "devDependencies": { "bar": "1.0.0" } } + +removePackageDependenciesSync({devDependencies: ['bar']}); +//=> { "foo": true } +``` +*/ +export function removePackageDependenciesSync(path: string, dependencies: DependenciesToRemove, options?: Options): void; +export function removePackageDependenciesSync(dependencies: DependenciesToRemove, options?: Options): void; diff --git a/index.js b/index.js index 1e22f8d..2d29446 100644 --- a/index.js +++ b/index.js @@ -1,64 +1,4 @@ -import path from 'node:path'; -import {writeJsonFile, writeJsonFileSync} from 'write-json-file'; -import sortKeys from 'sort-keys'; - -const dependencyKeys = new Set([ - 'dependencies', - 'devDependencies', - 'optionalDependencies', - 'peerDependencies', -]); - -function normalize(packageJson) { - const result = {}; - - for (const key of Object.keys(packageJson)) { - if (!dependencyKeys.has(key)) { - result[key] = packageJson[key]; - } else if (Object.keys(packageJson[key]).length > 0) { - result[key] = sortKeys(packageJson[key]); - } - } - - return result; -} - -export async function writePackage(filePath, data, options) { - if (typeof filePath !== 'string') { - options = data; - data = filePath; - filePath = '.'; - } - - options = { - normalize: true, - ...options, - detectIndent: true, - }; - - filePath = path.basename(filePath) === 'package.json' ? filePath : path.join(filePath, 'package.json'); - - data = options.normalize ? normalize(data) : data; - - return writeJsonFile(filePath, data, options); -} - -export function writePackageSync(filePath, data, options) { - if (typeof filePath !== 'string') { - options = data; - data = filePath; - filePath = '.'; - } - - options = { - normalize: true, - ...options, - detectIndent: true, - }; - - filePath = path.basename(filePath) === 'package.json' ? filePath : path.join(filePath, 'package.json'); - - data = options.normalize ? normalize(data) : data; - - writeJsonFileSync(filePath, data, options); -} +export {writePackage, writePackageSync} from './source/write-package.js'; +export {updatePackage, updatePackageSync} from './source/update-package.js'; +export {addPackageDependencies, addPackageDependenciesSync} from './source/add-remove-dependencies.js'; +export {removePackageDependencies, removePackageDependenciesSync} from './source/add-remove-dependencies.js'; diff --git a/index.test-d.ts b/index.test-d.ts index cb5f7b8..2b787b5 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,12 +1,80 @@ import {expectType} from 'tsd'; -import {writePackage, writePackageSync} from './index.js'; +import { + writePackage, + writePackageSync, + updatePackage, + updatePackageSync, + addPackageDependencies, + addPackageDependenciesSync, + removePackageDependencies, + removePackageDependenciesSync, +} from './index.js'; +expectType>(writePackage('package.json', {version: 1})); +expectType>(writePackage('package.json', {version: '1.0.0'})); expectType>(writePackage('package.json', {version: 1})); expectType>(writePackage('package.json', {version: 1}, {normalize: false})); +expectType>(writePackage('package.json', {version: 1}, {indent: 2})); expectType>(writePackage({version: 1})); expectType>(writePackage({version: 1}, {normalize: false})); +expectType>(writePackage({version: 1}, {indent: 2})); expectType(writePackageSync('package.json', {version: 1})); +expectType(writePackageSync('package.json', {version: '1.0.0'})); expectType(writePackageSync('package.json', {version: 1}, {normalize: false})); +expectType(writePackageSync('package.json', {version: 1}, {indent: 2})); expectType(writePackageSync({version: 1})); expectType(writePackageSync({version: 1}, {normalize: false})); +expectType(writePackageSync({version: 1}, {indent: 2})); + +expectType>(updatePackage('package.json', {version: 1})); +expectType>(updatePackage('package.json', {version: '1.0.0'})); +expectType>(updatePackage('package.json', {version: 1}, {normalize: false})); +expectType>(updatePackage('package.json', {version: 1}, {indent: 2})); +expectType>(updatePackage({version: 1})); +expectType>(updatePackage({version: 1}, {normalize: false})); +expectType>(updatePackage({version: 1}, {indent: 2})); + +expectType(updatePackageSync('package.json', {version: 1})); +expectType(updatePackageSync('package.json', {version: '1.0.0'})); +expectType(updatePackageSync('package.json', {version: 1}, {normalize: false})); +expectType(updatePackageSync('package.json', {version: 1}, {indent: 2})); +expectType(updatePackageSync({version: 1})); +expectType(updatePackageSync({version: 1}, {normalize: false})); +expectType(updatePackageSync({version: 1}, {indent: 2})); + +expectType>(addPackageDependencies('package.json', {foo: '1.0.0'})); +expectType>(addPackageDependencies('package.json', {dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}})); +expectType>(addPackageDependencies('package.json', {foo: '1.0.0'}, {normalize: false})); +expectType>(addPackageDependencies('package.json', {foo: '1.0.0'}, {indent: 2})); +expectType>(addPackageDependencies({foo: '1.0.0'})); +expectType>(addPackageDependencies({dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}})); +expectType>(addPackageDependencies({foo: '1.0.0'}, {normalize: false})); +expectType>(addPackageDependencies({foo: '1.0.0'}, {indent: 2})); + +expectType(addPackageDependenciesSync('package.json', {foo: '1.0.0'})); +expectType(addPackageDependenciesSync('package.json', {dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}})); +expectType(addPackageDependenciesSync('package.json', {foo: '1.0.0'}, {normalize: false})); +expectType(addPackageDependenciesSync('package.json', {foo: '1.0.0'}, {indent: 2})); +expectType(addPackageDependenciesSync({foo: '1.0.0'})); +expectType(addPackageDependenciesSync({dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}})); +expectType(addPackageDependenciesSync({foo: '1.0.0'}, {normalize: false})); +expectType(addPackageDependenciesSync({foo: '1.0.0'}, {indent: 2})); + +expectType>(removePackageDependencies('package.json', ['foo'])); +expectType>(removePackageDependencies('package.json', {dependencies: ['foo'], devDependencies: ['bar']})); +expectType>(removePackageDependencies('package.json', ['foo'], {normalize: false})); +expectType>(removePackageDependencies('package.json', ['foo'], {indent: 2})); +expectType>(removePackageDependencies(['foo'])); +expectType>(removePackageDependencies({dependencies: ['foo'], devDependencies: ['bar']})); +expectType>(removePackageDependencies(['foo'], {normalize: false})); +expectType>(removePackageDependencies(['foo'], {indent: 2})); + +expectType(removePackageDependenciesSync('package.json', ['foo'])); +expectType(removePackageDependenciesSync('package.json', {dependencies: ['foo'], devDependencies: ['bar']})); +expectType(removePackageDependenciesSync('package.json', ['foo'], {normalize: false})); +expectType(removePackageDependenciesSync('package.json', ['foo'], {indent: 2})); +expectType(removePackageDependenciesSync(['foo'])); +expectType(removePackageDependenciesSync({dependencies: ['foo'], devDependencies: ['bar']})); +expectType(removePackageDependenciesSync(['foo'], {normalize: false})); +expectType(removePackageDependenciesSync(['foo'], {indent: 2})); diff --git a/package.json b/package.json index c7f97de..fb68ed6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "write-pkg", - "version": "5.1.0", + "version": "6.0.0", "description": "Write a package.json file", "license": "MIT", "repository": "sindresorhus/write-pkg", @@ -13,14 +13,15 @@ "type": "module", "exports": "./index.js", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=16" }, "scripts": { "test": "xo && ava && tsd" }, "files": [ "index.js", - "index.d.ts" + "index.d.ts", + "source" ], "keywords": [ "json", @@ -32,15 +33,17 @@ "package" ], "dependencies": { + "deepmerge-ts": "^5.1.0", + "read-pkg": "^8.0.0", "sort-keys": "^5.0.0", - "type-fest": "^2.12.1", + "type-fest": "^3.13.0", "write-json-file": "^5.0.0" }, "devDependencies": { - "ava": "^4.1.0", - "read-pkg": "^7.1.0", - "tempfile": "^4.0.0", - "tsd": "^0.19.1", - "xo": "^0.48.0" + "ava": "^5.3.1", + "filter-anything": "^3.0.7", + "tempy": "^3.1.0", + "tsd": "^0.28.1", + "xo": "^0.54.2" } } diff --git a/readme.md b/readme.md index 9643c1c..a40a4b0 100644 --- a/readme.md +++ b/readme.md @@ -27,7 +27,7 @@ console.log('done'); ### writePackage(path?, data, options?) -Returns a `Promise`. +Returns a `Promise` that resolves when the `package.json` file has been written. ### writePackageSync(path?, data, options?) @@ -38,10 +38,131 @@ Default: `process.cwd()` The path to where the `package.json` file should be written or its directory. +#### data + +Type `object` + +JSON data to write to the `package.json` file. + #### options Type: `object` +See [Options](#options-4). + +### updatePackage(path?, data, options?) + +Returns a `Promise` that resolves when the `package.json` file has been updated. + +### updatePackageSync(path?, data, options?) + +```js +import {updatePackage} from 'write-pkg'; + +await updatePackage({foo: true}); +//=> { "foo": true } + +await updatePackage({foo: false, bar: true}); +//=> { "foo": false, "bar": true } +``` + +#### path + +Type: `string`\ +Default: `process.cwd()` + +The path to where the `package.json` file should be written or its directory. + +#### data + +Type `object` + +JSON data to write to the `package.json` file. If the file already exists, existing fields will be merged with the values in `data`. + +#### options + +Type: `object` + +See [Options](#options-4). + +### addPackageDependencies(path?, dependencies, options?) + +Returns a `Promise` that resolves when the `package.json` file has been written. + +### addPackageDependenciesSync(path?, dependencies, options?) + +```js +import {writePackage, addPackageDependencies} from 'write-pkg'; + +await writePackage({foo: true}); +//=> { "foo": true } + +await addPackageDependencies({foo: '1.0.0'}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" } } + +await addPackageDependencies({dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" }, "devDependencies": { "bar": "1.0.0" } } +``` + +#### path + +Type: `string`\ +Default: `process.cwd()` + +The path to where the `package.json` file should be written or its directory. + +#### dependencies + +Type: `Record | Partial>>` + +Dependencies to add to the `package.json` file. + +#### options + +Type: `object` + +See [Options](#options-4). + +### removePackageDependencies(path?, dependencies, options?) + +Returns a `Promise` that resolves when the `package.json` file has been written. Does not throw if the file does not exist. + +### removePackageDependenciesSync(path?, dependencies, options?) + +```js +import {writePackage, removePackageDependencies} from 'write-pkg'; + +await writePackage({foo: true, dependencies: {foo: '1.0.0'}, devDependencies: {bar: '1.0.0'}}); +//=> { "foo": true, "dependencies": { "foo": "1.0.0" }, "devDependencies": { "bar": "1.0.0" } } + +await removePackageDependencies(['foo']); +//=> { "foo": true, "devDependencies": { "bar": "1.0.0" } } + +await removePackageDependencies({devDependencies: ['bar']}); +//=> { "foo": true } +``` + +#### path + +Type: `string`\ +Default: `process.cwd()` + +The path to where the `package.json` file should be written or its directory. + +#### dependencies + +Type `string[] | Partial>` + +Dependencies to remove from the `package.json` file. + +#### options + +Type: `object` + +See [Options](#options-4). + +### Options + ##### indent Type: `string | number`\ diff --git a/source/add-remove-dependencies.js b/source/add-remove-dependencies.js new file mode 100644 index 0000000..6a6b046 --- /dev/null +++ b/source/add-remove-dependencies.js @@ -0,0 +1,83 @@ +import path from 'node:path'; +import {writeJsonFile, writeJsonFileSync} from 'write-json-file'; +import {readPackage, readPackageSync} from 'read-pkg'; +import {updatePackage, updatePackageSync} from './update-package.js'; +import {sanitize, normalize, hasMultipleDependencyTypes} from './util.js'; + +export async function addPackageDependencies(filePath, dependencies, options) { + return hasMultipleDependencyTypes(dependencies) + ? updatePackage(filePath, {...dependencies}, options) + : updatePackage(filePath, {dependencies}, options); +} + +export function addPackageDependenciesSync(filePath, dependencies, options) { + return hasMultipleDependencyTypes(dependencies) + ? updatePackageSync(filePath, {...dependencies}, options) + : updatePackageSync(filePath, {dependencies}, options); +} + +export async function removePackageDependencies(filePath, dependencies, options) { + ({filePath, data: dependencies, options} = sanitize(filePath, dependencies, options, {sanitizeData: false})); + + let pkg; + + try { + pkg = await readPackage({cwd: path.dirname(filePath), normalize: false}); + } catch (error) { + // 'package.json' doesn't exist + if (error.code === 'ENOENT') { + return; + } + + throw error; + } + + if (Array.isArray(dependencies)) { + for (const dependency of dependencies) { + delete pkg.dependencies[dependency]; + } + } else { + for (const [dependencyKey, dependency] of Object.entries(dependencies)) { + delete pkg[dependencyKey][dependency]; + } + } + + if (options.normalize) { + pkg = normalize(pkg); + } + + return writeJsonFile(filePath, pkg, options); +} + +export function removePackageDependenciesSync(filePath, dependencies, options) { + ({filePath, data: dependencies, options} = sanitize(filePath, dependencies, options, {sanitizeData: false})); + + let pkg; + + try { + pkg = readPackageSync({cwd: path.dirname(filePath), normalize: false}); + } catch (error) { + // 'package.json' doesn't exist + if (error.code === 'ENOENT') { + return; + } + + throw error; + } + + if (Array.isArray(dependencies)) { + for (const dependency of dependencies) { + delete pkg.dependencies[dependency]; + } + } else { + for (const [dependencyKey, dependency] of Object.entries(dependencies)) { + delete pkg[dependencyKey][dependency]; + } + } + + if (options.normalize) { + pkg = normalize(pkg); + } + + writeJsonFileSync(filePath, pkg, options); +} diff --git a/source/update-package.js b/source/update-package.js new file mode 100644 index 0000000..e5aa319 --- /dev/null +++ b/source/update-package.js @@ -0,0 +1,56 @@ +import path from 'node:path'; +import {writeJsonFile, writeJsonFileSync} from 'write-json-file'; +import {readPackage, readPackageSync} from 'read-pkg'; +import {deepmerge} from 'deepmerge-ts'; +import {sanitize, normalize} from './util.js'; + +export async function updatePackage(filePath, data, options) { + ({filePath, data, options} = sanitize(filePath, data, options)); + + let pkg; + + try { + pkg = await readPackage({cwd: path.dirname(filePath), normalize: false}); + } catch (error) { + // 'package.json' doesn't exist + if (error.code === 'ENOENT') { + return writeJsonFile(filePath, data, options); + } + + throw error; + } + + pkg = deepmerge(pkg, data); + + if (options.normalize) { + pkg = normalize(pkg); + } + + return writeJsonFile(filePath, pkg, options); +} + +export function updatePackageSync(filePath, data, options) { + ({filePath, data, options} = sanitize(filePath, data, options)); + + let pkg; + + try { + pkg = readPackageSync({cwd: path.dirname(filePath), normalize: false}); + } catch (error) { + // 'package.json' doesn't exist + if (error.code === 'ENOENT') { + writeJsonFileSync(filePath, data, options); + return; + } + + throw error; + } + + pkg = deepmerge(pkg, data); + + if (options.normalize) { + pkg = normalize(pkg); + } + + writeJsonFileSync(filePath, pkg, options); +} diff --git a/source/util.js b/source/util.js new file mode 100644 index 0000000..068ede8 --- /dev/null +++ b/source/util.js @@ -0,0 +1,47 @@ +import path from 'node:path'; +import sortKeys from 'sort-keys'; + +export const dependencyKeys = new Set([ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', +]); + +export const hasMultipleDependencyTypes = dependencies => Object.keys(dependencies).some(key => dependencyKeys.has(key)); + +export function normalize(packageJson) { + const result = {}; + + for (const key of Object.keys(packageJson)) { + if (!dependencyKeys.has(key)) { + result[key] = packageJson[key]; + } else if (Object.keys(packageJson[key]).length > 0) { + result[key] = sortKeys(packageJson[key]); + } + } + + return result; +} + +export function sanitize(filePath, data, options, {sanitizeData = true} = {}) { + if (typeof filePath !== 'string') { + options = data; + data = filePath; + filePath = '.'; + } + + options = { + normalize: true, + ...options, + detectIndent: true, + }; + + filePath = path.basename(filePath) === 'package.json' ? filePath : path.join(filePath, 'package.json'); + + if (options.normalize && sanitizeData) { + data = normalize(data); + } + + return {filePath, data, options}; +} diff --git a/source/write-package.js b/source/write-package.js new file mode 100644 index 0000000..6e7af48 --- /dev/null +++ b/source/write-package.js @@ -0,0 +1,12 @@ +import {writeJsonFile, writeJsonFileSync} from 'write-json-file'; +import {sanitize} from './util.js'; + +export async function writePackage(filePath, data, options) { + ({filePath, data, options} = sanitize(filePath, data, options)); + return writeJsonFile(filePath, data, options); +} + +export function writePackageSync(filePath, data, options) { + ({filePath, data, options} = sanitize(filePath, data, options)); + writeJsonFileSync(filePath, data, options); +} diff --git a/test/add-dependencies.js b/test/add-dependencies.js new file mode 100644 index 0000000..f6f78de --- /dev/null +++ b/test/add-dependencies.js @@ -0,0 +1,272 @@ +import path from 'node:path'; +import fs, {promises as fsPromises} from 'node:fs'; +import test from 'ava'; +import {pick} from 'filter-anything'; +import {temporaryDirectory} from 'tempy'; +import {readPackage, readPackageSync} from 'read-pkg'; +import {writePackage, writePackageSync, addPackageDependencies, addPackageDependenciesSync} from '../index.js'; + +const fixture = { + foo: true, + scripts: { + b: '1', + a: '1', + }, + dependencies: { + foo: '1.0.0', + bar: '1.0.0', + }, + devDependencies: { + foo: '1.0.0', + bar: '1.0.0', + }, + optionalDependencies: { + foo: '1.0.0', + bar: '1.0.0', + }, + peerDependencies: { + foo: '1.0.0', + bar: '1.0.0', + }, +}; + +const emptyPropFixture = { + foo: true, + dependencies: {}, + devDependencies: {}, + optionalDependencies: {}, + peerDependencies: {}, +}; + +const addFixture = { + dependencies: { + baz: '1.0.0', + }, + devDependencies: { + baz: '1.0.0', + }, + optionalDependencies: { + baz: '1.0.0', + }, + peerDependencies: { + baz: '1.0.0', + }, +}; + +test('async', async t => { + const temporary = temporaryDirectory(); + await writePackage(temporary, fixture); + await addPackageDependencies(temporary, addFixture.dependencies); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.true(packageJson.foo); + + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'foo']); + t.deepEqual(Object.keys(packageJson.optionalDependencies), ['bar', 'foo']); + t.deepEqual(Object.keys(packageJson.peerDependencies), ['bar', 'foo']); +}); + +test('async - multiple types', async t => { + const temporary = temporaryDirectory(); + await writePackage(temporary, fixture); + await addPackageDependencies(temporary, addFixture); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.optionalDependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.peerDependencies), ['bar', 'baz', 'foo']); +}); + +test('async - two types', async t => { + const temporary = temporaryDirectory(); + await writePackage(temporary, fixture); + await addPackageDependencies(temporary, pick(addFixture, ['dependencies', 'devDependencies'])); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.optionalDependencies), ['bar', 'foo']); + t.deepEqual(Object.keys(packageJson.peerDependencies), ['bar', 'foo']); +}); + +test('async - two types with empty', async t => { + const temporary = temporaryDirectory(); + await writePackage(temporary, pick(fixture, ['foo', 'scripts', 'dependencies', 'devDependencies'])); + await addPackageDependencies(temporary, pick(addFixture, ['dependencies', 'devDependencies'])); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'baz', 'foo']); + t.falsy(packageJson.optionalDependencies); + t.falsy(packageJson.peerDependencies); +}); + +test('async - overwrite dependency', async t => { + const temporary = temporaryDirectory(); + await writePackage(temporary, fixture); + await addPackageDependencies(temporary, {bar: '2.0.0'}); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'foo']); + t.is(packageJson.dependencies.bar, '2.0.0'); +}); + +test('async - allow not removing empty dependency properties', async t => { + const temporary = temporaryDirectory(); + await writePackage(temporary, emptyPropFixture, {normalize: false}); + await addPackageDependencies(temporary, pick(addFixture, ['dependencies', 'devDependencies']), {normalize: false}); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.dependencies), ['baz']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['baz']); + t.truthy(packageJson.optionalDependencies); + t.truthy(packageJson.peerDependencies); +}); + +test('async - create package.json if one does not exist', async t => { + const temporary = temporaryDirectory(); + await addPackageDependencies(temporary, addFixture); + const { + dependencies, + devDependencies, + optionalDependencies, + peerDependencies, + ...rest + } = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(Object.keys(dependencies), ['baz']); + t.deepEqual(Object.keys(devDependencies), ['baz']); + t.deepEqual(Object.keys(optionalDependencies), ['baz']); + t.deepEqual(Object.keys(peerDependencies), ['baz']); + t.true(Object.keys(rest).length === 0, 'package.json had additional fields!'); +}); + +test('sync', t => { + const temporary = temporaryDirectory(); + writePackageSync(temporary, fixture); + addPackageDependenciesSync(temporary, addFixture.dependencies); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.true(packageJson.foo); + + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'foo']); + t.deepEqual(Object.keys(packageJson.optionalDependencies), ['bar', 'foo']); + t.deepEqual(Object.keys(packageJson.peerDependencies), ['bar', 'foo']); +}); + +test('sync - multiple types', t => { + const temporary = temporaryDirectory(); + writePackageSync(temporary, fixture); + addPackageDependenciesSync(temporary, addFixture); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.optionalDependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.peerDependencies), ['bar', 'baz', 'foo']); +}); + +test('sync - two types', t => { + const temporary = temporaryDirectory(); + writePackageSync(temporary, fixture); + addPackageDependenciesSync(temporary, pick(addFixture, ['dependencies', 'devDependencies'])); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.optionalDependencies), ['bar', 'foo']); + t.deepEqual(Object.keys(packageJson.peerDependencies), ['bar', 'foo']); +}); + +test('sync - two types with empty', t => { + const temporary = temporaryDirectory(); + writePackageSync(temporary, pick(fixture, ['foo', 'scripts', 'dependencies', 'devDependencies'])); + addPackageDependenciesSync(temporary, pick(addFixture, ['dependencies', 'devDependencies'])); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.scripts), ['b', 'a']); + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'baz', 'foo']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['bar', 'baz', 'foo']); + t.falsy(packageJson.optionalDependencies); + t.falsy(packageJson.peerDependencies); +}); + +test('sync - overwrite dependency', t => { + const temporary = temporaryDirectory(); + writePackageSync(temporary, fixture); + addPackageDependenciesSync(temporary, {bar: '2.0.0'}); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(Object.keys(packageJson.dependencies), ['bar', 'foo']); + t.is(packageJson.dependencies.bar, '2.0.0'); +}); + +test('sync - allow not removing empty dependency properties', t => { + const temporary = temporaryDirectory(); + writePackageSync(temporary, emptyPropFixture, {normalize: false}); + addPackageDependenciesSync(temporary, pick(addFixture, ['dependencies', 'devDependencies']), {normalize: false}); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.deepEqual(Object.keys(packageJson.dependencies), ['baz']); + t.deepEqual(Object.keys(packageJson.devDependencies), ['baz']); + t.truthy(packageJson.optionalDependencies); + t.truthy(packageJson.peerDependencies); +}); + +test('sync - create package.json if one does not exist', t => { + const temporary = temporaryDirectory(); + addPackageDependenciesSync(temporary, addFixture); + const { + dependencies, + devDependencies, + optionalDependencies, + peerDependencies, + ...rest + } = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(Object.keys(dependencies), ['baz']); + t.deepEqual(Object.keys(devDependencies), ['baz']); + t.deepEqual(Object.keys(optionalDependencies), ['baz']); + t.deepEqual(Object.keys(peerDependencies), ['baz']); + t.true(Object.keys(rest).length === 0, 'package.json had additional fields!'); +}); + +const missingEndingBraceFixture = '{"name": "foo", "dependencies": {"bar": "1.0.0"}'; + +test('async - invalid package.json', async t => { + const temporary = temporaryDirectory(); + await fsPromises.writeFile(path.join(temporary, 'package.json'), missingEndingBraceFixture); + + await t.throwsAsync( + addPackageDependencies(temporary, addFixture.dependencies), + {name: 'JSONError'}, + ); +}); + +test('sync - invalid package.json', t => { + const temporary = temporaryDirectory(); + fs.writeFileSync(path.join(temporary, 'package.json'), missingEndingBraceFixture); + + t.throws( + () => addPackageDependenciesSync(temporary, addFixture.dependencies), + {name: 'JSONError'}, + ); +}); diff --git a/test/remove-dependencies.js b/test/remove-dependencies.js new file mode 100644 index 0000000..c664db9 --- /dev/null +++ b/test/remove-dependencies.js @@ -0,0 +1,223 @@ +import path from 'node:path'; +import fs, {promises as fsPromises} from 'node:fs'; +import test from 'ava'; +import {omit} from 'filter-anything'; +import {temporaryDirectory} from 'tempy'; +import {readPackage, readPackageSync} from 'read-pkg'; +import {removePackageDependencies, removePackageDependenciesSync, writePackage, writePackageSync} from '../index.js'; + +const emptyPropFixture = { + foo: true, + dependencies: {}, + devDependencies: {}, + optionalDependencies: {}, + peerDependencies: {}, +}; + +const missingEndingBraceFixture = '{"name": "foo", "dependencies": {"bar": "1.0.0"}'; + +test('async', async t => { + const temporary = temporaryDirectory(); + + await writePackage(temporary, {name: 'foo', dependencies: {foo: '1.0.0'}}); + await removePackageDependencies(temporary, ['foo']); + + const packageJson = await readPackage({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, {name: 'foo'}); +}); + +test('async - multiple types', async t => { + const temporary = temporaryDirectory(); + + const fixture = { + name: 'foo', + dependencies: {foo: '1.0.0'}, + devDependencies: {bar: '1.0.0', baz: '1.0.0'}, + peerDependencies: {foobar: '1.0.0'}, + }; + + await writePackage(temporary, fixture); + + await removePackageDependencies(temporary, ['foo']); + let packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, omit(fixture, ['dependencies'])); + + await removePackageDependencies(temporary, {devDependencies: ['bar']}); + packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, omit(fixture, ['dependencies', 'devDependencies.bar'])); +}); + +test('async - should not throw if package.json does not exist', async t => { + const temporary = temporaryDirectory(); + await t.notThrowsAsync(removePackageDependencies(temporary, ['foo'])); +}); + +test('async - removing non-existent dependencies should not throw', async t => { + const temporary = temporaryDirectory(); + const fixture = {name: 'foo', dependencies: {foo: '1.0.0'}}; + + await writePackage(temporary, fixture); + await removePackageDependencies(temporary, ['bar', 'baz', 'foobar']); + + const packageJson = await readPackage({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, fixture); +}); + +test('async - removes empty dependency properties by default', async t => { + const temporary = temporaryDirectory(); + + await writePackage(temporary, emptyPropFixture, {normalize: false}); + await removePackageDependencies(temporary, []); + + const packageJson = await readPackage({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, {foo: true}); +}); + +test('async - allow not removing empty dependency properties', async t => { + const temporary = temporaryDirectory(); + + await writePackage(temporary, emptyPropFixture, {normalize: false}); + await removePackageDependencies(temporary, [], {normalize: false}); + + const packageJson = await readPackage({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, emptyPropFixture); +}); + +test('async - detect tab indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + await writePackage(temporary, {name: 'foo', dependencies: {foo: '1.0.0'}}, {indent: '\t'}); + await removePackageDependencies(temporary, ['foo']); + + t.is( + await fsPromises.readFile(temporary, 'utf8'), + '{\n\t"name": "foo"\n}\n', + ); +}); + +test('async - detect 2 spaces indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + await writePackage(temporary, {name: 'foo', dependencies: {foo: '1.0.0'}}, {indent: 2}); + await removePackageDependencies(temporary, ['foo']); + + t.is( + await fsPromises.readFile(temporary, 'utf8'), + '{\n "name": "foo"\n}\n', + ); +}); + +test('async - invalid package.json should throw', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + await fsPromises.writeFile(temporary, missingEndingBraceFixture); + + await t.throwsAsync( + removePackageDependencies(temporary, ['bar']), + {name: 'JSONError'}, + ); +}); + +test('sync', t => { + const temporary = temporaryDirectory(); + + writePackageSync(temporary, {name: 'foo', dependencies: {foo: '1.0.0'}}); + removePackageDependenciesSync(temporary, ['foo']); + + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, {name: 'foo'}); +}); + +test('sync - multiple types', t => { + const temporary = temporaryDirectory(); + + const fixture = { + name: 'foo', + dependencies: {foo: '1.0.0'}, + devDependencies: {bar: '1.0.0', baz: '1.0.0'}, + peerDependencies: {foobar: '1.0.0'}, + }; + + writePackageSync(temporary, fixture); + + removePackageDependenciesSync(temporary, ['foo']); + let packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, omit(fixture, ['dependencies'])); + + removePackageDependenciesSync(temporary, {devDependencies: ['bar']}); + packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, omit(fixture, ['dependencies', 'devDependencies.bar'])); +}); + +test('sync - should not throw if package.json does not exist', t => { + const temporary = temporaryDirectory(); + t.notThrows(() => removePackageDependenciesSync(temporary, ['foo'])); +}); + +test('sync - removing non-existent dependencies should not throw', t => { + const temporary = temporaryDirectory(); + const fixture = {name: 'foo', dependencies: {foo: '1.0.0'}}; + + writePackageSync(temporary, fixture); + removePackageDependenciesSync(temporary, ['bar', 'baz', 'foobar']); + + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, fixture); +}); + +test('sync - removes empty dependency properties by default', t => { + const temporary = temporaryDirectory(); + + writePackageSync(temporary, emptyPropFixture, {normalize: false}); + removePackageDependenciesSync(temporary, []); + + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, {foo: true}); +}); + +test('sync - allow not removing empty dependency properties', t => { + const temporary = temporaryDirectory(); + + writePackageSync(temporary, emptyPropFixture, {normalize: false}); + removePackageDependenciesSync(temporary, [], {normalize: false}); + + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + t.deepEqual(packageJson, emptyPropFixture); +}); + +test('sync - detect tab indent', t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + writePackageSync(temporary, {name: 'foo', dependencies: {foo: '1.0.0'}}, {indent: '\t'}); + removePackageDependenciesSync(temporary, ['foo']); + + t.is( + fs.readFileSync(temporary, 'utf8'), + '{\n\t"name": "foo"\n}\n', + ); +}); + +test('sync - detect 2 spaces indent', t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + writePackageSync(temporary, {name: 'foo', dependencies: {foo: '1.0.0'}}, {indent: 2}); + removePackageDependenciesSync(temporary, ['foo']); + + t.is( + fs.readFileSync(temporary, 'utf8'), + '{\n "name": "foo"\n}\n', + ); +}); + +test('sync - invalid package.json should throw', t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + fs.writeFileSync(temporary, missingEndingBraceFixture); + + t.throws( + () => removePackageDependenciesSync(temporary, ['bar']), + {name: 'JSONError'}, + ); +}); diff --git a/test/update-package.js b/test/update-package.js new file mode 100644 index 0000000..43a4174 --- /dev/null +++ b/test/update-package.js @@ -0,0 +1,254 @@ +import path from 'node:path'; +import fs, {promises as fsPromises} from 'node:fs'; +import test from 'ava'; +import {temporaryDirectory} from 'tempy'; +import {readPackage, readPackageSync} from 'read-pkg'; +import {writePackage, writePackageSync, updatePackage, updatePackageSync} from '../index.js'; + +const emptyPropFixture = { + foo: true, + dependencies: {}, + devDependencies: {}, + optionalDependencies: {}, + peerDependencies: {}, +}; + +const missingEndingBraceFixture = '{"name": "foo", "dependencies": {"bar": "1.0.0"}'; + +test('async', async t => { + const temporary = temporaryDirectory(); + + await writePackage(temporary, {name: 'foo', version: '1.0.0'}); + await updatePackage(temporary, {version: '2.0.0', license: 'MIT'}); + + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, { + name: 'foo', + version: '2.0.0', + license: 'MIT', + }); +}); + +test('async - create package.json if one does not exist', async t => { + const temporary = temporaryDirectory(); + + await updatePackage(temporary, {name: 'foo', version: '1.0.0'}); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, { + name: 'foo', + version: '1.0.0', + }); +}); + +test('async - merge dependencies', async t => { + const temporary = temporaryDirectory(); + + await updatePackage(temporary, { + name: 'foo', + dependencies: { + foo: '1.0.0', + bar: '1.0.0', + }, + }); + + await updatePackage(temporary, { + dependencies: { + foo: '2.0.0', + baz: '1.0.0', + }, + devDependencies: { + foobar: '1.0.0', + }, + }); + + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, { + name: 'foo', + dependencies: { + foo: '2.0.0', + bar: '1.0.0', + baz: '1.0.0', + }, + devDependencies: { + foobar: '1.0.0', + }, + }); +}); + +test('async - removes empty dependency properties by default', async t => { + const temporary = temporaryDirectory(); + + await updatePackage(temporary, emptyPropFixture); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.falsy(packageJson.dependencies); + t.falsy(packageJson.devDependencies); + t.falsy(packageJson.optionalDependencies); + t.falsy(packageJson.peerDependencies); +}); + +test('async - allow not removing empty dependency properties', async t => { + const temporary = temporaryDirectory(); + + await updatePackage(temporary, emptyPropFixture, {normalize: false}); + const packageJson = await readPackage({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, emptyPropFixture); +}); + +test('async - detect tab indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + await writePackage(temporary, {foo: true, bar: true}, {indent: '\t'}); + await updatePackage(temporary, {foo: false, foobar: true}); + + t.is( + await fsPromises.readFile(temporary, 'utf8'), + '{\n\t"foo": false,\n\t"bar": true,\n\t"foobar": true\n}\n', + ); +}); + +test('async - detect 2 spaces indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + await writePackage(temporary, {foo: true, bar: true}, {indent: 2}); + await updatePackage(temporary, {foo: false, foobar: true}); + + t.is( + await fsPromises.readFile(temporary, 'utf8'), + '{\n "foo": false,\n "bar": true,\n "foobar": true\n}\n', + ); +}); + +test('async - invalid package.json should throw', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + await fsPromises.writeFile(temporary, missingEndingBraceFixture); + + await t.throwsAsync( + updatePackage(temporary, {version: '1.0.0'}), + {name: 'JSONError'}, + ); +}); + +test('sync', t => { + const temporary = temporaryDirectory(); + + writePackageSync(temporary, {name: 'foo', version: '1.0.0'}); + updatePackageSync(temporary, {version: '2.0.0', license: 'MIT'}); + + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, { + name: 'foo', + version: '2.0.0', + license: 'MIT', + }); +}); + +test('sync - create package.json if one does not exist', t => { + const temporary = temporaryDirectory(); + + updatePackageSync(temporary, {name: 'foo', version: '1.0.0'}); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, { + name: 'foo', + version: '1.0.0', + }); +}); + +test('sync - merge dependencies', t => { + const temporary = temporaryDirectory(); + + updatePackageSync(temporary, { + name: 'foo', + dependencies: { + foo: '1.0.0', + bar: '1.0.0', + }, + }); + + updatePackageSync(temporary, { + dependencies: { + foo: '2.0.0', + baz: '1.0.0', + }, + devDependencies: { + foobar: '1.0.0', + }, + }); + + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, { + name: 'foo', + dependencies: { + foo: '2.0.0', + bar: '1.0.0', + baz: '1.0.0', + }, + devDependencies: { + foobar: '1.0.0', + }, + }); +}); + +test('sync - removes empty dependency properties by default', t => { + const temporary = temporaryDirectory(); + + updatePackageSync(temporary, emptyPropFixture); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.true(packageJson.foo); + t.falsy(packageJson.dependencies); + t.falsy(packageJson.devDependencies); + t.falsy(packageJson.optionalDependencies); + t.falsy(packageJson.peerDependencies); +}); + +test('sync - allow not removing empty dependency properties', t => { + const temporary = temporaryDirectory(); + + updatePackageSync(temporary, emptyPropFixture, {normalize: false}); + const packageJson = readPackageSync({cwd: temporary, normalize: false}); + + t.deepEqual(packageJson, emptyPropFixture); +}); + +test('sync - detect tab indent', t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + writePackageSync(temporary, {foo: true, bar: true}, {indent: '\t'}); + updatePackageSync(temporary, {foo: false, foobar: true}); + + t.is( + fs.readFileSync(temporary, 'utf8'), + '{\n\t"foo": false,\n\t"bar": true,\n\t"foobar": true\n}\n', + ); +}); + +test('sync - detect 2 spaces indent', t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + + writePackageSync(temporary, {foo: true, bar: true}, {indent: 2}); + updatePackageSync(temporary, {foo: false, foobar: true}); + + t.is( + fs.readFileSync(temporary, 'utf8'), + '{\n "foo": false,\n "bar": true,\n "foobar": true\n}\n', + ); +}); + +test('sync - invalid package.json should throw', t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); + fs.writeFileSync(temporary, missingEndingBraceFixture); + + t.throws( + () => updatePackageSync(temporary, {version: '1.0.0'}), + {name: 'JSONError'}, + ); +}); diff --git a/test.js b/test/write-package.js similarity index 74% rename from test.js rename to test/write-package.js index 0b69643..25a9ad9 100644 --- a/test.js +++ b/test/write-package.js @@ -1,10 +1,10 @@ -import fs from 'node:fs'; +import fs, {promises as fsPromises} from 'node:fs'; import path from 'node:path'; import test from 'ava'; -import tempfile from 'tempfile'; +import {temporaryDirectory} from 'tempy'; import {readPackage, readPackageSync} from 'read-pkg'; import {writeJsonFile} from 'write-json-file'; -import {writePackage, writePackageSync} from './index.js'; +import {writePackage, writePackageSync} from '../index.js'; const fixture = { foo: true, @@ -31,7 +31,7 @@ const fixture = { }; test('async', async t => { - const temporary = tempfile(); + const temporary = temporaryDirectory(); await writePackage(temporary, fixture); const packageJson = await readPackage({cwd: temporary, normalize: false}); t.true(packageJson.foo); @@ -43,7 +43,7 @@ test('async', async t => { }); test('sync', t => { - const temporary = tempfile(); + const temporary = temporaryDirectory(); writePackageSync(temporary, fixture); const packageJson = readPackageSync({cwd: temporary, normalize: false}); t.true(packageJson.foo); @@ -62,8 +62,8 @@ const emptyPropFixture = { peerDependencies: {}, }; -test('removes empty dependency properties by default', async t => { - const temporary = tempfile(); +test('async - removes empty dependency properties by default', async t => { + const temporary = temporaryDirectory(); await writePackage(temporary, emptyPropFixture); const packageJson = await readPackage({cwd: temporary, normalize: false}); t.true(packageJson.foo); @@ -73,8 +73,8 @@ test('removes empty dependency properties by default', async t => { t.falsy(packageJson.peerDependencies); }); -test('removes empty dependency properties sync by default', t => { - const temporary = tempfile(); +test('sync - removes empty dependency properties by default', t => { + const temporary = temporaryDirectory(); writePackageSync(temporary, emptyPropFixture); const packageJson = readPackageSync({cwd: temporary, normalize: false}); t.true(packageJson.foo); @@ -84,8 +84,8 @@ test('removes empty dependency properties sync by default', t => { t.falsy(packageJson.peerDependencies); }); -test('allow not removing empty dependency properties', async t => { - const temporary = tempfile(); +test('async - allow not removing empty dependency properties', async t => { + const temporary = temporaryDirectory(); await writePackage(temporary, emptyPropFixture, {normalize: false}); const packageJson = await readPackage({cwd: temporary, normalize: false}); t.true(packageJson.foo); @@ -95,8 +95,8 @@ test('allow not removing empty dependency properties', async t => { t.truthy(packageJson.peerDependencies); }); -test('allow not removing empty dependency properties sync', t => { - const temporary = tempfile(); +test('sync - allow not removing empty dependency properties', t => { + const temporary = temporaryDirectory(); writePackageSync(temporary, emptyPropFixture, {normalize: false}); const packageJson = readPackageSync({cwd: temporary, normalize: false}); t.true(packageJson.foo); @@ -106,38 +106,38 @@ test('allow not removing empty dependency properties sync', t => { t.truthy(packageJson.peerDependencies); }); -test('detect tab indent', async t => { - const temporary = path.join(tempfile(), 'package.json'); +test('async - detect tab indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); await writeJsonFile(temporary, {foo: true}, {indent: '\t'}); await writePackage(temporary, {foo: true, bar: true, foobar: true}); t.is( - fs.readFileSync(temporary, 'utf8'), + await fsPromises.readFile(temporary, 'utf8'), '{\n\t"foo": true,\n\t"bar": true,\n\t"foobar": true\n}\n', ); }); -test('detect tab indent sync', async t => { - const temporary = path.join(tempfile(), 'package.json'); +test('sync - detect tab indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); await writeJsonFile(temporary, {foo: true}, {indent: '\t'}); writePackageSync(temporary, {foo: true, bar: true, foobar: true}); t.is( - fs.readFileSync(temporary, 'utf8'), + await fsPromises.readFile(temporary, 'utf8'), '{\n\t"foo": true,\n\t"bar": true,\n\t"foobar": true\n}\n', ); }); -test('detect 2 spaces indent', async t => { - const temporary = path.join(tempfile(), 'package.json'); +test('async - detect 2 spaces indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); await writeJsonFile(temporary, {foo: true}, {indent: 2}); await writePackage(temporary, {foo: true, bar: true, foobar: true}); t.is( - fs.readFileSync(temporary, 'utf8'), + await fsPromises.readFile(temporary, 'utf8'), '{\n "foo": true,\n "bar": true,\n "foobar": true\n}\n', ); }); -test('detect 2 spaces indent sync', async t => { - const temporary = path.join(tempfile(), 'package.json'); +test('sync - detect 2 spaces indent', async t => { + const temporary = path.join(temporaryDirectory(), 'package.json'); await writeJsonFile(temporary, {foo: true}, {indent: 2}); writePackageSync(temporary, {foo: true, bar: true, foobar: true}); t.is(