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