123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- const { readFile, writeFile } = require('node:fs/promises')
- const { resolve } = require('node:path')
- const parseJSON = require('json-parse-even-better-errors')
- const updateDeps = require('./update-dependencies.js')
- const updateScripts = require('./update-scripts.js')
- const updateWorkspaces = require('./update-workspaces.js')
- const normalize = require('./normalize.js')
- const { read, parse } = require('./read-package.js')
- const { packageSort } = require('./sort.js')
- // a list of handy specialized helper functions that take
- // care of special cases that are handled by the npm cli
- const knownSteps = new Set([
- updateDeps,
- updateScripts,
- updateWorkspaces,
- ])
- // list of all keys that are handled by "knownSteps" helpers
- const knownKeys = new Set([
- ...updateDeps.knownKeys,
- 'scripts',
- 'workspaces',
- ])
- class PackageJson {
- static normalizeSteps = Object.freeze([
- '_id',
- '_attributes',
- 'bundledDependencies',
- 'bundleDependencies',
- 'optionalDedupe',
- 'scripts',
- 'funding',
- 'bin',
- ])
- // npm pkg fix
- static fixSteps = Object.freeze([
- 'binRefs',
- 'bundleDependencies',
- 'bundleDependenciesFalse',
- 'fixNameField',
- 'fixVersionField',
- 'fixRepositoryField',
- 'fixDependencies',
- 'devDependencies',
- 'scriptpath',
- ])
- static prepareSteps = Object.freeze([
- '_id',
- '_attributes',
- 'bundledDependencies',
- 'bundleDependencies',
- 'bundleDependenciesDeleteFalse',
- 'gypfile',
- 'serverjs',
- 'scriptpath',
- 'authors',
- 'readme',
- 'mans',
- 'binDir',
- 'gitHead',
- 'fillTypes',
- 'normalizeData',
- 'binRefs',
- ])
- // 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) {
- const p = new PackageJson()
- await p.load(path, true)
- return p.prepare(opts)
- }
- // read-package-json-fast compatible behavior
- static async normalize (path, opts) {
- const p = new PackageJson()
- await p.load(path)
- return p.normalize(opts)
- }
- #path
- #manifest
- #readFileContent = ''
- #canSave = true
- // Load content from given path
- async load (path, parseIndex) {
- this.#path = path
- let parseErr
- try {
- this.#readFileContent = await read(this.filename)
- } catch (err) {
- if (!parseIndex) {
- throw err
- }
- parseErr = err
- }
- if (parseErr) {
- const indexFile = resolve(this.path, 'index.js')
- let indexFileContent
- try {
- indexFileContent = await readFile(indexFile, 'utf8')
- } catch (err) {
- throw parseErr
- }
- try {
- this.fromComment(indexFileContent)
- } catch (err) {
- throw parseErr
- }
- // 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) {
- this.#manifest = parse(data)
- return this
- }
- fromContent (data) {
- this.#manifest = data
- this.#canSave = false
- 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
- }
- get path () {
- 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) {
- 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.content })
- }
- // unknown properties will just be overwitten
- for (const [key, value] of Object.entries(content)) {
- if (!knownKeys.has(key)) {
- this.content[key] = value
- }
- }
- return this
- }
- async save ({ sort } = {}) {
- if (!this.#canSave) {
- throw new Error('No package.json to save to')
- }
- const {
- [Symbol.for('indent')]: indent,
- [Symbol.for('newline')]: newline,
- ...rest
- } = this.content
- const format = indent === undefined ? ' ' : indent
- const eol = newline === undefined ? '\n' : newline
- const content = sort ? packageSort(rest) : rest
- const fileContent = `${
- JSON.stringify(content, null, format)
- }\n`
- .replace(/\n/g, eol)
- if (fileContent.trim() !== this.#readFileContent.trim()) {
- const written = await writeFile(this.filename, fileContent)
- this.#readFileContent = fileContent
- return written
- }
- }
- async normalize (opts = {}) {
- if (!opts.steps) {
- opts.steps = this.constructor.normalizeSteps
- }
- await normalize(this, opts)
- return this
- }
- async prepare (opts = {}) {
- if (!opts.steps) {
- opts.steps = this.constructor.prepareSteps
- }
- await normalize(this, opts)
- return this
- }
- async fix (opts = {}) {
- // This one is not overridable
- opts.steps = this.constructor.fixSteps
- await normalize(this, opts)
- return this
- }
- }
- module.exports = PackageJson
|