Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parsePackage method, bump dependencies, target Node 16 #29

Merged
merged 13 commits into from
Apr 7, 2023
Merged
6 changes: 4 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ jobs:
fail-fast: false
matrix:
node-version:
- 19
- 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
Original file line number Diff line number Diff line change
@@ -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;
41 changes: 31 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,47 @@ 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
tommy-mitchell marked this conversation as resolved.
Show resolved Hide resolved
const clonedPackageFile = isObject
? (structuredClone === undefined
? {...packageFile}
: structuredClone(packageFile))
: packageFile;

return _readPackage(clonedPackageFile, normalize);
}
17 changes: 16 additions & 1 deletion index.test-d.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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": "^6.0.2",
"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
Original file line number Diff line number Diff line change
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. In environments without `structuredClone`, 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.
tommy-mitchell marked this conversation as resolved.
Show resolved Hide resolved

#### packageFile

Type: `object | string`\

An object or a stringified object to be parsed as 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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "unicorn",
"name": "unicorn ",
"version": "1.0.0",
"type": "module"
}
76 changes: 71 additions & 5 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -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', async 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_);
});