Skip to content

Commit fbbf401

Browse files
committed
feat: add normalize function
This brings in parity with `read-package-json-fast`, which is a normalization method intended for parsing package.json files inside of an existing node_modules. Tests were copied straight from that package to ensure functionality was compatible. The only tests that weren't copied were the ones testing down into the bin normalization. That is delegated to a subdependency so these tests only ensure that *some* normalization is happening. Eventually as we consolidate our package.json reading libs, bin normalization can live in this package and be tested here. Finally, the errors that this package was throwing now include the metadata from the original errors (such as code) instead of making new errors with no context.
1 parent 2c5aaaa commit fbbf401

5 files changed

Lines changed: 436 additions & 27 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,20 @@ const pkgJson = await PackageJson.load('./')
100100

101101
---
102102

103+
### `async PackageJson.normalize()`
104+
105+
Like `load` but intended for reading package.json files in a
106+
node_modules tree. Some light normalization is done to ensure that it
107+
is ready for use in `@npmcli/arborist`
108+
109+
---
110+
111+
### **static** `async PackageJson.normalize(path)`
112+
113+
Convenience static method like `load` but for calling `normalize`
114+
115+
---
116+
103117
### `PackageJson.update(content)`
104118

105119
Updates the contents of the `package.json` with the `content` provided.

lib/index.js

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
const fs = require('fs')
2-
const promisify = require('util').promisify
3-
const readFile = promisify(fs.readFile)
4-
const writeFile = promisify(fs.writeFile)
1+
const { readFile, writeFile } = require('fs/promises')
52
const { resolve } = require('path')
63
const updateDeps = require('./update-dependencies.js')
74
const updateScripts = require('./update-scripts.js')
85
const updateWorkspaces = require('./update-workspaces.js')
6+
const normalize = require('./normalize.js')
97

108
const parseJSON = require('json-parse-even-better-errors')
119

