index.js 6.3 KB

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