Skip to content

Commit 5f28de5

Browse files
authoredApr 7, 2023
Add parsePackage method, bump dependencies, target Node.js 16 (#29)
1 parent f50f5ff commit 5f28de5

File tree

8 files changed

+172
-40
lines changed

8 files changed

+172
-40
lines changed
 

‎.github/workflows/main.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ jobs:
1010
fail-fast: false
1111
matrix:
1212
node-version:
13+
- 18
1314
- 16
1415
steps:
15-
- uses: actions/checkout@v2
16-
- uses: actions/setup-node@v2
16+
- uses: actions/checkout@v3
17+
- uses: actions/setup-node@v3
1718
with:
1819
node-version: ${{ matrix.node-version }}
1920
- run: npm install

‎index.d.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import * as typeFest from 'type-fest';
2-
import * as normalize from 'normalize-package-data';
1+
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
2+
import type {PackageJson as typeFestPackageJson} from 'type-fest';
3+
import type {Package as normalizePackage} from 'normalize-package-data';
34

4-
export interface Options {
5+
export type Options = {
56
/**
67
Current working directory.
78
@@ -15,14 +16,20 @@ export interface Options {
1516
@default true
1617
*/
1718
readonly normalize?: boolean;
18-
}
19+
};
1920

20-
export interface NormalizeOptions extends Options {
21+
// eslint-disable-next-line @typescript-eslint/naming-convention
22+
type _NormalizeOptions = {
2123
readonly normalize?: true;
22-
}
24+
};
2325

24-
export type NormalizedPackageJson = PackageJson & normalize.Package;
25-
export type PackageJson = typeFest.PackageJson;
26+
export type NormalizeOptions = _NormalizeOptions & Options;
27+
28+
export type ParseOptions = Omit<Options, 'cwd'>;
29+
export type NormalizeParseOptions = _NormalizeOptions & ParseOptions;
30+
31+
export type NormalizedPackageJson = PackageJson & normalizePackage;
32+
export type PackageJson = typeFestPackageJson;
2633

2734
/**
2835
@returns The parsed JSON.
@@ -57,3 +64,6 @@ console.log(readPackageSync({cwd: 'some-other-directory'});
5764
*/
5865
export function readPackageSync(options?: NormalizeOptions): NormalizedPackageJson;
5966
export function readPackageSync(options: Options): PackageJson;
67+
68+
export function parsePackage(packageFile: PackageJson | string, options?: NormalizeParseOptions): NormalizedPackageJson;
69+
export function parsePackage(packageFile: PackageJson | string, options: ParseOptions): PackageJson;

‎index.js

+32-10
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,48 @@ import normalizePackageData from 'normalize-package-data';
77

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

10-
export async function readPackage({cwd, normalize = true} = {}) {
11-
cwd = toPath(cwd) || process.cwd();
12-
const filePath = path.resolve(cwd, 'package.json');
13-
const json = parseJson(await fsPromises.readFile(filePath, 'utf8'));
10+
const getPackagePath = cwd => {
11+
const packageDir = toPath(cwd) || process.cwd();
12+
return path.resolve(packageDir, 'package.json');
13+
};
14+
15+
const _readPackage = (file, normalize) => {
16+
const json = typeof file === 'string'
17+
? parseJson(file)
18+
: file;
1419

1520
if (normalize) {
1621
normalizePackageData(json);
1722
}
1823

1924
return json;
25+
};
26+
27+
export async function readPackage({cwd, normalize = true} = {}) {
28+
const packageFile = await fsPromises.readFile(getPackagePath(cwd), 'utf8');
29+
return _readPackage(packageFile, normalize);
2030
}
2131

2232
export function readPackageSync({cwd, normalize = true} = {}) {
23-
cwd = toPath(cwd) || process.cwd();
24-
const filePath = path.resolve(cwd, 'package.json');
25-
const json = parseJson(fs.readFileSync(filePath, 'utf8'));
33+
const packageFile = fs.readFileSync(getPackagePath(cwd), 'utf8');
34+
return _readPackage(packageFile, normalize);
35+
}
2636

27-
if (normalize) {
28-
normalizePackageData(json);
37+
export function parsePackage(packageFile, {normalize = true} = {}) {
38+
const isObject = packageFile !== null && typeof packageFile === 'object' && !Array.isArray(packageFile);
39+
const isString = typeof packageFile === 'string';
40+
41+
if (!isObject && !isString) {
42+
throw new TypeError('`packageFile` should be either an `object` or a `string`.');
2943
}
3044

31-
return json;
45+
// Input should not be modified - if `structuredClone` is available, do a deep clone, shallow otherwise
46+
// TODO: Remove shallow clone when targeting Node.js 18
47+
const clonedPackageFile = isObject
48+
? (globalThis.structuredClone === undefined
49+
? {...packageFile}
50+
: structuredClone(packageFile))
51+
: packageFile;
52+
53+
return _readPackage(clonedPackageFile, normalize);
3254
}

‎index.test-d.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import {expectType, expectError, expectAssignable} from 'tsd';
2-
import {readPackage, readPackageSync, NormalizedPackageJson, PackageJson} from './index.js';
2+
import {
3+
readPackage,
4+
readPackageSync,
5+
parsePackage,
6+
type NormalizedPackageJson,
7+
type PackageJson,
8+
} from './index.js';
39

410
expectError<NormalizedPackageJson>({});
511
expectAssignable<PackageJson>({});
@@ -19,3 +25,12 @@ expectType<PackageJson>(readPackageSync({normalize: false}));
1925
expectError<NormalizedPackageJson>(readPackageSync({normalize: false}));
2026
expectType<NormalizedPackageJson>(readPackageSync({cwd: '.'}));
2127
expectType<NormalizedPackageJson>(readPackageSync({cwd: new URL('file:///path/to/cwd/')}));
28+
29+
expectType<NormalizedPackageJson>(parsePackage(''));
30+
expectType<NormalizedPackageJson>(parsePackage({name: 'unicorn'}));
31+
expectType<NormalizedPackageJson>(parsePackage('', {normalize: true}));
32+
expectType<PackageJson>(parsePackage('', {normalize: false}));
33+
expectType<PackageJson>(parsePackage({name: 'unicorn'}, {normalize: false}));
34+
expectError(parsePackage());
35+
expectError<NormalizedPackageJson>(parsePackage('', {normalize: false}));
36+
expectError(parsePackage('', {cwd: '.'}));

‎package.json

+8-13
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
"type": "module",
1414
"exports": "./index.js",
1515
"engines": {
16-
"node": ">=12.20"
16+
"node": ">=16"
1717
},
1818
"scripts": {
19-
"test": "xo && ava && tsd"
19+
"test": "xo && tsd && cd test && ava"
2020
},
2121
"files": [
2222
"index.js",
@@ -35,18 +35,13 @@
3535
],
3636
"dependencies": {
3737
"@types/normalize-package-data": "^2.4.1",
38-
"normalize-package-data": "^3.0.2",
39-
"parse-json": "^5.2.0",
40-
"type-fest": "^2.0.0"
38+
"normalize-package-data": "^5.0.0",
39+
"parse-json": "^7.0.0",
40+
"type-fest": "^3.8.0"
4141
},
4242
"devDependencies": {
43-
"ava": "^3.15.0",
44-
"tsd": "^0.17.0",
45-
"xo": "^0.44.0"
46-
},
47-
"xo": {
48-
"ignores": [
49-
"test/test.js"
50-
]
43+
"ava": "^5.2.0",
44+
"tsd": "^0.28.1",
45+
"xo": "^0.54.0"
5146
}
5247
}

‎readme.md

+23
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,29 @@ Default: `true`
5353

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

56+
### parsePackage(packageFile, options?)
57+
58+
Parses an object or string into JSON.
59+
60+
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.
61+
62+
#### packageFile
63+
64+
Type: `object | string`\
65+
66+
An object or a stringified object to be parsed as a package.json.
67+
68+
#### options
69+
70+
Type: `object`
71+
72+
##### normalize
73+
74+
Type: `boolean`\
75+
Default: `true`
76+
77+
[Normalize](https://github.com/npm/normalize-package-data#what-normalization-currently-entails) the package data.
78+
5679
## Related
5780

5881
- [read-pkg-up](https://github.com/sindresorhus/read-pkg-up) - Read the closest package.json file

‎test/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "unicorn",
2+
"name": "unicorn ",
33
"version": "1.0.0",
44
"type": "module"
55
}

‎test/test.js

+71-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import {fileURLToPath, pathToFileURL} from 'url';
2-
import path from 'path';
1+
import {fileURLToPath, pathToFileURL} from 'node:url';
2+
import path from 'node:path';
33
import test from 'ava';
4-
import {readPackage, readPackageSync} from '../index.js';
4+
import {readPackage, readPackageSync, parsePackage} from '../index.js';
55

6-
const dirname = path.dirname(fileURLToPath(import.meta.url));
7-
process.chdir(dirname);
6+
const dirname = path.dirname(fileURLToPath(test.meta.file));
87
const rootCwd = path.join(dirname, '..');
98

109
test('async', async t => {
@@ -22,6 +21,11 @@ test('async - cwd option', async t => {
2221
);
2322
});
2423

24+
test('async - normalize option', async t => {
25+
const package_ = await readPackage({normalize: false});
26+
t.is(package_.name, 'unicorn ');
27+
});
28+
2529
test('sync', t => {
2630
const package_ = readPackageSync();
2731
t.is(package_.name, 'unicorn');
@@ -36,3 +40,65 @@ test('sync - cwd option', t => {
3640
package_,
3741
);
3842
});
43+
44+
test('sync - normalize option', t => {
45+
const package_ = readPackageSync({normalize: false});
46+
t.is(package_.name, 'unicorn ');
47+
});
48+
49+
const pkgJson = {
50+
name: 'unicorn ',
51+
version: '1.0.0',
52+
type: 'module',
53+
};
54+
55+
test('parsePackage - json input', t => {
56+
const package_ = parsePackage(pkgJson);
57+
t.is(package_.name, 'unicorn');
58+
t.deepEqual(
59+
readPackageSync(),
60+
package_,
61+
);
62+
});
63+
64+
test('parsePackage - string input', t => {
65+
const package_ = parsePackage(JSON.stringify(pkgJson));
66+
t.is(package_.name, 'unicorn');
67+
t.deepEqual(
68+
readPackageSync(),
69+
package_,
70+
);
71+
});
72+
73+
test('parsePackage - normalize option', t => {
74+
const package_ = parsePackage(pkgJson, {normalize: false});
75+
t.is(package_.name, 'unicorn ');
76+
t.deepEqual(
77+
readPackageSync({normalize: false}),
78+
package_,
79+
);
80+
});
81+
82+
test('parsePackage - errors on invalid input', t => {
83+
t.throws(
84+
() => parsePackage(['foo', 'bar']),
85+
{message: '`packageFile` should be either an `object` or a `string`.'},
86+
);
87+
88+
t.throws(
89+
() => parsePackage(null),
90+
{message: '`packageFile` should be either an `object` or a `string`.'},
91+
);
92+
93+
t.throws(
94+
() => parsePackage(() => ({name: 'unicorn'})),
95+
{message: '`packageFile` should be either an `object` or a `string`.'},
96+
);
97+
});
98+
99+
test('parsePackage - does not modify source object', t => {
100+
const pkgObject = {name: 'unicorn', version: '1.0.0'};
101+
const package_ = parsePackage(pkgObject);
102+
103+
t.not(pkgObject, package_);
104+
});

0 commit comments

Comments
 (0)
Please sign in to comment.