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 new pkg fix command #6626

Merged
merged 3 commits into from Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/lib/content/commands/npm-pkg.md
Expand Up @@ -135,6 +135,13 @@ Returned values are always in **json** format.
npm pkg delete scripts.build
```

* `npm pkg fix`

Auto corrects common errors in your `package.json`. npm already
does this during `publish`, which leads to subtle (mostly harmless)
differences between the contents of your `package.json` file and the
manifest that npm uses during installation.

### Workspaces support

You can set/get/delete items across your configured workspaces by using the
Expand Down
8 changes: 8 additions & 0 deletions lib/commands/pkg.js
Expand Up @@ -11,6 +11,7 @@ class Pkg extends BaseCommand {
'delete <key> [<key> ...]',
'set [<array>[<index>].<key>=<value> ...]',
'set [<array>[].<key>=<value> ...]',
'fix',
]

static params = [
Expand Down Expand Up @@ -45,6 +46,8 @@ class Pkg extends BaseCommand {
return this.set(_args)
case 'delete':
return this.delete(_args)
case 'fix':
return this.fix(_args)
default:
throw this.usageError()
}
Expand Down Expand Up @@ -136,6 +139,11 @@ class Pkg extends BaseCommand {
pkgJson.update(q.toJSON())
await pkgJson.save()
}

async fix () {
const pkgJson = await PackageJson.fix(this.prefix)
await pkgJson.save()
}
}

module.exports = Pkg
172 changes: 119 additions & 53 deletions node_modules/@npmcli/package-json/lib/index.js
Expand Up @@ -34,7 +34,23 @@ class PackageJson {
'bin',
])

// npm pkg fix
static fixSteps = Object.freeze([
'binRefs',
'bundleDependencies',
'bundleDependenciesFalse',
'fixNameField',
'fixVersionField',
'fixRepositoryField',
'fixBinField',
'fixDependencies',
'fixScriptsField',
'devDependencies',
'scriptpath',
])

static prepareSteps = Object.freeze([
'_id',
'_attributes',
'bundledDependencies',
'bundleDependencies',
Expand All @@ -52,37 +68,67 @@ class PackageJson {
'binRefs',
])

// 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)
}

// read-package-json-fast compatible behavior
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) {
err.message = `Could not read package.json: ${err}`
if (!parseIndex) {
Expand All @@ -92,31 +138,58 @@ class PackageJson {
}

if (parseErr) {
const indexFile = resolve(this.#path, 'index.js')
const indexFile = resolve(this.path, 'index.js')
let indexFileContent
try {
indexFileContent = await readFile(indexFile, 'utf8')
} catch (err) {
throw parseErr
}
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) {
err.message = `Invalid package.json: ${err}`
throw err
}
return this
}

// 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 () {
return this.#manifest
}
Expand All @@ -125,58 +198,64 @@ class PackageJson {
return this.#path
}

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 })
}

// unknown properties will just be overwitten
for (const [key, value] of Object.entries(content)) {
if (!knownKeys.has(key)) {
this.#manifest[key] = value
this.content[key] = value
}
}

return this
}

async save () {
if (this.#fromIndex) {
if (!this.#canSave) {
throw new Error('No package.json to save to')
}
const {
[Symbol.for('indent')]: indent,
[Symbol.for('newline')]: newline,
} = this.#manifest
} = this.content

const format = indent === undefined ? ' ' : indent
const eol = newline === undefined ? '\n' : newline
const fileContent = `${
JSON.stringify(this.#manifest, null, format)
JSON.stringify(this.content, null, format)
}\n`
.replace(/\n/g, eol)

if (fileContent.trim() !== this.#readFileContent.trim()) {
return await writeFile(this.#filename, fileContent)
return await writeFile(this.filename, fileContent)
}
}

async normalize (opts = {}) {
if (!opts.steps) {
opts.steps = this.constructor.normalizeSteps
}
await this.load()
await normalize(this, opts)
return this
}
Expand All @@ -185,29 +264,16 @@ class PackageJson {
if (!opts.steps) {
opts.steps = this.constructor.prepareSteps
}
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')
}
data = data[1]
data = data.split(/\*\*\/$/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[0]
data = data.replace(/^\s*\*/mg, '')

return parseJSON(data)
}

module.exports = PackageJson