1 | "use strict";
|
2 |
|
3 | const fs = require("fs-extra");
|
4 | const path = require("path");
|
5 | const os = require("os");
|
6 | const { URL } = require("whatwg-url");
|
7 | const { camelCase } = require("yargs-parser");
|
8 | const dedent = require("dedent");
|
9 | const initPackageJson = require("pify")(require("init-package-json"));
|
10 | const pacote = require("pacote");
|
11 | const npa = require("npm-package-arg");
|
12 | const pReduce = require("p-reduce");
|
13 | const slash = require("slash");
|
14 |
|
15 | const { Command } = require("@lerna/command");
|
16 | const childProcess = require("@lerna/child-process");
|
17 | const npmConf = require("@lerna/npm-conf");
|
18 | const { ValidationError } = require("@lerna/validation-error");
|
19 | const { builtinNpmrc } = require("./lib/builtin-npmrc");
|
20 | const { catFile } = require("./lib/cat-file");
|
21 |
|
22 | const LERNA_MODULE_DATA = require.resolve("./lib/lerna-module-data.js");
|
23 | const DEFAULT_DESCRIPTION = [
|
24 | "Now I’m the model of a modern major general",
|
25 | "The venerated Virginian veteran whose men are all",
|
26 | "Lining up, to put me up on a pedestal",
|
27 | "Writin’ letters to relatives",
|
28 | "Embellishin’ my elegance and eloquence",
|
29 | "But the elephant is in the room",
|
30 | "The truth is in ya face when ya hear the British cannons go",
|
31 | "BOOM",
|
32 | ].join(" / ");
|
33 |
|
34 | module.exports = factory;
|
35 |
|
36 | function factory(argv) {
|
37 | return new CreateCommand(argv);
|
38 | }
|
39 |
|
40 | class CreateCommand extends Command {
|
41 | initialize() {
|
42 | const {
|
43 | bin,
|
44 | description = DEFAULT_DESCRIPTION,
|
45 | esModule,
|
46 | keywords,
|
47 | license,
|
48 | loc: pkgLocation,
|
49 | name: pkgName,
|
50 | yes,
|
51 | } = this.options;
|
52 |
|
53 |
|
54 | const { name, scope } = npa(pkgName);
|
55 |
|
56 |
|
57 | this.dirName = scope ? name.split("/").pop() : name;
|
58 | this.pkgName = name;
|
59 | this.pkgsDir =
|
60 | this.project.packageParentDirs.find((pd) => pd.indexOf(pkgLocation) > -1) ||
|
61 | this.project.packageParentDirs[0];
|
62 |
|
63 | this.camelName = camelCase(this.dirName);
|
64 |
|
65 |
|
66 | this.outDir = esModule ? "dist" : "lib";
|
67 | this.targetDir = path.resolve(this.pkgsDir, this.dirName);
|
68 |
|
69 | this.binDir = path.join(this.targetDir, "bin");
|
70 | this.binFileName = bin === true ? this.dirName : bin;
|
71 |
|
72 | this.libDir = path.join(this.targetDir, esModule ? "src" : "lib");
|
73 | this.libFileName = `${this.dirName}.js`;
|
74 |
|
75 | this.testDir = path.join(this.targetDir, "__tests__");
|
76 | this.testFileName = `${this.dirName}.test.js`;
|
77 |
|
78 | this.conf = npmConf({
|
79 | description,
|
80 | esModule,
|
81 | keywords,
|
82 | scope,
|
83 | yes,
|
84 | });
|
85 |
|
86 |
|
87 | this.conf.addFile(builtinNpmrc(), "builtin");
|
88 |
|
89 |
|
90 | this.conf.set("init-main", `${this.outDir}/${this.libFileName}`);
|
91 |
|
92 | if (esModule) {
|
93 | this.conf.set("init-es-module", `${this.outDir}/${this.dirName}.module.js`);
|
94 | }
|
95 |
|
96 |
|
97 | if (!this.project.isIndependent()) {
|
98 | this.conf.set("init-version", this.project.version);
|
99 | }
|
100 |
|
101 |
|
102 | if (this.conf.get("init-author-name") === "") {
|
103 | this.conf.set("init-author-name", this.gitConfig("user.name"));
|
104 | }
|
105 |
|
106 | if (this.conf.get("init-author-email") === "") {
|
107 | this.conf.set("init-author-email", this.gitConfig("user.email"));
|
108 | }
|
109 |
|
110 |
|
111 | if (license) {
|
112 | this.conf.set("init-license", license);
|
113 | }
|
114 |
|
115 |
|
116 | if (this.options.private) {
|
117 | this.conf.set("private", true);
|
118 | }
|
119 |
|
120 |
|
121 |
|
122 | if (this.options.loglevel === "silent") {
|
123 | this.conf.set("silent", true);
|
124 | }
|
125 |
|
126 |
|
127 | if (this.binFileName) {
|
128 | this.conf.set("bin", {
|
129 | [this.binFileName]: `bin/${this.binFileName}`,
|
130 | });
|
131 | }
|
132 |
|
133 |
|
134 |
|
135 | this.conf.set("directories", {
|
136 | lib: this.outDir,
|
137 | test: "__tests__",
|
138 | });
|
139 |
|
140 | this.setFiles();
|
141 | this.setHomepage();
|
142 | this.setPublishConfig();
|
143 | this.setRepository();
|
144 |
|
145 | return Promise.resolve(this.setDependencies());
|
146 | }
|
147 |
|
148 | execute() {
|
149 | let chain = Promise.resolve();
|
150 |
|
151 | chain = chain.then(() => fs.mkdirp(this.libDir));
|
152 | chain = chain.then(() => fs.mkdirp(this.testDir));
|
153 | chain = chain.then(() => Promise.all([this.writeReadme(), this.writeLibFile(), this.writeTestFile()]));
|
154 |
|
155 | if (this.binFileName) {
|
156 | chain = chain.then(() => fs.mkdirp(this.binDir));
|
157 | chain = chain.then(() => Promise.all([this.writeBinFile(), this.writeCliFile(), this.writeCliTest()]));
|
158 | }
|
159 |
|
160 | chain = chain.then(() => initPackageJson(this.targetDir, LERNA_MODULE_DATA, this.conf));
|
161 |
|
162 | return chain.then((data) => {
|
163 | if (this.options.esModule) {
|
164 | this.logger.notice(
|
165 | "✔",
|
166 | dedent`
|
167 | Ensure '${path.relative(".", this.pkgsDir)}/*/${this.outDir}' has been added to ./.gitignore
|
168 | Ensure rollup or babel build scripts are in the root
|
169 | `
|
170 | );
|
171 | }
|
172 |
|
173 | this.logger.success(
|
174 | "create",
|
175 | `New package ${data.name} created at ./${path.relative(".", this.targetDir)}`
|
176 | );
|
177 | });
|
178 | }
|
179 |
|
180 | gitConfig(prop) {
|
181 | return childProcess.execSync("git", ["config", "--get", prop], this.execOpts);
|
182 | }
|
183 |
|
184 | collectExternalVersions() {
|
185 |
|
186 | const extVersions = new Map();
|
187 |
|
188 | for (const { externalDependencies } of this.packageGraph.values()) {
|
189 | for (const [name, resolved] of externalDependencies) {
|
190 | extVersions.set(name, resolved.fetchSpec);
|
191 | }
|
192 | }
|
193 |
|
194 | return extVersions;
|
195 | }
|
196 |
|
197 | hasLocalRelativeFileSpec() {
|
198 |
|
199 |
|
200 | for (const { localDependencies } of this.packageGraph.values()) {
|
201 | for (const spec of localDependencies.values()) {
|
202 | if (spec.type === "directory") {
|
203 | return true;
|
204 | }
|
205 | }
|
206 | }
|
207 | }
|
208 |
|
209 | resolveRelative(depNode) {
|
210 |
|
211 | const relPath = path.relative(this.targetDir, depNode.location);
|
212 | const spec = npa.resolve(depNode.name, relPath, this.targetDir);
|
213 |
|
214 | return slash(spec.saveSpec);
|
215 | }
|
216 |
|
217 | setDependencies() {
|
218 | const inputs = new Set((this.options.dependencies || []).sort());
|
219 |
|
220 |
|
221 | if (this.options.bin) {
|
222 | inputs.add("yargs");
|
223 | }
|
224 |
|
225 | if (!inputs.size) {
|
226 | return;
|
227 | }
|
228 |
|
229 | const exts = this.collectExternalVersions();
|
230 | const localRelative = this.hasLocalRelativeFileSpec();
|
231 | const savePrefix = this.conf.get("save-exact") ? "" : this.conf.get("save-prefix");
|
232 | const pacoteOpts = this.conf.snapshot;
|
233 |
|
234 | const decideVersion = (spec) => {
|
235 | if (this.packageGraph.has(spec.name)) {
|
236 |
|
237 | const depNode = this.packageGraph.get(spec.name);
|
238 |
|
239 | if (localRelative) {
|
240 |
|
241 | return this.resolveRelative(depNode);
|
242 | }
|
243 |
|
244 |
|
245 | return `${savePrefix}${depNode.version}`;
|
246 | }
|
247 |
|
248 | if (spec.type === "tag" && spec.fetchSpec === "latest") {
|
249 |
|
250 | if (exts.has(spec.name)) {
|
251 |
|
252 | return exts.get(spec.name);
|
253 | }
|
254 |
|
255 |
|
256 | return pacote.manifest(spec, pacoteOpts).then((pkg) => `${savePrefix}${pkg.version}`);
|
257 | }
|
258 |
|
259 | if (spec.type === "git") {
|
260 | throw new ValidationError("EGIT", "Do not use git dependencies");
|
261 | }
|
262 |
|
263 |
|
264 | return spec.rawSpec;
|
265 | };
|
266 |
|
267 | let chain = Promise.resolve();
|
268 |
|
269 | chain = chain.then(() =>
|
270 | pReduce(
|
271 | inputs,
|
272 | (obj, input) => {
|
273 | const spec = npa(input);
|
274 |
|
275 | return Promise.resolve(spec)
|
276 | .then(decideVersion)
|
277 | .then((version) => {
|
278 | obj[spec.name] = version;
|
279 |
|
280 | return obj;
|
281 | });
|
282 | },
|
283 | {}
|
284 | )
|
285 | );
|
286 |
|
287 | chain = chain.then((dependencies) => {
|
288 | this.conf.set("dependencies", dependencies);
|
289 | });
|
290 |
|
291 | return chain;
|
292 | }
|
293 |
|
294 | setFiles() {
|
295 |
|
296 | const files = [this.outDir];
|
297 |
|
298 | if (this.options.bin) {
|
299 | files.unshift("bin");
|
300 | }
|
301 |
|
302 | this.conf.set("files", files);
|
303 | }
|
304 |
|
305 | setHomepage() {
|
306 |
|
307 | let { homepage = this.project.manifest.get("homepage") } = this.options;
|
308 |
|
309 | if (!homepage) {
|
310 |
|
311 | return;
|
312 | }
|
313 |
|
314 |
|
315 | if (homepage.indexOf("http") !== 0) {
|
316 | homepage = `http://${homepage}`;
|
317 | }
|
318 |
|
319 | const hurl = new URL(homepage);
|
320 | const relativeTarget = path.relative(this.project.rootPath, this.targetDir);
|
321 |
|
322 | if (hurl.hostname.match("github")) {
|
323 | hurl.pathname = path.posix.join(hurl.pathname, "tree/main", relativeTarget);
|
324 |
|
325 |
|
326 |
|
327 | hurl.hash = "readme";
|
328 | } else if (!this.options.homepage) {
|
329 |
|
330 | hurl.pathname = path.posix.join(hurl.pathname, relativeTarget);
|
331 | }
|
332 |
|
333 | this.conf.set("homepage", hurl.href);
|
334 | }
|
335 |
|
336 | setPublishConfig() {
|
337 | const scope = this.conf.get("scope");
|
338 | const registry = this.options.registry || this.conf.get(`${scope}:registry`) || this.conf.get("registry");
|
339 | const isPublicRegistry = registry === this.conf.root.registry;
|
340 | const publishConfig = {};
|
341 |
|
342 | if (scope && isPublicRegistry) {
|
343 | publishConfig.access = this.options.access || "public";
|
344 | }
|
345 |
|
346 | if (registry && !isPublicRegistry) {
|
347 | publishConfig.registry = registry;
|
348 | }
|
349 |
|
350 | if (this.options.tag) {
|
351 | publishConfig.tag = this.options.tag;
|
352 | }
|
353 |
|
354 | if (Object.keys(publishConfig).length) {
|
355 | this.conf.set("publishConfig", publishConfig);
|
356 | }
|
357 | }
|
358 |
|
359 | setRepository() {
|
360 | try {
|
361 | const url = childProcess.execSync("git", ["remote", "get-url", "origin"], this.execOpts);
|
362 |
|
363 | this.conf.set("repository", url);
|
364 | } catch (err) {
|
365 | this.logger.warn("ENOREMOTE", "No git remote found, skipping repository property");
|
366 | }
|
367 | }
|
368 |
|
369 | writeReadme() {
|
370 | const readmeContent = dedent`
|
371 | # \`${this.pkgName}\`
|
372 |
|
373 | > ${this.options.description || "TODO: description"}
|
374 |
|
375 | ## Usage
|
376 |
|
377 | \`\`\`
|
378 | ${
|
379 | // eslint-disable-next-line no-nested-ternary
|
380 | this.options.bin
|
381 | ? dedent`
|
382 | npm -g i ${this.pkgName}
|
383 |
|
384 | ${this.binFileName} --help
|
385 | `
|
386 | : this.options.esModule
|
387 | ? `import ${this.camelName} from '${this.pkgName}';`
|
388 | : `const ${this.camelName} = require('${this.pkgName}');`
|
389 | }
|
390 |
|
391 | // TODO: DEMONSTRATE API
|
392 | \`\`\`
|
393 | `;
|
394 |
|
395 | return catFile(this.targetDir, "README.md", readmeContent);
|
396 | }
|
397 |
|
398 | writeLibFile() {
|
399 | const libContent = this.options.esModule
|
400 | ? dedent`
|
401 | export default function ${this.camelName}() {
|
402 | return "Hello from ${this.camelName}";
|
403 | }
|
404 | `
|
405 | : dedent`
|
406 | 'use strict';
|
407 |
|
408 | module.exports = ${this.camelName};
|
409 |
|
410 | function ${this.camelName}() {
|
411 | return "Hello from ${this.camelName}";
|
412 | }
|
413 | `;
|
414 |
|
415 | return catFile(this.libDir, this.libFileName, libContent);
|
416 | }
|
417 |
|
418 | writeTestFile() {
|
419 | const testContent = this.options.esModule
|
420 | ? dedent`
|
421 | import ${this.camelName} from '../src/${this.dirName}.js';
|
422 | import { strict as assert } from 'assert';
|
423 |
|
424 | assert.strictEqual(${this.camelName}(), 'Hello from ${this.camelName}');
|
425 | console.info("${this.camelName} tests passed");
|
426 | `
|
427 | : dedent`
|
428 | 'use strict';
|
429 |
|
430 | const ${this.camelName} = require('..');
|
431 | const assert = require('assert').strict;
|
432 |
|
433 | assert.strictEqual(${this.camelName}(), 'Hello from ${this.camelName}');
|
434 | console.info("${this.camelName} tests passed");
|
435 | `;
|
436 |
|
437 | return catFile(this.testDir, this.testFileName, testContent);
|
438 | }
|
439 |
|
440 | writeCliFile() {
|
441 | const cliFileName = "cli.js";
|
442 | const cliContent = [
|
443 | this.options.esModule
|
444 | ? dedent`
|
445 | import factory from 'yargs/yargs';
|
446 | import ${this.camelName} from './${this.dirName}';
|
447 |
|
448 | export default cli;
|
449 | `
|
450 | : dedent`
|
451 | 'use strict';
|
452 |
|
453 | const factory = require('yargs/yargs');
|
454 | const ${this.camelName} = require('./${this.dirName}');
|
455 |
|
456 | module.exports = cli;
|
457 | `,
|
458 | "",
|
459 | dedent`
|
460 | function cli(cwd) {
|
461 | const parser = factory(null, cwd);
|
462 |
|
463 | parser.alias('h', 'help');
|
464 | parser.alias('v', 'version');
|
465 |
|
466 | parser.usage(
|
467 | "$0",
|
468 | "TODO: description",
|
469 | yargs => {
|
470 | yargs.options({
|
471 | // TODO: options
|
472 | });
|
473 | },
|
474 | argv => ${this.camelName}(argv)
|
475 | );
|
476 |
|
477 | return parser;
|
478 | }
|
479 | `,
|
480 | ].join(os.EOL);
|
481 |
|
482 | return catFile(this.libDir, cliFileName, cliContent);
|
483 | }
|
484 |
|
485 | writeCliTest() {
|
486 | const cliTestFileName = "cli.test.js";
|
487 | const cliTestContent = [
|
488 | this.options.esModule
|
489 | ? dedent`
|
490 | import cli from '../src/cli';
|
491 | `
|
492 | : dedent`
|
493 | 'use strict';
|
494 |
|
495 | const cli = require('../lib/cli');
|
496 | `,
|
497 | "",
|
498 | dedent`
|
499 | describe('${this.pkgName} cli', () => {
|
500 | // const argv = cli(cwd).parse(['args']);
|
501 | it('needs tests');
|
502 | });
|
503 | `,
|
504 | ].join(os.EOL);
|
505 |
|
506 | return catFile(this.testDir, cliTestFileName, cliTestContent);
|
507 | }
|
508 |
|
509 | writeBinFile() {
|
510 | const binContent = dedent`
|
511 | #!/usr/bin/env node
|
512 |
|
513 | 'use strict';
|
514 |
|
515 | // eslint-disable-next-line no-unused-expressions
|
516 | require('../${this.outDir}/cli')${
|
517 | this.options.esModule ? ".default" : ""
|
518 | }().parse(process.argv.slice(2));`;
|
519 |
|
520 | return catFile(this.binDir, this.binFileName, binContent, { mode: 0o755 });
|
521 | }
|
522 | }
|
523 |
|
524 | module.exports.CreateCommand = CreateCommand;
|