index.js 6.3 KB

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