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