@npmcli/package-json
Advanced tools
Comparing version 3.1.1 to 4.0.0
172
lib/index.js
@@ -37,3 +37,19 @@ const { readFile, writeFile } = require('fs/promises') | ||
// npm pkg fix | ||
static fixSteps = Object.freeze([ | ||
'binRefs', | ||
'bundleDependencies', | ||
'bundleDependenciesFalse', | ||
'fixNameField', | ||
'fixVersionField', | ||
'fixRepositoryField', | ||
'fixBinField', | ||
'fixDependencies', | ||
'fixScriptsField', | ||
'devDependencies', | ||
'scriptpath', | ||
]) | ||
static prepareSteps = Object.freeze([ | ||
'_id', | ||
'_attributes', | ||
@@ -56,10 +72,43 @@ 'bundledDependencies', | ||
// default behavior, just loads and parses | ||
static async load (path) { | ||
return await new PackageJson(path).load() | ||
// create a new empty package.json, so we can save at the given path even | ||
// though we didn't start from a parsed file | ||
static async create (path, opts = {}) { | ||
const p = new PackageJson() | ||
await p.create(path) | ||
if (opts.data) { | ||
return p.update(opts.data) | ||
} | ||
return p | ||
} | ||
// Loads a package.json at given path and JSON parses | ||
static async load (path, opts = {}) { | ||
const p = new PackageJson() | ||
// Avoid try/catch if we aren't going to create | ||
if (!opts.create) { | ||
return p.load(path) | ||
} | ||
try { | ||
return await p.load(path) | ||
} catch (err) { | ||
if (!err.message.startsWith('Could not read package.json')) { | ||
throw err | ||
} | ||
return await p.create(path) | ||
} | ||
} | ||
// npm pkg fix | ||
static async fix (path, opts) { | ||
const p = new PackageJson() | ||
await p.load(path, true) | ||
return p.fix(opts) | ||
} | ||
// read-package-json compatible behavior | ||
static async prepare (path, opts) { | ||
return await new PackageJson(path).prepare(opts) | ||
const p = new PackageJson() | ||
await p.load(path, true) | ||
return p.prepare(opts) | ||
} | ||
@@ -69,21 +118,18 @@ | ||
static async normalize (path, opts) { | ||
return await new PackageJson(path).normalize(opts) | ||
const p = new PackageJson() | ||
await p.load(path) | ||
return p.normalize(opts) | ||
} | ||
#filename | ||
#path | ||
#manifest = {} | ||
#manifest | ||
#readFileContent = '' | ||
#fromIndex = false | ||
#canSave = true | ||
constructor (path) { | ||
// Load content from given path | ||
async load (path, parseIndex) { | ||
this.#path = path | ||
this.#filename = resolve(path, 'package.json') | ||
} | ||
async load (parseIndex) { | ||
let parseErr | ||
try { | ||
this.#readFileContent = | ||
await readFile(this.#filename, 'utf8') | ||
this.#readFileContent = await readFile(this.filename, 'utf8') | ||
} catch (err) { | ||
@@ -98,3 +144,3 @@ err.message = `Could not read package.json: ${err}` | ||
if (parseErr) { | ||
const indexFile = resolve(this.#path, 'index.js') | ||
const indexFile = resolve(this.path, 'index.js') | ||
let indexFileContent | ||
@@ -107,12 +153,18 @@ try { | ||
try { | ||
this.#manifest = fromComment(indexFileContent) | ||
this.fromComment(indexFileContent) | ||
} catch (err) { | ||
throw parseErr | ||
} | ||
this.#fromIndex = true | ||
// This wasn't a package.json so prevent saving | ||
this.#canSave = false | ||
return this | ||
} | ||
return this.fromJSON(this.#readFileContent) | ||
} | ||
// Load data from a JSON string/buffer | ||
fromJSON (data) { | ||
try { | ||
this.#manifest = parseJSON(this.#readFileContent) | ||
this.#manifest = parseJSON(data) | ||
} catch (err) { | ||
@@ -125,2 +177,23 @@ err.message = `Invalid package.json: ${err}` | ||
// Load data from a comment | ||
// /**package { "name": "foo", "version": "1.2.3", ... } **/ | ||
fromComment (data) { | ||
data = data.split(/^\/\*\*package(?:\s|$)/m) | ||
if (data.length < 2) { | ||
throw new Error('File has no package in comments') | ||
} | ||
data = data[1] | ||
data = data.split(/\*\*\/$/m) | ||
if (data.length < 2) { | ||
throw new Error('File has no package in comments') | ||
} | ||
data = data[0] | ||
data = data.replace(/^\s*\*/mg, '') | ||
this.#manifest = parseJSON(data) | ||
return this | ||
} | ||
get content () { | ||
@@ -134,16 +207,23 @@ return this.#manifest | ||
get filename () { | ||
if (this.path) { | ||
return resolve(this.path, 'package.json') | ||
} | ||
return undefined | ||
} | ||
create (path) { | ||
this.#path = path | ||
this.#manifest = {} | ||
return this | ||
} | ||
// This should be the ONLY way to set content in the manifest | ||
update (content) { | ||
// validates both current manifest and content param | ||
const invalidContent = | ||
typeof this.#manifest !== 'object' | ||
|| typeof content !== 'object' | ||
if (invalidContent) { | ||
throw Object.assign( | ||
new Error(`Can't update invalid package.json data`), | ||
{ code: 'EPACKAGEJSONUPDATE' } | ||
) | ||
if (!this.content) { | ||
throw new Error('Can not update without content. Please `load` or `create`') | ||
} | ||
for (const step of knownSteps) { | ||
this.#manifest = step({ content, originalContent: this.#manifest }) | ||
this.#manifest = step({ content, originalContent: this.content }) | ||
} | ||
@@ -154,3 +234,3 @@ | ||
if (!knownKeys.has(key)) { | ||
this.#manifest[key] = value | ||
this.content[key] = value | ||
} | ||
@@ -163,3 +243,3 @@ } | ||
async save () { | ||
if (this.#fromIndex) { | ||
if (!this.#canSave) { | ||
throw new Error('No package.json to save to') | ||
@@ -170,3 +250,3 @@ } | ||
[Symbol.for('newline')]: newline, | ||
} = this.#manifest | ||
} = this.content | ||
@@ -176,3 +256,3 @@ const format = indent === undefined ? ' ' : indent | ||
const fileContent = `${ | ||
JSON.stringify(this.#manifest, null, format) | ||
JSON.stringify(this.content, null, format) | ||
}\n` | ||
@@ -182,3 +262,3 @@ .replace(/\n/g, eol) | ||
if (fileContent.trim() !== this.#readFileContent.trim()) { | ||
return await writeFile(this.#filename, fileContent) | ||
return await writeFile(this.filename, fileContent) | ||
} | ||
@@ -191,3 +271,2 @@ } | ||
} | ||
await this.load() | ||
await normalize(this, opts) | ||
@@ -201,27 +280,14 @@ return this | ||
} | ||
await this.load(true) | ||
await normalize(this, opts) | ||
return this | ||
} | ||
} | ||
// /**package { "name": "foo", "version": "1.2.3", ... } **/ | ||
function fromComment (data) { | ||
data = data.split(/^\/\*\*package(?:\s|$)/m) | ||
if (data.length < 2) { | ||
throw new Error('File has no package in comments') | ||
async fix (opts = {}) { | ||
// This one is not overridable | ||
opts.steps = this.constructor.fixSteps | ||
await normalize(this, opts) | ||
return this | ||
} | ||
data = data[1] | ||
data = data.split(/\*\*\/$/m) | ||
if (data.length < 2) { | ||
throw new Error('File has no package in comments') | ||
} | ||
data = data[0] | ||
data = data.replace(/^\s*\*/mg, '') | ||
return parseJSON(data) | ||
} | ||
module.exports = PackageJson |
const fs = require('fs/promises') | ||
const { glob } = require('glob') | ||
const normalizePackageBin = require('npm-normalize-package-bin') | ||
const normalizePackageData = require('normalize-package-data') | ||
const legacyFixer = require('normalize-package-data/lib/fixer.js') | ||
const legacyMakeWarning = require('normalize-package-data/lib/make_warning.js') | ||
const path = require('path') | ||
@@ -9,3 +10,9 @@ const log = require('proc-log') | ||
const normalize = async (pkg, { strict, steps, root }) => { | ||
// We don't want the `changes` array in here by default because this is a hot | ||
// path for parsing packuments during install. So the calling method passes it | ||
// in if it wants to track changes. | ||
const normalize = async (pkg, { strict, steps, root, changes, allowLegacyCase }) => { | ||
if (!pkg.content) { | ||
throw new Error('Can not normalize without content') | ||
} | ||
const data = pkg.content | ||
@@ -15,2 +22,14 @@ const scripts = data.scripts || {} | ||
legacyFixer.warn = function () { | ||
changes?.push(legacyMakeWarning.apply(null, arguments)) | ||
} | ||
// name and version are load bearing so we have to clean them up first | ||
if (steps.includes('fixNameField') || steps.includes('normalizeData')) { | ||
legacyFixer.fixNameField(data, { strict, allowLegacyCase }) | ||
} | ||
if (steps.includes('fixVersionField') || steps.includes('normalizeData')) { | ||
legacyFixer.fixVersionField(data, strict) | ||
} | ||
// remove attributes that start with "_" | ||
@@ -20,2 +39,3 @@ if (steps.includes('_attributes')) { | ||
if (key.startsWith('_')) { | ||
changes?.push(`"${key}" was removed`) | ||
delete pkg.content[key] | ||
@@ -29,2 +49,3 @@ } | ||
if (data.name && data.version) { | ||
changes?.push(`"_id" was set to ${pkgId}`) | ||
data._id = pkgId | ||
@@ -39,2 +60,3 @@ } | ||
} | ||
changes?.push(`Deleted incorrect "bundledDependencies"`) | ||
delete data.bundledDependencies | ||
@@ -46,10 +68,14 @@ } | ||
if (bd === false && !steps.includes('bundleDependenciesDeleteFalse')) { | ||
changes?.push(`"bundleDependencies" was changed from "false" to "[]"`) | ||
data.bundleDependencies = [] | ||
} else if (bd === true) { | ||
changes?.push(`"bundleDependencies" was auto-populated from "dependencies"`) | ||
data.bundleDependencies = Object.keys(data.dependencies || {}) | ||
} else if (bd && typeof bd === 'object') { | ||
if (!Array.isArray(bd)) { | ||
changes?.push(`"bundleDependencies" was changed from an object to an array`) | ||
data.bundleDependencies = Object.keys(bd) | ||
} | ||
} else { | ||
changes?.push(`"bundleDependencies" was removed`) | ||
delete data.bundleDependencies | ||
@@ -67,5 +93,7 @@ } | ||
for (const name in data.optionalDependencies) { | ||
changes?.push(`optionalDependencies entry "${name}" was removed`) | ||
delete data.dependencies[name] | ||
} | ||
if (!Object.keys(data.dependencies).length) { | ||
changes?.push(`empty "optionalDependencies" was removed`) | ||
delete data.dependencies | ||
@@ -84,2 +112,4 @@ } | ||
data.gypfile = true | ||
changes?.push(`"scripts.install" was set to "node-gyp rebuild"`) | ||
changes?.push(`"gypfile" was set to "true"`) | ||
} | ||
@@ -95,2 +125,3 @@ } | ||
data.scripts = scripts | ||
changes?.push('"scripts.start" was set to "node server.js"') | ||
} catch { | ||
@@ -108,7 +139,10 @@ // do nothing | ||
delete data.scripts[name] | ||
changes?.push(`invalid scripts entry "${name}" was removed`) | ||
} else if (steps.includes('scriptpath')) { | ||
data.scripts[name] = data.scripts[name].replace(spre, '') | ||
changes?.push(`scripts entry "${name}" was fixed to remove node_modules/.bin reference`) | ||
} | ||
} | ||
} else { | ||
changes?.push(`removed invalid "scripts"`) | ||
delete data.scripts | ||
@@ -121,2 +155,3 @@ } | ||
data.funding = { url: data.funding } | ||
changes?.push(`"funding" was changed to an object with a url attribute`) | ||
} | ||
@@ -133,2 +168,3 @@ } | ||
data.contributors = authors | ||
changes.push('"contributors" was auto-populated with the contents of the "AUTHORS" file') | ||
} catch { | ||
@@ -160,3 +196,9 @@ // do nothing | ||
data.readmeFilename = readmeFile | ||
changes?.push(`"readme" was set to the contents of ${readmeFile}`) | ||
changes?.push(`"readmeFilename" was set to ${readmeFile}`) | ||
} | ||
if (!data.readme) { | ||
// this.warn('missingReadme') | ||
data.readme = 'ERROR: No README data found!' | ||
} | ||
} | ||
@@ -286,5 +328,43 @@ | ||
// "normalizeData" from read-package-json | ||
// "normalizeData" from "read-package-json", which was just a call through to | ||
// "normalize-package-data". We only call the "fixer" functions because | ||
// outside of that it was also clobbering _id (which we already conditionally | ||
// do) and also adding the gypfile script (which we also already | ||
// conditionally do) | ||
// Some steps are isolated so we can do a limited subset of these in `fix` | ||
if (steps.includes('fixRepositoryField') || steps.includes('normalizeData')) { | ||
legacyFixer.fixRepositoryField(data) | ||
} | ||
if (steps.includes('fixBinField') || steps.includes('normalizeData')) { | ||
legacyFixer.fixBinField(data) | ||
} | ||
if (steps.includes('fixDependencies') || steps.includes('normalizeData')) { | ||
legacyFixer.fixDependencies(data, strict) | ||
} | ||
if (steps.includes('fixScriptsField') || steps.includes('normalizeData')) { | ||
legacyFixer.fixScriptsField(data) | ||
} | ||
if (steps.includes('normalizeData')) { | ||
normalizePackageData(data, strict) | ||
const legacySteps = [ | ||
'fixDescriptionField', | ||
'fixModulesField', | ||
'fixFilesField', | ||
'fixManField', | ||
'fixBugsField', | ||
'fixKeywordsField', | ||
'fixBundleDependenciesField', | ||
'fixHomepageField', | ||
'fixReadmeField', | ||
'fixLicenseField', | ||
'fixPeople', | ||
'fixTypos', | ||
] | ||
for (const legacyStep of legacySteps) { | ||
legacyFixer[legacyStep](data) | ||
} | ||
} | ||
@@ -291,0 +371,0 @@ |
{ | ||
"name": "@npmcli/package-json", | ||
"version": "3.1.1", | ||
"version": "4.0.0", | ||
"description": "Programmatic API to update package.json", | ||
@@ -5,0 +5,0 @@ "main": "lib/index.js", |
100
README.md
@@ -55,28 +55,33 @@ # @npmcli/package-json | ||
### `constructor(path)` | ||
### `constructor()` | ||
Creates a new instance of `PackageJson`. | ||
Creates a new empty instance of `PackageJson`. | ||
- `path`: `String` that points to the folder from where to read the | ||
`package.json` from | ||
--- | ||
### `async PackageJson.create(path)` | ||
Creates an empty `package.json` at the given path. If one already exists | ||
it will be overwritten. | ||
--- | ||
### `async PackageJson.load()` | ||
### `async PackageJson.load(path, opts = {})` | ||
Loads the `package.json` at location determined in the `path` option of | ||
the constructor. | ||
Loads a `package.json` at the given path. | ||
- `opts`: `Object` can contain: | ||
- `create`: `Boolean` if true, a new package.json will be created if one does not already exist. Will not clobber ane existing package.json that can not be parsed. | ||
### Example: | ||
Loads contents of the `package.json` file located at `./`: | ||
Loads contents of a `package.json` file located at `./`: | ||
```js | ||
const PackageJson = require('@npmcli/package-json') | ||
const pkgJson = new PackageJson('./') | ||
await pkgJson.load() | ||
const pkgJson = new PackageJson() | ||
await pkgJson.load('./') | ||
``` | ||
Throws an error in case the `package.json` file is missing or has invalid | ||
contents. | ||
Throws an error in case a `package.json` file is missing or has invalid contents. | ||
@@ -87,11 +92,9 @@ --- | ||
Convenience static method that returns a new instance and loads the contents of | ||
the `package.json` file from that location. | ||
Convenience static method that returns a new instance and loads the contents of a `package.json` file from that location. | ||
- `path`: `String` that points to the folder from where to read the | ||
`package.json` from | ||
- `path`: `String` that points to the folder from where to read the `package.json` from | ||
### Example: | ||
Loads contents of the `package.json` file located at `./`: | ||
Loads contents of a `package.json` file located at `./`: | ||
@@ -107,13 +110,23 @@ ```js | ||
Like `load` but intended for reading package.json files in a | ||
node_modules tree. Some light normalization is done to ensure that it | ||
is ready for use in `@npmcli/arborist` | ||
Intended for normalizing package.json files in a node_modules tree. Some light normalization is done to ensure that it is ready for use in `@npmcli/arborist` | ||
- `path`: `String` that points to the folder from where to read the `package.json` from | ||
- `opts`: `Object` can contain: | ||
- `strict`: `Boolean` enables optional strict mode when applying the `normalizeData` step | ||
- `steps`: `Array` optional normalization steps that will be applied to the `package.json` file, replacing the default steps | ||
- `root`: `Path` optional git root to provide when applying the `gitHead` step | ||
- `changes`: `Array` if provided, a message about each change that was made to the packument will be added to this array | ||
--- | ||
### **static** `async PackageJson.normalize(path)` | ||
### **static** `async PackageJson.normalize(path, opts = {})` | ||
Convenience static method like `load` but for calling `normalize` | ||
Convenience static that calls `load` before calling `normalize` | ||
--- | ||
- `path`: `String` that points to the folder from where to read the `package.json` from | ||
- `opts`: `Object` can contain: | ||
- `strict`: `Boolean` enables optional strict mode when applying the `normalizeData` step | ||
- `steps`: `Array` optional normalization steps that will be applied to the `package.json` file, replacing the default steps | ||
- `root`: `Path` optional git root to provide when applying the `gitHead` step | ||
- `changes`: `Array` if provided, a message about each change that was made to the packument will be added to this array | ||
@@ -124,15 +137,28 @@ --- | ||
Like `load` but intended for reading package.json files before publish. | ||
Like `normalize` but intended for preparing package.json files for publish. | ||
--- | ||
### **static** `async PackageJson.prepare(path)` | ||
### **static** `async PackageJson.prepare(path, opts = {})` | ||
Convenience static method like `load` but for calling `prepare` | ||
Convenience static that calls `load` before calling `prepare` | ||
- `path`: `String` that points to the folder from where to read the `package.json` from | ||
- `opts`: `Object` can contain: | ||
- `strict`: `Boolean` enables optional strict mode when applying the `normalizeData` step | ||
- `steps`: `Array` optional normalization steps that will be applied to the `package.json` file, replacing the default steps | ||
- `root`: `Path` optional git root to provide when applying the `gitHead` step | ||
- `changes`: `Array` if provided, a message about each change that was made to the packument will be added to this array | ||
--- | ||
### `async PackageJson.fix()` | ||
Like `normalize` but intended for the `npm pkg fix` command. | ||
--- | ||
### `PackageJson.update(content)` | ||
Updates the contents of the `package.json` with the `content` provided. | ||
Updates the contents of a `package.json` with the `content` provided. | ||
@@ -144,3 +170,3 @@ - `content`: `Object` containing the properties to be updated/replaced in the | ||
`optionalDependencies`, `peerDependencies` will have special logic to handle | ||
the update of these options, such as deduplications. | ||
the update of these options, such as sorting and deduplication. | ||
@@ -208,19 +234,5 @@ ### Example: | ||
Saves the current `content` to the same location used when initializing | ||
this instance. | ||
Saves the current `content` to the same location used when calling | ||
`load()`. | ||
<br /> | ||
## Related | ||
When you make a living out of reading and writing `package.json` files, you end | ||
up with quite the amount of packages dedicated to it, the **npm cli** also | ||
uses: | ||
- [read-package-json-fast](https://github.com/npm/read-package-json-fast) reads | ||
and normalizes `package.json` files the way the **npm cli** expects it. | ||
- [read-package-json](https://github.com/npm/read-package-json) reads and | ||
normalizes more info from your `package.json` file. Used by `npm@6` and in | ||
`npm@7` for publishing. | ||
## LICENSE | ||
@@ -227,0 +239,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
30817
696
236