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