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