create-nevela 0.2.1 → 0.3.0
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/README.md +17 -6
- package/index.mjs +77 -16
- package/package.json +2 -1
- package/project.mjs +9 -4
- package/selfupdate.mjs +13 -3
- package/template/laravel/README.md +1 -1
- package/template/laravel/src/Console/StatusCommand.php +2 -2
- package/template/laravel/src/Console/UpdateCommand.php +12 -6
- package/template/laravel/src/Generator/ResourceGenerator.php +3 -2
- package/template/laravel/src/Nevela.php +1 -1
- package/template/laravel/src/Support/Descriptor.php +3 -3
- package/template/laravel/src/Support/Field.php +29 -2
- package/template/web/package.json +1 -1
- package/template.mjs +1 -1
- package/tool.mjs +58 -0
package/README.md
CHANGED
|
@@ -36,21 +36,32 @@ php nevela resource Product --fields="name:string, sku:string!, price:money"
|
|
|
36
36
|
|
|
37
37
|
That writes the Laravel code and the dashboard pages, and creates the table. `php nevela` lists everything else.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## The nevela command
|
|
40
|
+
|
|
41
|
+
Install this package once and you have a `nevela` command, in every shell:
|
|
40
42
|
|
|
41
43
|
```sh
|
|
42
|
-
|
|
44
|
+
npm install -g create-nevela
|
|
43
45
|
```
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
```sh
|
|
48
|
+
nevela new my-app # anywhere: create an app
|
|
49
|
+
nevela update # anywhere: update the nevela command itself
|
|
50
|
+
nevela dev # inside an app: run the API and the dashboard
|
|
51
|
+
nevela upgrade # inside an app: bring it to the latest Nevela
|
|
52
|
+
```
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
As in Grit, `update` is for the tool and `upgrade` is for an app.
|
|
55
|
+
|
|
56
|
+
## Upgrading an app later
|
|
48
57
|
|
|
49
58
|
```sh
|
|
50
|
-
|
|
59
|
+
nevela upgrade
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
That updates the Laravel package, regenerates, and upgrades the dashboard files you have not changed. Files you changed are kept, and everything it replaces is backed up first.
|
|
63
|
+
|
|
64
|
+
Without the command installed, `npx create-nevela upgrade` does the same, in any shell and on an app of any age. So does the app's own launcher, `php nevela upgrade`. See https://nevela-docs.vercel.app/guides/updating/.
|
|
54
65
|
|
|
55
66
|
## Options
|
|
56
67
|
|
package/index.mjs
CHANGED
|
@@ -12,10 +12,11 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import readline from 'node:readline/promises';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { latestRelease, onPackagist } from './packagist.mjs';
|
|
16
|
-
import { COMMANDS, findProject, forward,
|
|
15
|
+
import { isNewer, latestRelease, onPackagist } from './packagist.mjs';
|
|
16
|
+
import { COMMANDS, findProject, forward, upgrade } from './project.mjs';
|
|
17
17
|
import { shellLine, spawnCommand, windows } from './shell.mjs';
|
|
18
|
-
import { newerVersion, runVersion } from './selfupdate.mjs';
|
|
18
|
+
import { latestVersion, newerVersion, runVersion } from './selfupdate.mjs';
|
|
19
|
+
import { globalInstall, INSTALL_COMMAND } from './tool.mjs';
|
|
19
20
|
import { copyLaravelPackage, copyWebTemplate, fromRepository, templateRecord } from './template.mjs';
|
|
20
21
|
|
|
21
22
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,12 +42,15 @@ const HELP = `
|
|
|
41
42
|
|
|
42
43
|
Create a new Nevela app: a Laravel API and a Next.js dashboard.
|
|
43
44
|
|
|
44
|
-
${bold('
|
|
45
|
-
|
|
46
|
-
npm create nevela@latest <name> -- [options]
|
|
45
|
+
${bold('Get the nevela command')} (once; everything below is then this short)
|
|
46
|
+
npm install -g create-nevela
|
|
47
47
|
|
|
48
|
-
${bold('
|
|
49
|
-
nevela
|
|
48
|
+
${bold('Anywhere')}
|
|
49
|
+
nevela new <name> [options] Create an app. Without the command: pnpm create nevela <name>
|
|
50
|
+
nevela update Update the nevela command itself to the latest version
|
|
51
|
+
|
|
52
|
+
${bold('Inside an app')}
|
|
53
|
+
nevela upgrade Bring this app to the latest Nevela: the package and the dashboard
|
|
50
54
|
nevela dev Run the API and the dashboard
|
|
51
55
|
nevela status Check versions, migrations, users and the dashboard
|
|
52
56
|
nevela resource <Name> --fields="…" [--seed]
|
|
@@ -301,7 +305,7 @@ Everything else runs from this folder too, with \`php nevela\`:
|
|
|
301
305
|
\`\`\`sh
|
|
302
306
|
php nevela user # add someone who can sign in
|
|
303
307
|
php nevela status # versions, migrations, users, and what the dashboard is talking to
|
|
304
|
-
php nevela
|
|
308
|
+
php nevela upgrade # bring this app to the latest Nevela
|
|
305
309
|
php nevela # everything it can do
|
|
306
310
|
\`\`\`
|
|
307
311
|
|
|
@@ -328,25 +332,82 @@ async function inProject(name, args) {
|
|
|
328
332
|
fail(`"${name}" runs inside a Nevela app, and this folder isn't in one.`, `To create an app: pnpm create nevela my-app`);
|
|
329
333
|
}
|
|
330
334
|
if (name === 'version') console.log(`create-nevela ${VERSION}`);
|
|
331
|
-
if (name !== '
|
|
335
|
+
if (name !== 'upgrade') process.exit(forward(project, name, args));
|
|
332
336
|
|
|
333
|
-
//
|
|
337
|
+
// Upgrading is the one command that must not run on stale code itself: it is the newest
|
|
338
|
+
// installer that knows what an older app needs repaired. So even a nevela command that
|
|
339
|
+
// hasn't been updated in a while upgrades an app correctly.
|
|
334
340
|
if (!process.env.NEVELA_NO_SELF_UPDATE && !fromRepository) {
|
|
335
341
|
const newer = await newerVersion(VERSION);
|
|
336
342
|
if (newer) {
|
|
337
|
-
const status = await runVersion(newer, ['
|
|
343
|
+
const status = await runVersion(newer, ['upgrade', ...args]);
|
|
338
344
|
if (status !== null) process.exit(status);
|
|
339
345
|
}
|
|
340
346
|
}
|
|
341
|
-
console.log(`\n ${indigo(bold('Nevela'))} ${dim('
|
|
347
|
+
console.log(`\n ${indigo(bold('Nevela'))} ${dim('upgrade')}\n`);
|
|
342
348
|
const releases = await latestRelease();
|
|
343
|
-
const status = await
|
|
349
|
+
const status = await upgrade(project, args, { latest: releases, say: (line) => console.log(` ${green('✔')} ${line}`) });
|
|
344
350
|
process.exit(status);
|
|
345
351
|
}
|
|
346
352
|
|
|
353
|
+
/**
|
|
354
|
+
* `nevela update`, from anywhere: update the nevela command itself.
|
|
355
|
+
*
|
|
356
|
+
* As in Grit, updating the tool and upgrading an app are two commands. This one doesn't
|
|
357
|
+
* touch any app; it ends by saying how to upgrade the one you are in, if you are in one.
|
|
358
|
+
*
|
|
359
|
+
* It returns instead of calling process.exit(): on Windows, exiting in the same moment a
|
|
360
|
+
* fetch() has just finished trips an assertion inside Node and prints a crash.
|
|
361
|
+
*/
|
|
362
|
+
async function updateTool(args) {
|
|
363
|
+
const project = findProject(process.cwd());
|
|
364
|
+
const install = fromRepository ? null : globalInstall(here);
|
|
365
|
+
|
|
366
|
+
if (!install) {
|
|
367
|
+
// Run through npx or `pnpm dlx`, which fetch the latest every time, started by an
|
|
368
|
+
// older nevela command that fetched this one to do its work, or a copy in a project's
|
|
369
|
+
// own node_modules. There is no installed command to update. Inside an app, "update" used to mean "upgrade this app", and
|
|
370
|
+
// instructions saying so are still around, so that is what it does.
|
|
371
|
+
if (project) {
|
|
372
|
+
console.log(`\n ${dim(`"update" now updates the nevela command, and an app is brought up to date with "upgrade". Upgrading this app.`)}`);
|
|
373
|
+
await inProject('upgrade', args);
|
|
374
|
+
}
|
|
375
|
+
console.log(`\n ${indigo(bold('Nevela'))} ${dim(`v${VERSION}`)}\n`);
|
|
376
|
+
console.log(` The nevela command isn't installed on this computer, so there is nothing to update.`);
|
|
377
|
+
console.log(` To install it: ${bold(INSTALL_COMMAND)}`);
|
|
378
|
+
console.log(` Then, anywhere: ${bold('nevela new my-app')} and inside an app: ${bold('nevela upgrade')}, ${bold('nevela dev')}\n`);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`\n ${indigo(bold('Nevela'))} ${dim('update')}\n`);
|
|
383
|
+
const latest = await latestVersion();
|
|
384
|
+
const newer = latest !== null && isNewer(latest, VERSION) ? latest : null;
|
|
385
|
+
if (latest === null) {
|
|
386
|
+
// Not the same as being up to date, so it isn't reported as that.
|
|
387
|
+
console.log(` ${red('!')} Couldn't reach npm to see whether there is a newer nevela than ${bold(VERSION)}. Nothing was changed.`);
|
|
388
|
+
console.log(` Try again when you are online, or run: ${install.command.join(' ')}`);
|
|
389
|
+
process.exitCode = 1;
|
|
390
|
+
} else if (!newer) {
|
|
391
|
+
console.log(` ${green('✔')} nevela ${bold(VERSION)} is the latest.`);
|
|
392
|
+
} else {
|
|
393
|
+
console.log(` Updating the nevela command ${bold(VERSION)} → ${bold(newer)} ${dim(`(${install.command.join(' ')})`)}\n`);
|
|
394
|
+
const [command, ...rest] = install.command;
|
|
395
|
+
const result = spawnCommand(command, rest, { stdio: 'inherit' });
|
|
396
|
+
if (result.error || result.status !== 0) {
|
|
397
|
+
fail(`Couldn't update the nevela command.`, `Run it yourself to see why: ${install.command.join(' ')}`);
|
|
398
|
+
}
|
|
399
|
+
console.log(`\n ${green('✔')} nevela is now ${bold(newer)}.`);
|
|
400
|
+
}
|
|
401
|
+
if (project) {
|
|
402
|
+
console.log(` ${dim('This app is not changed by that. To bring it to the latest Nevela:')} ${bold('nevela upgrade')}`);
|
|
403
|
+
}
|
|
404
|
+
console.log('');
|
|
405
|
+
}
|
|
406
|
+
|
|
347
407
|
async function main() {
|
|
348
408
|
const argv = process.argv.slice(2);
|
|
349
409
|
if (argv[0] === 'new') argv.shift();
|
|
410
|
+
else if (argv[0] === 'update') return updateTool(argv.slice(1));
|
|
350
411
|
else if (COMMANDS.includes(argv[0])) await inProject(argv[0], argv.slice(1));
|
|
351
412
|
const options = parseArgs(argv);
|
|
352
413
|
|
|
@@ -399,7 +460,7 @@ async function main() {
|
|
|
399
460
|
write(path.join(web, 'pnpm-workspace.yaml'), 'allowBuilds:\n sharp: true\n');
|
|
400
461
|
}
|
|
401
462
|
// Which dashboard this app started from, with a fingerprint of every file in it.
|
|
402
|
-
// `nevela
|
|
463
|
+
// `nevela upgrade` compares against these to tell the files you changed from the ones
|
|
403
464
|
// you didn't, so an update never overwrites your work.
|
|
404
465
|
write(path.join(web, '.nevela.json'), `${JSON.stringify(templateRecord(VERSION), null, 4)}\n`);
|
|
405
466
|
writeRootFiles(root, { name, title, pm, admin: options.user ? ADMIN : null });
|
|
@@ -510,7 +571,7 @@ async function main() {
|
|
|
510
571
|
console.log(` ${dim(`That account is in this app's local database only. Add your own: ${nevela} user`)}`);
|
|
511
572
|
}
|
|
512
573
|
console.log(`\n Add your first resource: ${dim(`${nevela} resource Product --fields="name:string, price:money"`)}`);
|
|
513
|
-
console.log(` Check the app: ${dim(`${nevela} status`)}
|
|
574
|
+
console.log(` Check the app: ${dim(`${nevela} status`)} Upgrade it later: ${dim(`${nevela} upgrade`)} Everything: ${dim(nevela)}`);
|
|
514
575
|
if (nevela !== 'php nevela') {
|
|
515
576
|
console.log(`\n ${dim(`In this shell "php" isn't a command (your PHP is php.bat), so use ${nevela}. In PowerShell, php nevela works too.`)}`);
|
|
516
577
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nevela",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"selfupdate.mjs",
|
|
15
15
|
"shell.mjs",
|
|
16
16
|
"project.mjs",
|
|
17
|
+
"tool.mjs",
|
|
17
18
|
"template",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
package/project.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The same commands as the app's own `php nevela`, but started from Node. That matters in
|
|
4
4
|
// shells where typing `php` doesn't work (Git Bash with Laravel Herd, where PHP is
|
|
5
|
-
// php.bat), and for `
|
|
5
|
+
// php.bat), and for `upgrade`, which has to work on an app of any age, including one from
|
|
6
6
|
// before the app had a launcher at all.
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
|
@@ -11,7 +11,7 @@ import { isNewer, satisfiesCaret } from './packagist.mjs';
|
|
|
11
11
|
import { spawnCommand } from './shell.mjs';
|
|
12
12
|
|
|
13
13
|
/** What `nevela <name>` runs: these go to `php artisan nevela:<name>`. */
|
|
14
|
-
const NEVELA = ['
|
|
14
|
+
const NEVELA = ['upgrade', 'dev', 'status', 'version', 'resource', 'generate', 'seed', 'user'];
|
|
15
15
|
/** These go to the artisan command of the same name. */
|
|
16
16
|
const ARTISAN = ['migrate', 'tinker', 'test', 'serve'];
|
|
17
17
|
|
|
@@ -108,8 +108,13 @@ function inApp(project, command, args) {
|
|
|
108
108
|
return result.status ?? 1;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* `nevela upgrade`: bring an app of any age to the latest Nevela.
|
|
113
|
+
*
|
|
114
|
+
* The artisan command is called by its older name, nevela:update, which every version of
|
|
115
|
+
* the package answers to (from 0.3.0 it is an alias of nevela:upgrade).
|
|
116
|
+
*/
|
|
117
|
+
export async function upgrade(project, args, { latest, say }) {
|
|
113
118
|
const check = args.includes('--check');
|
|
114
119
|
const manifestFile = path.join(project.api, 'composer.json');
|
|
115
120
|
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
package/selfupdate.mjs
CHANGED
|
@@ -14,17 +14,27 @@ import { isNewer } from './packagist.mjs';
|
|
|
14
14
|
|
|
15
15
|
const REGISTRY = 'https://registry.npmjs.org/create-nevela';
|
|
16
16
|
|
|
17
|
-
/** The newest create-nevela on npm
|
|
18
|
-
export async function
|
|
17
|
+
/** The newest create-nevela on npm, or null when npm couldn't be asked. Never throws. */
|
|
18
|
+
export async function latestVersion() {
|
|
19
19
|
try {
|
|
20
20
|
const response = await fetch(`${REGISTRY}/latest`, { signal: AbortSignal.timeout(4000) });
|
|
21
21
|
const latest = (await response.json()).version;
|
|
22
|
-
return
|
|
22
|
+
return typeof latest === 'string' && /^\d+\.\d+\.\d+$/.test(latest) ? latest : null;
|
|
23
23
|
} catch {
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The newest create-nevela on npm when it is newer than `current`, else null. Never throws.
|
|
30
|
+
* Null also when npm couldn't be asked: callers that only want to run the newest carry on
|
|
31
|
+
* with what they have.
|
|
32
|
+
*/
|
|
33
|
+
export async function newerVersion(current) {
|
|
34
|
+
const latest = await latestVersion();
|
|
35
|
+
return latest !== null && isNewer(latest, current) ? latest : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
/**
|
|
29
39
|
* Unpack a .tar (already gunzipped) into `dir`. Enough of the format for an npm tarball:
|
|
30
40
|
* regular files and directories, with the long-name extensions npm uses.
|
|
@@ -43,7 +43,7 @@ Needs PHP 8.2 or newer and Laravel 11, 12 or 13.
|
|
|
43
43
|
| `nevela:generate` | Regenerate after changing a descriptor. Your own code is kept. |
|
|
44
44
|
| `nevela:seed` | Fill a resource with plausible records, and print how long it took. |
|
|
45
45
|
| `nevela:user` | Create someone who can sign in. |
|
|
46
|
-
| `nevela:
|
|
46
|
+
| `nevela:upgrade` | Bring the app to the latest Nevela: the package, the generated code and the dashboard. `nevela:update` is its older name and still works. |
|
|
47
47
|
|
|
48
48
|
Full documentation: https://nevela-docs.vercel.app
|
|
49
49
|
|
|
@@ -30,7 +30,7 @@ final class StatusCommand extends Command
|
|
|
30
30
|
$latest = Releases::latest();
|
|
31
31
|
$row('Nevela', Nevela::VERSION.match (true) {
|
|
32
32
|
$latest === null => ' <fg=gray>(couldn\'t check for a newer one)</>',
|
|
33
|
-
version_compare($latest, Nevela::VERSION, '>') => " <fg=yellow>({$latest} is out: php nevela
|
|
33
|
+
version_compare($latest, Nevela::VERSION, '>') => " <fg=yellow>({$latest} is out: php nevela upgrade)</>",
|
|
34
34
|
default => ' <fg=green>(latest)</>',
|
|
35
35
|
});
|
|
36
36
|
$row('Laravel / PHP', app()->version().' / '.PHP_VERSION);
|
|
@@ -99,7 +99,7 @@ final class StatusCommand extends Command
|
|
|
99
99
|
// What this app has made its own. An update leaves every one of these alone.
|
|
100
100
|
$changes = $state->yourChanges($web);
|
|
101
101
|
if ($changes === null) {
|
|
102
|
-
$row('Your dashboard changes', '<fg=gray>not tracked yet: php nevela
|
|
102
|
+
$row('Your dashboard changes', '<fg=gray>not tracked yet: php nevela upgrade records them</>');
|
|
103
103
|
} else {
|
|
104
104
|
$count = count($changes['changed']) + count($changes['deleted']);
|
|
105
105
|
$row('Your dashboard changes', $count === 0 ? 'none' : "{$count} file(s), kept on every update");
|
|
@@ -16,12 +16,18 @@ use Throwable;
|
|
|
16
16
|
|
|
17
17
|
final class UpdateCommand extends Command
|
|
18
18
|
{
|
|
19
|
-
protected $signature = 'nevela:
|
|
19
|
+
protected $signature = 'nevela:upgrade
|
|
20
20
|
{--check : Show what would change, and change nothing}
|
|
21
|
-
{--skip-package : Leave the nevela/laravel package as it is;
|
|
22
|
-
{--undo : Put the dashboard back as it was before the last
|
|
21
|
+
{--skip-package : Leave the nevela/laravel package as it is; upgrade only the dashboard}
|
|
22
|
+
{--undo : Put the dashboard back as it was before the last upgrade}';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The name this command had before 0.3.0, and still answers to. As in Grit, "update"
|
|
26
|
+
* is now what the nevela command does to itself, and "upgrade" is what it does to an app.
|
|
27
|
+
*/
|
|
28
|
+
protected $aliases = ['nevela:update'];
|
|
29
|
+
|
|
30
|
+
protected $description = 'Bring this app to the latest Nevela: the package, the generated code and the dashboard';
|
|
25
31
|
|
|
26
32
|
/** Apps created before the dashboard recorded its version all came from this template. */
|
|
27
33
|
private const FIRST_TRACKED = '0.1.1';
|
|
@@ -184,7 +190,7 @@ final class UpdateCommand extends Command
|
|
|
184
190
|
// The package's own files have just been replaced. Finish in a new process, so the
|
|
185
191
|
// rest runs on the new code instead of a mix of old and new.
|
|
186
192
|
$php = (new PhpExecutableFinder)->find(false) ?: 'php';
|
|
187
|
-
$rest = new Process([$php, 'artisan', 'nevela:
|
|
193
|
+
$rest = new Process([$php, 'artisan', 'nevela:upgrade', '--skip-package'], base_path(), null, null, null);
|
|
188
194
|
$rest->run(fn ($type, $buffer) => $this->output->write($buffer));
|
|
189
195
|
|
|
190
196
|
return $rest->getExitCode() ?? self::FAILURE;
|
|
@@ -312,7 +318,7 @@ final class UpdateCommand extends Command
|
|
|
312
318
|
$state->write($web);
|
|
313
319
|
|
|
314
320
|
if ($touched) {
|
|
315
|
-
$this->line(" Every file this replaced was copied to <fg=cyan>{$this->relative($web)}/{$backup}</> first. To put it all back: <fg=cyan>php nevela
|
|
321
|
+
$this->line(" Every file this replaced was copied to <fg=cyan>{$this->relative($web)}/{$backup}</> first. To put it all back: <fg=cyan>php nevela upgrade --undo</>");
|
|
316
322
|
}
|
|
317
323
|
}
|
|
318
324
|
if ($done[DashboardUpdate::CONFLICT] !== []) {
|
|
@@ -225,7 +225,8 @@ final class ResourceGenerator
|
|
|
225
225
|
\$name = array_shift(\$args) ?? 'help';
|
|
226
226
|
|
|
227
227
|
// What you type => the artisan command it runs.
|
|
228
|
-
|
|
228
|
+
// "update" is what upgrade was called before 0.3.0; it still works.
|
|
229
|
+
\$nevela = ['resource', 'generate', 'seed', 'user', 'upgrade', 'update', 'dev', 'status', 'version'];
|
|
229
230
|
\$artisan = ['migrate', 'tinker', 'test', 'serve'];
|
|
230
231
|
if (in_array(\$name, ['--version', '-v', '-V'], true)) {
|
|
231
232
|
\$name = 'version';
|
|
@@ -254,7 +255,7 @@ final class ResourceGenerator
|
|
|
254
255
|
php nevela generate regenerate after editing a descriptor
|
|
255
256
|
php nevela seed Product fill a resource with records
|
|
256
257
|
php nevela user create someone who can sign in
|
|
257
|
-
php nevela
|
|
258
|
+
php nevela upgrade bring this app to the latest Nevela
|
|
258
259
|
php nevela version which Nevela this is
|
|
259
260
|
|
|
260
261
|
php nevela migrate php artisan migrate
|
|
@@ -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.
|
|
21
|
+
public const VERSION = '0.3.0';
|
|
22
22
|
|
|
23
23
|
/** @var array<string, Descriptor> */
|
|
24
24
|
private static array $descriptors = [];
|
|
@@ -148,16 +148,16 @@ final class Descriptor
|
|
|
148
148
|
return str_replace('-', '_', Naming::snake($this->name));
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
/** Split on commas that are not inside
|
|
151
|
+
/** Split on commas that are not inside parentheses or square brackets. @return list<string> */
|
|
152
152
|
private static function splitSpec(string $spec): array
|
|
153
153
|
{
|
|
154
154
|
$parts = [];
|
|
155
155
|
$depth = 0;
|
|
156
156
|
$current = '';
|
|
157
157
|
foreach (str_split($spec) as $char) {
|
|
158
|
-
if ($char === '(') {
|
|
158
|
+
if ($char === '(' || $char === '[') {
|
|
159
159
|
$depth++;
|
|
160
|
-
} elseif ($char === ')') {
|
|
160
|
+
} elseif ($char === ')' || $char === ']') {
|
|
161
161
|
$depth--;
|
|
162
162
|
}
|
|
163
163
|
if ($char === ',' && $depth === 0) {
|
|
@@ -106,12 +106,15 @@ final class Field
|
|
|
106
106
|
* category:belongsTo(Category) stored as categoryId
|
|
107
107
|
* image:image an optimised image; image:image(product) names a profile
|
|
108
108
|
* manual:file(pdf|document) any other upload
|
|
109
|
+
*
|
|
110
|
+
* Grit's and Flare's spellings are understood too: see normalise().
|
|
109
111
|
*/
|
|
110
112
|
public static function parse(string $token): self
|
|
111
113
|
{
|
|
112
|
-
$
|
|
114
|
+
$written = trim($token);
|
|
115
|
+
$token = self::normalise($written);
|
|
113
116
|
if (! preg_match('/^([A-Za-z][A-Za-z0-9_]*):([A-Za-z_]+)(?:\(([^)]*)\))?([?!]{0,2})$/', $token, $m)) {
|
|
114
|
-
throw new InvalidArgumentException("Invalid field \"{$
|
|
117
|
+
throw new InvalidArgumentException("Invalid field \"{$written}\". Expected name:type, e.g. price:money, status:enum(draft|live)?, image:image or category:belongsTo(Category).");
|
|
115
118
|
}
|
|
116
119
|
[, $name, $type, $args, $suffix] = $m + [3 => '', 4 => ''];
|
|
117
120
|
$name = Naming::camel($name);
|
|
@@ -142,6 +145,30 @@ final class Field
|
|
|
142
145
|
return new self($name, $kind, $format, $required, $unique);
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
/**
|
|
149
|
+
* The other ways people write a field, turned into Nevela's own.
|
|
150
|
+
*
|
|
151
|
+
* Grit puts what a type takes after a second colon, and Flare in square brackets, so
|
|
152
|
+
* someone coming from either types what they know:
|
|
153
|
+
*
|
|
154
|
+
* image:file:image image:file[image] image:file:[image] → image:file(image)
|
|
155
|
+
* category:belongs_to:Category → category:belongs_to(Category)
|
|
156
|
+
* status:enum:draft|live docs:file:[pdf, image] → …(draft|live), …(pdf|image)
|
|
157
|
+
*/
|
|
158
|
+
private static function normalise(string $token): string
|
|
159
|
+
{
|
|
160
|
+
// Spaces around the punctuation don't mean anything: "file: [ image ]".
|
|
161
|
+
$token = (string) preg_replace('/\s*([:\[\]])\s*/', '$1', $token);
|
|
162
|
+
if (preg_match('/^([^:()\[\]]+):([A-Za-z_]+):?\[([^\[\]()]*)\]([?!]{0,2})$/', $token, $m)
|
|
163
|
+
|| preg_match('/^([^:()\[\]]+):([A-Za-z_]+):([^:()\[\]?!]+)([?!]{0,2})$/', $token, $m)) {
|
|
164
|
+
$list = implode('|', array_filter(array_map('trim', preg_split('/[,|]/', $m[3])), fn ($value) => $value !== ''));
|
|
165
|
+
|
|
166
|
+
return "{$m[1]}:{$m[2]}({$list}){$m[4]}";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return $token;
|
|
170
|
+
}
|
|
171
|
+
|
|
145
172
|
/** @param array<string, mixed> $data */
|
|
146
173
|
public static function fromArray(string $name, array $data): self
|
|
147
174
|
{
|
package/template.mjs
CHANGED
|
@@ -81,7 +81,7 @@ export function copyWebTemplate(to, { name, title }) {
|
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* A fingerprint of every template file, and the template's dependencies: what the app
|
|
84
|
-
* records in .nevela.json so that `nevela
|
|
84
|
+
* records in .nevela.json so that `nevela upgrade` can tell, later and exactly, which
|
|
85
85
|
* files the developer changed. The same fingerprint the Laravel package computes: SHA-1
|
|
86
86
|
* with line endings ignored.
|
|
87
87
|
*/
|
package/tool.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// `nevela update`: the nevela command updating itself, from any folder.
|
|
2
|
+
//
|
|
3
|
+
// Two different things get newer, and they have two different commands, as in Grit:
|
|
4
|
+
//
|
|
5
|
+
// nevela update the `nevela` command on this computer
|
|
6
|
+
// nevela upgrade the app you are in: its Nevela package and its dashboard
|
|
7
|
+
//
|
|
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.
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
|
|
13
|
+
/** The command that installs the latest create-nevela globally, for each package manager. */
|
|
14
|
+
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'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* How the running copy was installed, from the folder it is in.
|
|
23
|
+
*
|
|
24
|
+
* Returns the package manager and the command that updates it, or null when this copy
|
|
25
|
+
* isn't the installed command: it was fetched for a single run (npx, pnpm dlx, a
|
|
26
|
+
* self-update), it is the repository itself, or it is a dependency in some project's own
|
|
27
|
+
* node_modules. Updating the global command would not change the copy that is running in
|
|
28
|
+
* any of those cases.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} dir The folder this package is running from
|
|
31
|
+
* @param {(file: string) => boolean} exists Whether a file exists; replaceable in tests
|
|
32
|
+
*/
|
|
33
|
+
export function globalInstall(dir, exists = fs.existsSync) {
|
|
34
|
+
const plain = dir.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
35
|
+
const where = plain.toLowerCase();
|
|
36
|
+
|
|
37
|
+
// 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;
|
|
39
|
+
// Installed, as opposed to checked out: it sits in somebody's node_modules.
|
|
40
|
+
if (!/\/node_modules\/create-nevela$/.test(where)) return null;
|
|
41
|
+
|
|
42
|
+
// 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 };
|
|
46
|
+
|
|
47
|
+
// npm's global folder is a plain node_modules, like a project's. Two things tell them
|
|
48
|
+
// apart: a project has a package.json beside its node_modules, and a dependency of a
|
|
49
|
+
// dependency (or pnpm's local store) sits in a node_modules inside another.
|
|
50
|
+
if (where.split('/node_modules/').length !== 2) return null;
|
|
51
|
+
const owner = plain.slice(0, where.lastIndexOf('/node_modules/'));
|
|
52
|
+
if (exists(`${owner}/package.json`)) return null;
|
|
53
|
+
|
|
54
|
+
return { manager: 'npm', command: INSTALL.npm };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What to type to get the `nevela` command, for someone who doesn't have it yet. */
|
|
58
|
+
export const INSTALL_COMMAND = INSTALL.npm.join(' ').replace('@latest', '');
|