Skip to content

Commit

Permalink
Add parsePackage method, bump dependencies, target Node.js 16 (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy-mitchell committed Apr 7, 2023
1 parent f50f5ff commit 5f28de5
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 40 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/main.yml
Expand Up @@ -10,10 +10,11 @@ jobs:
fail-fast: false
matrix:
node-version:
- 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
Expand Down
26 changes: 18 additions & 8 deletions index.d.ts
@@ -1,7 +1,8 @@
import * as typeFest from 'type-fest';
import * as normalize from 'normalize-package-data';
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
import type {PackageJson as typeFestPackageJson} from 'type-fest';
import type {Package as normalizePackage} from 'normalize-package-data';

export interface Options {
export type Options = {
/**
Current working directory.
Expand All @@ -15,14 +16,20 @@ export interface Options {
@default true
*/
readonly normalize?: boolean;
}
};

export interface NormalizeOptions extends Options {
// eslint-disable-next-line @typescript-eslint/naming-convention
type _NormalizeOptions = {
readonly normalize?: true;
}
};

export type NormalizedPackageJson = PackageJson & normalize.Package;
export type PackageJson = typeFest.PackageJson;
export type NormalizeOptions = _NormalizeOptions & Options;

export type ParseOptions = Omit<Options, 'cwd'>;
export type NormalizeParseOptions = _NormalizeOptions & ParseOptions;

export type NormalizedPackageJson = PackageJson & normalizePackage;
export type PackageJson = typeFestPackageJson;

/**
@returns The parsed JSON.
Expand Down Expand Up @@ -57,3 +64,6 @@ console.log(readPackageSync({cwd: 'some-other-directory'});
*/
export function readPackageSync(options?: NormalizeOptions): NormalizedPackageJson;
export function readPackageSync(options: Options): PackageJson;

export function parsePackage(packageFile: PackageJson | string, options?: NormalizeParseOptions): NormalizedPackageJson;
export function parsePackage(packageFile: PackageJson | string, options: ParseOptions): PackageJson;
42 changes: 32 additions & 10 deletions index.js
Expand Up @@ -7,26 +7,48 @@ import normalizePackageData from 'normalize-package-data';

const toPath = urlOrPath => urlOrPath instanceof URL ? fileURLToPath(urlOrPath) : urlOrPath;

export async function readPackage({cwd, normalize = true} = {}) {
cwd = toPath(cwd) || process.cwd();
const filePath = path.resolve(cwd, 'package.json');
const json = parseJson(await fsPromises.readFile(filePath, 'utf8'));
const getPackagePath = cwd => {
const packageDir = toPath(cwd) || process.cwd();
return path.resolve(packageDir, 'package.json');
};

const _readPackage = (file, normalize) => {
const json = typeof file === 'string'
? parseJson(file)
: file;

if (normalize) {
normalizePackageData(json);
}

return json;
};

export async function readPackage({cwd, normalize = true} = {}) {
const packageFile = await fsPromises.readFile(getPackagePath(cwd), 'utf8');
return _readPackage(packageFile, normalize);
}

export function readPackageSync({cwd, normalize = true} = {}) {
cwd = toPath(cwd) || process.cwd();
const filePath = path.resolve(cwd, 'package.json');
const json = parseJson(fs.readFileSync(filePath, 'utf8'));
const packageFile = fs.readFileSync(getPackagePath(cwd), 'utf8');
return _readPackage(packageFile, normalize);
}

if (normalize) {
normalizePackageData(json);
export function parsePackage(packageFile, {normalize = true} = {}) {
const isObject = packageFile !== null && typeof packageFile === 'object' && !Array.isArray(packageFile);
const isString = typeof packageFile === 'string';

if (!isObject && !isString) {
throw new TypeError('`packageFile` should be either an `object` or a `string`.');
}

return json;
// Input should not be modified - if `structuredClone` is available, do a deep clone, shallow otherwise
// TODO: Remove shallow clone when targeting Node.js 18
const clonedPackageFile = isObject
? (globalThis.structuredClone === undefined
? {...packageFile}
: structuredClone(packageFile))
: packageFile;

return _readPackage(clonedPackageFile, normalize);
}
17 changes: 16 additions & 1 deletion index.test-d.ts
@@ -1,5 +1,11 @@
import {expectType, expectError, expectAssignable} from 'tsd';
import {readPackage, readPackageSync, NormalizedPackageJson, PackageJson} from './index.js';
import {
readPackage,
readPackageSync,
parsePackage,
type NormalizedPackageJson,
type PackageJson,
} from './index.js';

expectError<NormalizedPackageJson>({});
expectAssignable<PackageJson>({});
Expand All @@ -19,3 +25,12 @@ expectType<PackageJson>(readPackageSync({normalize: false}));
expectError<NormalizedPackageJson>(readPackageSync({normalize: false}));
expectType<NormalizedPackageJson>(readPackageSync({cwd: '.'}));
expectType<NormalizedPackageJson>(readPackageSync({cwd: new URL('file:///path/to/cwd/')}));

expectType<NormalizedPackageJson>(parsePackage(''));
expectType<NormalizedPackageJson>(parsePackage({name: 'unicorn'}));
expectType<NormalizedPackageJson>(parsePackage('', {normalize: true}));
expectType<PackageJson>(parsePackage('', {normalize: false}));
expectType<PackageJson>(parsePackage({name: 'unicorn'}, {normalize: false}));
expectError(parsePackage());
expectError<NormalizedPackageJson>(parsePackage('', {normalize: false}));
expectError(parsePackage('', {cwd: '.'}));
21 changes: 8 additions & 13 deletions package.json
Expand Up @@ -13,10 +13,10 @@
"type": "module",
"exports": "./index.js",
"engines": {
"node": ">=12.20"
"node": ">=16"
},
"scripts": {
"test": "xo && ava && tsd"
"test": "xo && tsd && cd test && ava"
},
"files": [
"index.js",
Expand All @@ -35,18 +35,13 @@
],
"dependencies": {
"@types/normalize-package-data": "^2.4.1",
"normalize-package-data": "^3.0.2",
"parse-json": "^5.2.0",
"type-fest": "^2.0.0"
"normalize-package-data": "^5.0.0",
"parse-json": "^7.0.0",
"type-fest": "^3.8.0"
},
"devDependencies": {
"ava": "^3.15.0",
"tsd": "^0.17.0",
"xo": "^0.44.0"
},
"xo": {
"ignores": [
"test/test.js"
]
"ava": "^5.2.0",
"tsd": "^0.28.1",
"xo": "^0.54.0"
}
}
23 changes: 23 additions & 0 deletions readme.md
Expand Up @@ -53,6 +53,29 @@ Default: `true`

[Normalize](https://github.com/npm/normalize-package-data#what-normalization-currently-entails) the package data.

### parsePackage(packageFile, options?)

Parses an object or string into JSON.

Note: `packageFile` is cloned using [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) to prevent modification to the input object. This function is available from Node.js 18 on. In environments without `structuredClone` (such as Node.js 16), a shallow spread is used instead, which can cause deep properties of the object to be modified. Consider cloning the object before using `parsePackage` if that's the case.

#### packageFile

Type: `object | string`\

An object or a stringified object to be parsed as a package.json.

#### options

Type: `object`

##### normalize

Type: `boolean`\
Default: `true`

[Normalize](https://github.com/npm/normalize-package-data#what-normalization-currently-entails) the package data.

## Related

- [read-pkg-up](https://github.com/sindresorhus/read-pkg-up) - Read the closest package.json file
Expand Down
2 changes: 1 addition & 1 deletion test/package.json
@@ -1,5 +1,5 @@
{
"name": "unicorn",
"name": "unicorn ",
"version": "1.0.0",
"type": "module"
}
76 changes: 71 additions & 5 deletions test/test.js
@@ -1,10 +1,9 @@
import {fileURLToPath, pathToFileURL} from 'url';
import path from 'path';
import {fileURLToPath, pathToFileURL} from 'node:url';
import path from 'node:path';
import test from 'ava';
import {readPackage, readPackageSync} from '../index.js';
import {readPackage, readPackageSync, parsePackage} from '../index.js';

const dirname = path.dirname(fileURLToPath(import.meta.url));
process.chdir(dirname);
const dirname = path.dirname(fileURLToPath(test.meta.file));
const rootCwd = path.join(dirname, '..');

test('async', async t => {
Expand All @@ -22,6 +21,11 @@ test('async - cwd option', async t => {
);
});

test('async - normalize option', async t => {
const package_ = await readPackage({normalize: false});
t.is(package_.name, 'unicorn ');
});

test('sync', t => {
const package_ = readPackageSync();
t.is(package_.name, 'unicorn');
Expand All @@ -36,3 +40,65 @@ test('sync - cwd option', t => {
package_,
);
});

test('sync - normalize option', t => {
const package_ = readPackageSync({normalize: false});
t.is(package_.name, 'unicorn ');
});

const pkgJson = {
name: 'unicorn ',
version: '1.0.0',
type: 'module',
};

test('parsePackage - json input', t => {
const package_ = parsePackage(pkgJson);
t.is(package_.name, 'unicorn');
t.deepEqual(
readPackageSync(),
package_,
);
});

test('parsePackage - string input', t => {
const package_ = parsePackage(JSON.stringify(pkgJson));
t.is(package_.name, 'unicorn');
t.deepEqual(
readPackageSync(),
package_,
);
});

test('parsePackage - normalize option', t => {
const package_ = parsePackage(pkgJson, {normalize: false});
t.is(package_.name, 'unicorn ');
t.deepEqual(
readPackageSync({normalize: false}),
package_,
);
});

test('parsePackage - errors on invalid input', t => {
t.throws(
() => parsePackage(['foo', 'bar']),
{message: '`packageFile` should be either an `object` or a `string`.'},
);

t.throws(
() => parsePackage(null),
{message: '`packageFile` should be either an `object` or a `string`.'},
);

t.throws(
() => parsePackage(() => ({name: 'unicorn'})),
{message: '`packageFile` should be either an `object` or a `string`.'},
);
});

test('parsePackage - does not modify source object', t => {
const pkgObject = {name: 'unicorn', version: '1.0.0'};
const package_ = parsePackage(pkgObject);

t.not(pkgObject, package_);
});

0 comments on commit 5f28de5

Please sign in to comment.