create-nevela 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.mjs CHANGED
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // No dependencies on purpose: this runs before anything is installed.
8
8
 
9
- import { spawn } from 'node:child_process';
9
+ import { spawn, spawnSync } from 'node:child_process';
10
10
  import crypto from 'node:crypto';
11
11
  import fs from 'node:fs';
12
12
  import os from 'node:os';
@@ -369,17 +369,62 @@ async function inProject(name, args) {
369
369
  process.exit(status);
370
370
  }
371
371
 
372
+ /**
373
+ * How the copy that is running was installed, or null when it isn't the installed command.
374
+ * The folder it was started through is passed as well as the one its files are in: under
375
+ * pnpm 11 only the first says it is pnpm's.
376
+ */
377
+ function installed() {
378
+ if (fromRepository) return null;
379
+ const startedFrom = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : here;
380
+ return globalInstall(here, fs.existsSync, startedFrom);
381
+ }
382
+
383
+ /** What typing `nevela` runs now, as "x.y.z", or null when that can't be found out. */
384
+ function versionOnPath() {
385
+ const answer = spawnCommand('nevela', ['--version'], { stdio: 'pipe', encoding: 'utf8' });
386
+ return /^\d+\.\d+\.\d+$/m.exec(`${answer.stdout ?? ''}`)?.[0] ?? null;
387
+ }
388
+
389
+ /** The launcher of the nevela command in `dir`: the file the shell runs. */
390
+ const launcherIn = (dir) => path.join(dir, windows ? 'nevela.cmd' : 'nevela');
391
+
392
+ /** The version of the nevela command in `dir`, as "x.y.z", or null when it won't say. */
393
+ function copyVersion(dir) {
394
+ const file = launcherIn(dir);
395
+ const answer = windows
396
+ ? spawnSync(`"${file}" --version`, { shell: true, stdio: 'pipe', encoding: 'utf8' })
397
+ : spawnSync(file, ['--version'], { stdio: 'pipe', encoding: 'utf8' });
398
+ return /^\d+\.\d+\.\d+$/m.exec(`${answer.stdout ?? ''}`)?.[0] ?? null;
399
+ }
400
+
372
401
  /**
373
402
  * Say so when the nevela command is installed more than once. Only the first on the PATH
374
403
  * ever runs, so an update to one of the others looks like an update that did nothing.
404
+ * The one to keep is the newest, which is not always the one that runs.
375
405
  */
376
406
  function otherCopies() {
377
- const copies = copiesOnPath();
407
+ const copies = copiesOnPath().map((dir) => ({ dir, version: copyVersion(dir) }));
378
408
  if (copies.length < 2) return;
409
+ let keep = copies[0];
410
+ for (const copy of copies) {
411
+ if (copy.version && (!keep.version || isNewer(copy.version, keep.version))) keep = copy;
412
+ }
413
+ const width = Math.max(...copies.map((copy) => copy.dir.length));
379
414
  console.log(`\n ${red('!')} The nevela command is installed ${copies.length} times. Typing ${bold('nevela')} runs the first of these:`);
380
- for (const dir of copies) console.log(` ${dir}`);
381
- console.log(` Keep that one and remove the rest, so there is one to keep up to date:`);
382
- for (const dir of copies.slice(1)) console.log(` ${bold(removeCommand(dir))}`);
415
+ for (const copy of copies) console.log(` ${copy.dir.padEnd(width)} ${copy.version ? `v${copy.version}` : ''}`);
416
+ console.log(keep === copies[0]
417
+ ? ` Keep that one and remove the rest, so there is one to keep up to date:`
418
+ : ` The one that runs is not the newest. Remove the others, and ${bold(`v${keep.version}`)} is what ${bold('nevela')} runs:`);
419
+ const launcher = (dir) => {
420
+ try {
421
+ return fs.readFileSync(launcherIn(dir), 'utf8').slice(0, 4000);
422
+ } catch {
423
+ return '';
424
+ }
425
+ };
426
+ const removals = new Set(copies.filter((copy) => copy !== keep).map((copy) => removeCommand(copy.dir, launcher(copy.dir))));
427
+ for (const removal of removals) console.log(` ${bold(removal)}`);
383
428
  }
