init.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. const { statSync } = require('node:fs')
  2. const { relative, resolve } = require('node:path')
  3. const { mkdir } = require('node:fs/promises')
  4. const initJson = require('init-package-json')
  5. const npa = require('npm-package-arg')
  6. const libexec = require('libnpmexec')
  7. const mapWorkspaces = require('@npmcli/map-workspaces')
  8. const PackageJson = require('@npmcli/package-json')
  9. const { log, output, input } = require('proc-log')
  10. const updateWorkspaces = require('../utils/update-workspaces.js')
  11. const BaseCommand = require('../base-cmd.js')
  12. const posixPath = p => p.split('\\').join('/')
  13. class Init extends BaseCommand {
  14. static description = 'Create a package.json file'
  15. static params = [
  16. 'init-author-name',
  17. 'init-author-url',
  18. 'init-license',
  19. 'init-module',
  20. 'init-type',
  21. 'init-version',
  22. 'init-private',
  23. 'yes',
  24. 'force',
  25. 'scope',
  26. 'workspace',
  27. 'workspaces',
  28. 'workspaces-update',
  29. 'include-workspace-root',
  30. ]
  31. static name = 'init'
  32. static usage = [
  33. '<package-spec> (same as `npx create-<package-spec>`)',
  34. '<@scope> (same as `npx <@scope>/create`)',
  35. ]
  36. static workspaces = true
  37. static ignoreImplicitWorkspace = false
  38. async exec (args) {
  39. // npm exec style
  40. if (args.length) {
  41. return await this.execCreate(args)
  42. }
  43. // no args, uses classic init-package-json boilerplate
  44. await this.template()
  45. }
  46. async execWorkspaces (args) {
  47. // if the root package is uninitiated, take care of it first
  48. if (this.npm.flatOptions.includeWorkspaceRoot) {
  49. await this.exec(args)
  50. }
  51. // reads package.json for the top-level folder first
  52. // by doing this we ensure the command throw if no package.json is found before trying to create a workspace package.json file or its folders
  53. const { content: pkg } = await PackageJson.normalize(this.npm.localPrefix).catch(err => {
  54. if (err.code === 'ENOENT') {
  55. log.warn('init', 'Missing package.json. Try with `--include-workspace-root`.')
  56. }
  57. throw err
  58. })
  59. // these are workspaces that are being created, so we can't use this.setWorkspaces()
  60. const filters = this.npm.config.get('workspace')
  61. const wPath = filterArg => resolve(this.npm.localPrefix, filterArg)
  62. const workspacesPaths = []
  63. // npm-exec style, runs in the context of each workspace filter
  64. if (args.length) {
  65. for (const filterArg of filters) {
  66. const path = wPath(filterArg)
  67. await mkdir(path, { recursive: true })
  68. workspacesPaths.push(path)
  69. await this.execCreate(args, path)
  70. await this.setWorkspace(pkg, path)
  71. }
  72. return
  73. }
  74. // no args, uses classic init-package-json boilerplate
  75. for (const filterArg of filters) {
  76. const path = wPath(filterArg)
  77. await mkdir(path, { recursive: true })
  78. workspacesPaths.push(path)
  79. await this.template(path)
  80. await this.setWorkspace(pkg, path)
  81. }
  82. // reify packages once all workspaces have been initialized
  83. await this.update(workspacesPaths)
  84. }
  85. async execCreate (args, runPath = process.cwd()) {
  86. const [initerName, ...otherArgs] = args
  87. let packageName = initerName
  88. // Only a scope, possibly with a version
  89. if (/^@[^/]+$/.test(initerName)) {
  90. const [, scope, version] = initerName.split('@')
  91. packageName = `@${scope}/create`
  92. if (version) {
  93. packageName = `${packageName}@${version}`
  94. }
  95. } else {
  96. const req = npa(initerName)
  97. if (req.type === 'git' && req.hosted) {
  98. const { user, project } = req.hosted
  99. packageName = initerName.replace(`${user}/${project}`, `${user}/create-${project}`)
  100. } else if (req.registry) {
  101. packageName = `${req.name.replace(/^(@[^/]+\/)?/, '$1create-')}@${req.rawSpec}`
  102. } else {
  103. throw Object.assign(new Error(
  104. 'Unrecognized initializer: ' + initerName +
  105. '\nFor more package binary executing power check out `npx`:' +
  106. '\nhttps://docs.npmjs.com/cli/commands/npx'
  107. ), { code: 'EUNSUPPORTED' })
  108. }
  109. }
  110. const newArgs = [packageName, ...otherArgs]
  111. const {
  112. flatOptions,
  113. localBin,
  114. globalBin,
  115. chalk,
  116. } = this.npm
  117. const scriptShell = this.npm.config.get('script-shell') || undefined
  118. const yes = this.npm.config.get('yes')
  119. // only send the init-private flag if it is set
  120. const opts = { ...flatOptions }
  121. if (this.npm.config.isDefault('init-private')) {
  122. delete opts.initPrivate
  123. }
  124. await libexec({
  125. ...opts,
  126. args: newArgs,
  127. localBin,
  128. globalBin,
  129. output,
  130. chalk,
  131. path: this.npm.localPrefix,
  132. runPath,
  133. scriptShell,
  134. yes,
  135. })
  136. }
  137. async template (path = process.cwd()) {
  138. const initFile = this.npm.config.get('init-module')
  139. if (!this.npm.config.get('yes') && !this.npm.config.get('force')) {
  140. output.standard([
  141. 'This utility will walk you through creating a package.json file.',
  142. 'It only covers the most common items, and tries to guess sensible defaults.',
  143. '',
  144. 'See `npm help init` for definitive documentation on these fields and exactly what they do.',
  145. '',
  146. 'Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file.',
  147. '',
  148. 'Press ^C at any time to quit.',
  149. ].join('\n'))
  150. }
  151. try {
  152. const data = await input.read(() => initJson(path, initFile, this.npm.config))
  153. log.silly('package data', data)
  154. return data
  155. } catch (er) {
  156. if (er.message === 'canceled') {
  157. output.flush()
  158. log.warn('init', 'canceled')
  159. } else {
  160. throw er
  161. }
  162. }
  163. }
  164. async setWorkspace (pkg, workspacePath) {
  165. const workspaces = await mapWorkspaces({ cwd: this.npm.localPrefix, pkg })
  166. // skip setting workspace if current package.json glob already satisfies it
  167. for (const wPath of workspaces.values()) {
  168. if (wPath === workspacePath) {
  169. return
  170. }
  171. }
  172. // if a create-pkg didn't generate a package.json at the workspace folder level, it might not be recognized as a workspace by mapWorkspaces, so we're just going to avoid touching the top-level package.json
  173. try {
  174. statSync(resolve(workspacePath, 'package.json'))
  175. } catch {
  176. return
  177. }
  178. const pkgJson = await PackageJson.load(this.npm.localPrefix)
  179. pkgJson.update({
  180. workspaces: [
  181. ...(pkgJson.content.workspaces || []),
  182. posixPath(relative(this.npm.localPrefix, workspacePath)),
  183. ],
  184. })
  185. await pkgJson.save()
  186. }
  187. async update (workspacesPaths) {
  188. // translate workspaces paths into an array containing workspaces names
  189. const workspaces = []
  190. for (const path of workspacesPaths) {
  191. const { content: { name } } = await PackageJson.normalize(path).catch(() => ({ content: {} }))
  192. if (name) {
  193. workspaces.push(name)
  194. }
  195. }
  196. const {
  197. config,
  198. flatOptions,
  199. localPrefix,
  200. } = this.npm
  201. await updateWorkspaces({
  202. config,
  203. flatOptions,
  204. localPrefix,
  205. npm: this.npm,
  206. workspaces,
  207. })
  208. }
  209. }
  210. module.exports = Init