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