384
429
 
385
430
  /** The version of Nevela an app has installed, read from the package, or null. */
@@ -395,7 +440,7 @@ function appVersion(project) {
395
440
  /** `nevela version`, from anywhere: the command's version, and the app's when in one. */
396
441
  function showVersion() {
397
442
  const project = findProject(process.cwd());
398
- const install = fromRepository ? null : globalInstall(here);
443
+ const install = installed();
399
444
  console.log(`\n ${indigo(bold('Nevela command'))} ${bold(`v${VERSION}`)}${install ? ` ${dim(`installed with ${install.manager}`)}` : ''}`);
400
445
  if (project) {
401
446
  const app = appVersion(project);
@@ -459,7 +504,7 @@ async function askName() {
459
504
  */
460
505
  async function updateTool(args) {
461
506
  const project = findProject(process.cwd());
462
- const install = fromRepository ? null : globalInstall(here);
507
+ const install = installed();
463
508
 
464
509
  if (!install) {
465
510
  // Run through npx or `pnpm dlx`, which fetch the latest every time, started by an
@@ -485,18 +530,29 @@ async function updateTool(args) {
485
530
  if (latest === null) {
486
531
  // Not the same as being up to date, so it isn't reported as that.
487
532
  console.log(` ${red('!')} Couldn't reach npm to see whether there is a newer nevela than ${bold(VERSION)}. Nothing was changed.`);
488
- console.log(` Try again when you are online, or run: ${install.command.join(' ')}`);
533
+ console.log(` Try again when you are online, or run: ${install.command().join(' ')}`);
489
534
  process.exitCode = 1;
490
535
  } else if (!newer) {
491
536
  console.log(` ${green(`✔ Already on the latest version (v${VERSION}). Nothing to do.`)}`);
492
537
  } else {
493
- console.log(` Updating v${VERSION} → ${bold(`v${newer}`)} ${dim(`(${install.command.join(' ')})`)}\n`);
494
- const [command, ...rest] = install.command;
538
+ // The version by number, not "latest": pnpm holds "latest" back for a day.
539
+ const [command, ...rest] = install.command(newer);
540
+ console.log(` Updating v${VERSION} → ${bold(`v${newer}`)} ${dim(`(${[command, ...rest].join(' ')})`)}\n`);
495
541
  const result = spawnCommand(command, rest, { stdio: 'inherit' });
496
542
  if (result.error || result.status !== 0) {
497
- fail(`Couldn't update the nevela command.`, `Run it yourself to see why: ${install.command.join(' ')}`);
543
+ fail(`Couldn't update the nevela command.`, `Run it yourself to see why: ${[command, ...rest].join(' ')}`);
544
+ }
545
+ // The package manager saying so isn't the same as the command being newer: ask it.
546
+ const now = versionOnPath();
547
+ if (now === newer || now === null) {
548
+ console.log(`\n ${green(`✔ Updated to v${newer}.`)} ${dim('Every nevela command, and every app you create from now on, uses it.')}`);
549
+ } else {
550
+ console.log(`\n ${red('!')} ${install.manager} installed ${bold(`v${newer}`)}, but typing ${bold('nevela')} still runs ${bold(`v${now}`)}.`);
551
+ if (copiesOnPath().length < 2) {
552
+ console.log(` Open a new terminal and run ${bold('nevela version')}. If it still says v${now}, the copy that runs was installed another way.`);
553
+ }
554
+ process.exitCode = 1;
498
555
  }
499
- console.log(`\n ${green(`✔ Updated to v${newer}.`)} ${dim('Every nevela command, and every app you create from now on, uses it.')}`);
500
556
  }
501
557
  otherCopies();
502
558
  if (project) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nevela",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Create a new Nevela app: a Laravel API and a Next.js dashboard from one resource description.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@ use Nevela\Laravel\Support\Naming;
18
18
  final class Nevela
19
19
  {
20
20
  /** Written by `pnpm release`; the same number as the repository's git tag. */
21
- public const VERSION = '0.4.2';
21
+ public const VERSION = '0.4.3';
22
22
 
23
23
  /** @var array<string, Descriptor> */
24
24
  private static array $descriptors = [];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nevela-web",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
package/tool.mjs CHANGED
@@ -6,43 +6,64 @@
6
6
  // nevela upgrade the app you are in: its Nevela package and its dashboard
7
7
  //
8
8
  // Updating the command means asking the package manager that installed it to install the
9
- // latest. Which one that was is read from where this file is running from.
9
+ // newest version. Which one that was is read from where this file is running from.
10
10
 
11
11
  import fs from 'node:fs';
12
12
 
13
- /** The command that installs the latest create-nevela globally, for each package manager. */
13
+ /**
14
+ * The command that installs create-nevela globally, for each package manager.
15
+ *
16
+ * `nevela update` names the exact version it found on npm, not "latest": pnpm holds a
17
+ * version back for a day after it is published, so `pnpm add -g create-nevela@latest`
18
+ * installs yesterday's. Asked for a version by number, it installs that one.
19
+ */
14
20
  const INSTALL = {
15
- npm: ['npm', 'install', '-g', 'create-nevela@latest'],
16
- pnpm: ['pnpm', 'add', '-g', 'create-nevela@latest'],
17
- yarn: ['yarn', 'global', 'add', 'create-nevela@latest'],
18
- bun: ['bun', 'add', '-g', 'create-nevela@latest'],
21
+ npm: (version) => ['npm', 'install', '-g', `create-nevela@${version}`],
22
+ pnpm: (version) => ['pnpm', 'add', '-g', `create-nevela@${version}`],
23
+ yarn: (version) => ['yarn', 'global', 'add', `create-nevela@${version}`],
24
+ bun: (version) => ['bun', 'add', '-g', `create-nevela@${version}`],
19
25
  };
20
26
 
21
27
  /**
22
- * How the running copy was installed, from the folder it is in.
28
+ * How the running copy was installed, from the folder it is in and the folder it was
29
+ * started through.
30
+ *
31
+ * Those two differ under pnpm 11, which keeps the files of every package in its store and
32
+ * leaves a link to them in its global folder. Node reports the store; the command was
33
+ * started through the global folder. Going by the store alone, a pnpm install looked like
34
+ * an npm one, and `nevela update` updated a copy that was not the one running.
23
35
  *
24
- * Returns the package manager and the command that updates it, or null when this copy
36
+ * Returns the package manager and `command(version)`, which updates it, or null when this copy
25
37
  * isn't the installed command: it was fetched for a single run (npx, pnpm dlx, a
26
38
  * self-update), it is the repository itself, or it is a dependency in some project's own
27
39
  * node_modules. Updating the global command would not change the copy that is running in
28
40
  * any of those cases.
29
41
  *
30
- * @param {string} dir The folder this package is running from
42
+ * @param {string} dir The folder this package's files are in
31
43
  * @param {(file: string) => boolean} exists Whether a file exists; replaceable in tests
44
+ * @param {string} startedFrom The folder it was started through, when that is another
32
45
  */
33
- export function globalInstall(dir, exists = fs.existsSync) {
34
- const plain = dir.replace(/\\/g, '/').replace(/\/+$/, '');
46
+ export function globalInstall(dir, exists = fs.existsSync, startedFrom = dir) {
47
+ const tidy = (folder) => folder.replace(/\\/g, '/').replace(/\/+$/, '');
48
+ const plain = tidy(dir);
35
49
  const where = plain.toLowerCase();
50
+ const started = tidy(startedFrom).toLowerCase();
51
+ const found = (manager) => ({ manager, command: (version = 'latest') => INSTALL[manager](version) });
36
52
 
37
53
  // Fetched for one run and thrown away afterwards.
38
- if (/\/_npx\/|\/dlx[-/]|\/\.?bunx-|\/xfs-[0-9a-f]+\/|\/create-nevela-\d[^/]*\/package$/.test(where)) return null;
54
+ if ([where, started].some((folder) => /\/_npx\/|\/dlx[-/]|\/\.?bunx-|\/xfs-[0-9a-f]+\/|\/create-nevela-\d[^/]*\/package$/.test(folder))) return null;
39
55
  // Installed, as opposed to checked out: it sits in somebody's node_modules.
40
56
  if (!/\/node_modules\/create-nevela$/.test(where)) return null;
41
57
 
42
58
  // pnpm, yarn and bun each keep global packages in a folder of their own.
43
- if (/\/pnpm\/global\//.test(where)) return { manager: 'pnpm', command: INSTALL.pnpm };
44
- if (/\/\.bun\/install\/global\//.test(where)) return { manager: 'bun', command: INSTALL.bun };
45
- if (/\/yarn\/(data\/)?global\/|\/\.config\/yarn\/global\//.test(where)) return { manager: 'yarn', command: INSTALL.yarn };
59
+ for (const folder of [started, where]) {
60
+ if (!/\/node_modules\/create-nevela$/.test(folder)) continue;
61
+ if (/\/pnpm\/global\/|\/global\/v?\d+\/([^/]+\/)?node_modules\/create-nevela$/.test(folder)) return found('pnpm');
62
+ if (/\/\.bun\/install\/global\//.test(folder)) return found('bun');
63
+ if (/\/yarn\/(data\/)?global\/|\/\.config\/yarn\/global\//.test(folder)) return found('yarn');
64
+ }
65
+ // In pnpm's store, and not reached through its global folder: a project's dependency.
66
+ if (/\/store\/v?\d+\/links\//.test(where)) return null;
46
67
 
47
68
  // npm's global folder is a plain node_modules, like a project's. Two things tell them
48
69
  // apart: a project has a package.json beside its node_modules, and a dependency of a
@@ -51,7 +72,7 @@ export function globalInstall(dir, exists = fs.existsSync) {
51
72
  const owner = plain.slice(0, where.lastIndexOf('/node_modules/'));
52
73
  if (exists(`${owner}/package.json`)) return null;
53
74
 
54
- return { manager: 'npm', command: INSTALL.npm };
75
+ return found('npm');
55
76
  }
56
77
 
57
78
  /**
@@ -90,14 +111,18 @@ export function copiesOnPath(env = process.env, { exists = fs.existsSync, real =
90
111
  return found;
91
112
  }
92
113
 
93
- /** What removes the copy whose command is in `dir`, judged by the folder. */
94
- export function removeCommand(dir) {
114
+ /**
115
+ * What removes the copy whose command is in `dir`, judged by the folder and by the
116
+ * launcher in it: a small script that names the folder the package itself is in.
117
+ */
118
+ export function removeCommand(dir, launcher = '') {
95
119
  const where = dir.replace(/\\/g, '/').toLowerCase();
96
- if (/\/pnpm(\/|$)/.test(where)) return 'pnpm remove -g create-nevela';
120
+ const points = launcher.replace(/\\/g, '/').toLowerCase();
121
+ if (/\/pnpm(\/|$)/.test(where) || /\/global\/v?\d+\/([^/]+\/)?node_modules\/create-nevela\//.test(points)) return 'pnpm remove -g create-nevela';
97
122
  if (/\/\.bun\//.test(where)) return 'bun remove -g create-nevela';
98
123
  if (/\/yarn\/|\/\.yarn\//.test(where)) return 'yarn global remove create-nevela';
99
124
  return 'npm uninstall -g create-nevela';
100
125
  }
101
126
 
102
127
  /** What to type to get the `nevela` command, for someone who doesn't have it yet. */
103
- export const INSTALL_COMMAND = INSTALL.npm.join(' ').replace('@latest', '');
128
+ export const INSTALL_COMMAND = 'npm install -g create-nevela';