12-
const _filename = Symbol('filename')
13-
const _manifest = Symbol('manifest')
14-
const _readFileContent = Symbol('readFileContent')
15-
1610
// a list of handy specialized helper functions that take
1711
// care of special cases that are handled by the npm cli
1812
const knownSteps = new Set([
@@ -29,42 +23,54 @@ const knownKeys = new Set([
2923
])
3024

3125
class PackageJson {
26+
// default behavior, just loads and parses
3227
static async load (path) {
3328
return await new PackageJson(path).load()
3429
}
3530

31+
// read-package-json-fast compatible behavior
32+
static async normalize (path) {
33+
return await new PackageJson(path).normalize()
34+
}
35+
36+
#filename
37+
#path
38+
#manifest = {}
39+
#readFileContent = ''
40+
3641
constructor (path) {
37-
this[_filename] = resolve(path, 'package.json')
38-
this[_manifest] = {}
39-
this[_readFileContent] = ''
42+
this.#path = path
43+
this.#filename = resolve(path, 'package.json')
4044
}
4145

4246
async load () {
4347
try {
44-
this[_readFileContent] =
45-
await readFile(this[_filename], 'utf8')
48+
this.#readFileContent =
49+
await readFile(this.#filename, 'utf8')
4650
} catch (err) {
47-
throw new Error('package.json not found')
51+
err.message = `Could not read package.json: ${err}`
52+
throw err
4853
}
4954

5055
try {
51-
this[_manifest] =
52-
parseJSON(this[_readFileContent])
56+
this.#manifest =
57+
parseJSON(this.#readFileContent)
5358
} catch (err) {
54-
throw new Error(`Invalid package.json: ${err}`)
59+
err.message = `Invalid package.json: ${err}`
60+
throw err
5561
}
5662

5763
return this
5864
}
5965

6066
get content () {
61-
return this[_manifest]
67+
return this.#manifest
6268
}
6369

6470
update (content) {
6571
// validates both current manifest and content param
6672
const invalidContent =
67-
typeof this[_manifest] !== 'object'
73+
typeof this.#manifest !== 'object'
6874
|| typeof content !== 'object'
6975
if (invalidContent) {
7076
throw Object.assign(
@@ -74,13 +80,13 @@ class PackageJson {
7480
}
7581

7682
for (const step of knownSteps) {
77-
this[_manifest] = step({ content, originalContent: this[_manifest] })
83+
this.#manifest = step({ content, originalContent: this.#manifest })
7884
}
7985

8086
// unknown properties will just be overwitten
8187
for (const [key, value] of Object.entries(content)) {
8288
if (!knownKeys.has(key)) {
83-
this[_manifest][key] = value
89+
this.#manifest[key] = value
8490
}
8591
}
8692

@@ -91,19 +97,25 @@ class PackageJson {
9197
const {
9298
[Symbol.for('indent')]: indent,
9399
[Symbol.for('newline')]: newline,
94-
} = this[_manifest]
100+
} = this.#manifest
95101

96102
const format = indent === undefined ? ' ' : indent
97103
const eol = newline === undefined ? '\n' : newline
98104
const fileContent = `${
99-
JSON.stringify(this[_manifest], null, format)
105+
JSON.stringify(this.#manifest, null, format)
100106
}\n`
101107
.replace(/\n/g, eol)
102108

103-
if (fileContent.trim() !== this[_readFileContent].trim()) {
104-
return await writeFile(this[_filename], fileContent)
109+
if (fileContent.trim() !== this.#readFileContent.trim()) {
110+
return await writeFile(this.#filename, fileContent)
105111
}
106112
}
113+
114+
async normalize () {
115+
await this.load()
116+
await normalize(this)
117+
return this
118+
}
107119
}
108120

109121
module.exports = PackageJson

lib/normalize.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const normalizePackageBin = require('npm-normalize-package-bin')
2+
3+
const normalize = async (pkg) => {
4+
const data = pkg.content
5+
6+
// remove _attributes
7+
for (const key in data) {
8+
if (key.startsWith('_')) {
9+
delete pkg.content[key]
10+
}
11+
}
12+
13+
// _id
14+
if (data.name && data.version) {
15+
data._id = `${data.name}@${data.version}`
16+
}
17+
18+
// bundleDependencies
19+
if (data.bundleDependencies === undefined && data.bundledDependencies !== undefined) {
20+
data.bundleDependencies = data.bundledDependencies
21+
}
22+
delete data.bundledDependencies
23+
const bd = data.bundleDependencies
24+
if (bd === true) {
25+
data.bundleDependencies = Object.keys(data.dependencies || {})
26+
} else if (bd && typeof bd === 'object') {
27+
if (!Array.isArray(bd)) {
28+
data.bundleDependencies = Object.keys(bd)
29+
}
30+
} else {
31+
data.bundleDependencies = []
32+
}
33+
34+
// it was once common practice to list deps both in optionalDependencies and
35+
// in dependencies, to support npm versions that did not know about
36+
// optionalDependencies. This is no longer a relevant need, so duplicating
37+
// the deps in two places is unnecessary and excessive.
38+
if (data.dependencies &&
39+
data.optionalDependencies && typeof data.optionalDependencies === 'object') {
40+
for (const name in data.optionalDependencies) {
41+
delete data.dependencies[name]
42+
}
43+
if (!Object.keys(data.dependencies).length) {
44+
delete data.dependencies
45+
}
46+
}
47+
48+
// scripts
49+
if (typeof data.scripts === 'object') {
50+
for (const name in data.scripts) {
51+
if (typeof data.scripts[name] !== 'string') {
52+
delete data.scripts[name]
53+
}
54+
}
55+
} else {
56+
delete data.scripts
57+
}
58+
59+
// funding
60+
if (data.funding && typeof data.funding === 'string') {
61+
data.funding = { url: data.funding }
62+
}
63+
64+
// bin
65+
normalizePackageBin(data)
66+
}
67+
68+
module.exports = normalize

test/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ t.test('read missing package.json', async t => {
8989
const path = t.testdirName
9090
return t.rejects(
9191
PackageJson.load(path),
92-
/package.json not found/,
93-
'should throw package.json not found error'
92+
{
93+
message: /package.json/,
94+
code: 'ENOENT',
95+
}
9496
)
9597
})
9698

0 commit comments

Comments
 (0)