1const { readFile, writeFile } = require('fs/promises') 2const { resolve } = require('path') 3const updateDeps = require('./update-dependencies.js') 4const updateScripts = require('./update-scripts.js') 5const updateWorkspaces = require('./update-workspaces.js') 6const normalize = require('./normalize.js') 7 8const parseJSON = require('json-parse-even-better-errors') 9 10// a list of handy specialized helper functions that take 11// care of special cases that are handled by the npm cli 12const knownSteps = new Set([ 13 updateDeps, 14 updateScripts, 15 updateWorkspaces, 16]) 17 18// list of all keys that are handled by "knownSteps" helpers 19const knownKeys = new Set([ 20 ...updateDeps.knownKeys, 21 'scripts', 22 'workspaces', 23]) 24 25class PackageJson { 26 static normalizeSteps = Object.freeze([ 27 '_id', 28 '_attributes', 29 'bundledDependencies', 30 'bundleDependencies', 31 'optionalDedupe', 32 'scripts', 33 'funding', 34 'bin', 35 ]) 36 37 // npm pkg fix 38 static fixSteps = Object.freeze([ 39 'binRefs', 40 'bundleDependencies', 41 'bundleDependenciesFalse', 42 'fixNameField', 43 'fixVersionField', 44 'fixRepositoryField', 45 'fixDependencies', 46 'devDependencies', 47 'scriptpath', 48 ]) 49 50 static prepareSteps = Object.freeze([ 51 '_id', 52 '_attributes', 53 'bundledDependencies', 54 'bundleDependencies', 55 'bundleDependenciesDeleteFalse', 56 'gypfile', 57 'serverjs', 58 'scriptpath', 59 'authors', 60 'readme', 61 'mans', 62 'binDir', 63 'gitHead', 64 'fillTypes', 65 'normalizeData', 66 'binRefs', 67 ]) 68 69 // create a new empty package.json, so we can save at the given path even 70 // though we didn't start from a parsed file 71 static async create (path, opts = {}) { 72 const p = new PackageJson() 73 await p.create(path) 74 if (opts.data) { 75 return p.update(opts.data) 76 } 77 return p 78 } 79 80 // Loads a package.json at given path and JSON parses 81 static async load (path, opts = {}) { 82 const p = new PackageJson() 83 // Avoid try/catch if we aren't going to create 84 if (!opts.create) { 85 return p.load(path) 86 } 87 88 try { 89 return await p.load(path) 90 } catch (err) { 91 if (!err.message.startsWith('Could not read package.json')) { 92 throw err 93 } 94 return await p.create(path) 95 } 96 } 97 98 // npm pkg fix 99 static async fix (path, opts) { 100 const p = new PackageJson() 101 await p.load(path, true) 102 return p.fix(opts) 103 } 104 105 // read-package-json compatible behavior 106 static async prepare (path, opts) { 107 const p = new PackageJson() 108 await p.load(path, true) 109 return p.prepare(opts) 110 } 111 112 // read-package-json-fast compatible behavior 113 static async normalize (path, opts) { 114 const p = new PackageJson() 115 await p.load(path) 116 return p.normalize(opts) 117 } 118 119 #path 120 #manifest 121 #readFileContent = '' 122 #canSave = true 123 124 // Load content from given path 125 async load (path, parseIndex) { 126 this.#path = path 127 let parseErr 128 try { 129 this.#readFileContent = await readFile(this.filename, 'utf8') 130 } catch (err) { 131 err.message = `Could not read package.json: ${err}` 132 if (!parseIndex) { 133 throw err 134 } 135 parseErr = err 136 } 137 138 if (parseErr) { 139 const indexFile = resolve(this.path, 'index.js') 140 let indexFileContent 141 try { 142 indexFileContent = await readFile(indexFile, 'utf8') 143 } catch (err) { 144 throw parseErr 145 } 146 try { 147 this.fromComment(indexFileContent) 148 } catch (err) { 149 throw parseErr 150 } 151 // This wasn't a package.json so prevent saving 152 this.#canSave = false 153 return this 154 } 155 156 return this.fromJSON(this.#readFileContent) 157 } 158 159 // Load data from a JSON string/buffer 160 fromJSON (data) { 161 try { 162 this.#manifest = parseJSON(data) 163 } catch (err) { 164 err.message = `Invalid package.json: ${err}` 165 throw err 166 } 167 return this 168 } 169 170 // Load data from a comment 171 // /**package { "name": "foo", "version": "1.2.3", ... } **/ 172 fromComment (data) { 173 data = data.split(/^\/\*\*package(?:\s|$)/m) 174 175 if (data.length < 2) { 176 throw new Error('File has no package in comments') 177 } 178 data = data[1] 179 data = data.split(/\*\*\/$/m) 180 181 if (data.length < 2) { 182 throw new Error('File has no package in comments') 183 } 184 data = data[0] 185 data = data.replace(/^\s*\*/mg, '') 186 187 this.#manifest = parseJSON(data) 188 return this 189 } 190 191 get content () { 192 return this.#manifest 193 } 194 195 get path () { 196 return this.#path 197 } 198 199 get filename () { 200 if (this.path) { 201 return resolve(this.path, 'package.json') 202 } 203 return undefined 204 } 205 206 create (path) { 207 this.#path = path 208 this.#manifest = {} 209 return this 210 } 211 212 // This should be the ONLY way to set content in the manifest 213 update (content) { 214 if (!this.content) { 215 throw new Error('Can not update without content. Please `load` or `create`') 216 } 217 218 for (const step of knownSteps) { 219 this.#manifest = step({ content, originalContent: this.content }) 220 } 221 222 // unknown properties will just be overwitten 223 for (const [key, value] of Object.entries(content)) { 224 if (!knownKeys.has(key)) { 225 this.content[key] = value 226 } 227 } 228 229 return this 230 } 231 232 async save () { 233 if (!this.#canSave) { 234 throw new Error('No package.json to save to') 235 } 236 const { 237 [Symbol.for('indent')]: indent, 238 [Symbol.for('newline')]: newline, 239 } = this.content 240 241 const format = indent === undefined ? ' ' : indent 242 const eol = newline === undefined ? '\n' : newline 243 const fileContent = `${ 244 JSON.stringify(this.content, null, format) 245 }\n` 246 .replace(/\n/g, eol) 247 248 if (fileContent.trim() !== this.#readFileContent.trim()) { 249 return await writeFile(this.filename, fileContent) 250 } 251 } 252 253 async normalize (opts = {}) { 254 if (!opts.steps) { 255 opts.steps = this.constructor.normalizeSteps 256 } 257 await normalize(this, opts) 258 return this 259 } 260 261 async prepare (opts = {}) { 262 if (!opts.steps) { 263 opts.steps = this.constructor.prepareSteps 264 } 265 await normalize(this, opts) 266 return this 267 } 268 269 async fix (opts = {}) { 270 // This one is not overridable 271 opts.steps = this.constructor.fixSteps 272 await normalize(this, opts) 273 return this 274 } 275} 276 277module.exports = PackageJson 278