create-nevela 0.1.0 → 0.1.2
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 +16 -5
- package/index.mjs +142 -63
- package/package.json +1 -1
- package/packagist.mjs +34 -25
- package/template/laravel/README.md +1 -0
- package/template/laravel/config/nevela.php +7 -0
- package/template/laravel/src/Console/GenerateCommand.php +107 -77
- package/template/laravel/src/Console/MakeResourceCommand.php +9 -2
- package/template/laravel/src/Console/UpdateCommand.php +304 -0
- package/template/laravel/src/Generator/GeneratedFile.php +11 -2
- package/template/laravel/src/Generator/ResourceGenerator.php +719 -639
- package/template/laravel/src/Nevela.php +1 -1
- package/template/laravel/src/NevelaServiceProvider.php +66 -65
- package/template/laravel/src/Support/DashboardUpdate.php +110 -0
- package/template/web/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,23 +26,34 @@ cd my-app
|
|
|
26
26
|
pnpm run dev
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Open http://localhost:3000/sign-in.
|
|
29
|
+
Open http://localhost:3000/sign-in and sign in as `admin@example.com` with the password `password`. That starter account is in your local database only; add your own with `php artisan nevela:user`.
|
|
30
|
+
|
|
31
|
+
Add your first resource, from the same folder:
|
|
30
32
|
|
|
31
33
|
```sh
|
|
32
|
-
php
|
|
33
|
-
php artisan migrate
|
|
34
|
+
php nevela resource Product --fields="name:string, sku:string!, price:money"
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
That writes the Laravel code and the dashboard pages, and creates the table. `php nevela` lists everything else.
|
|
38
|
+
|
|
39
|
+
## Updating an app later
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
php nevela update
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That updates the Laravel package, regenerates, and updates the dashboard files you have not changed. See https://nevela-docs.vercel.app/guides/updating/.
|
|
46
|
+
|
|
36
47
|
## Options
|
|
37
48
|
|
|
38
49
|
| Option | Meaning |
|
|
39
50
|
|---|---|
|
|
40
51
|
| `--pm <pnpm\|npm\|yarn\|bun>` | Package manager for the dashboard. Default: the one you ran this with. |
|
|
41
52
|
| `--no-install` | Don't install the dashboard's dependencies. |
|
|
42
|
-
| `--no-user` | Don't
|
|
53
|
+
| `--no-user` | Don't create the starter account. |
|
|
43
54
|
| `--no-git` | Don't run `git init`. |
|
|
44
55
|
| `--bundled-package` | Use the copy of `nevela/laravel` inside this installer instead of the release on Packagist. |
|
|
45
|
-
| `-y`, `--yes` | Ask nothing
|
|
56
|
+
| `-y`, `--yes` | Ask nothing. Only the app name is ever asked for. |
|
|
46
57
|
|
|
47
58
|
With npm, put options after `--`: `npm create nevela@latest my-app -- --no-install`.
|
|
48
59
|
|
package/index.mjs
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
//
|
|
7
7
|
// No dependencies on purpose: this runs before anything is installed.
|
|
8
8
|
|
|
9
|
-
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import readline from 'node:readline/promises';
|
|
13
14
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import { onPackagist } from './packagist.mjs';
|
|
15
|
+
import { isNewer, onPackagist } from './packagist.mjs';
|
|
15
16
|
import { copyLaravelPackage, copyWebTemplate } from './template.mjs';
|
|
16
17
|
|
|
17
18
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -19,6 +20,13 @@ const VERSION = JSON.parse(fs.readFileSync(path.join(here, 'package.json'), 'utf
|
|
|
19
20
|
const DOCS = 'https://nevela-docs.vercel.app';
|
|
20
21
|
const windows = process.platform === 'win32';
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The account every new app starts with, so there is something to sign in as straight
|
|
25
|
+
* away. It is created in the app's local database, never in code or a migration, so it
|
|
26
|
+
* does not follow the app to a server.
|
|
27
|
+
*/
|
|
28
|
+
const ADMIN = { name: 'Admin', email: 'admin@example.com', password: 'password' };
|
|
29
|
+
|
|
22
30
|
const colour = (code) => (text) => (process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
23
31
|
const bold = colour(1);
|
|
24
32
|
const dim = colour(2);
|
|
@@ -38,11 +46,11 @@ const HELP = `
|
|
|
38
46
|
${bold('Options')}
|
|
39
47
|
--pm <pnpm|npm|yarn|bun> Package manager for the dashboard. Default: the one you ran this with.
|
|
40
48
|
--no-install Don't install the dashboard's dependencies.
|
|
41
|
-
--no-user Don't
|
|
49
|
+
--no-user Don't create the starter admin account.
|
|
42
50
|
--no-git Don't run git init.
|
|
43
51
|
--bundled-package Use the copy of nevela/laravel that ships with this installer
|
|
44
52
|
instead of the release on Packagist.
|
|
45
|
-
-y, --yes Ask nothing
|
|
53
|
+
-y, --yes Ask nothing. (Only the app name is ever asked for.)
|
|
46
54
|
-h, --help Show this.
|
|
47
55
|
-v, --version Show the version.
|
|
48
56
|
|
|
@@ -75,10 +83,40 @@ function run(command, args, { cwd, interactive = false, allowFailure = false } =
|
|
|
75
83
|
*/
|
|
76
84
|
function spawnCommand(command, args, options) {
|
|
77
85
|
if (!windows) return spawnSync(command, args, options);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
return spawnSync(shellLine(command, args), { ...options, shell: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// `^` is not in the safe list on purpose: unquoted, cmd.exe treats it as an escape
|
|
90
|
+
// character and drops it, which turned the constraint "^0.1" into an exact "0.1".
|
|
91
|
+
const quote = (arg) => (/^[\w./:=@,-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`);
|
|
92
|
+
const shellLine = (command, args) => [command, ...args.map(quote)].join(' ');
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Start a command and carry on. Its output goes to a file rather than a pipe: the steps
|
|
96
|
+
* that follow block this process for minutes, and a pipe nobody is reading fills up and
|
|
97
|
+
* stalls the command.
|
|
98
|
+
*/
|
|
99
|
+
function start(command, args, { cwd }) {
|
|
100
|
+
const log = path.join(os.tmpdir(), `create-nevela-${process.pid}-${command}.log`);
|
|
101
|
+
const out = fs.openSync(log, 'w');
|
|
102
|
+
const started = Date.now();
|
|
103
|
+
const child = windows
|
|
104
|
+
? spawn(shellLine(command, args), { cwd, shell: true, stdio: ['ignore', out, out] })
|
|
105
|
+
: spawn(command, args, { cwd, stdio: ['ignore', out, out] });
|
|
106
|
+
const done = new Promise((resolve) => {
|
|
107
|
+
const finish = (ok) => {
|
|
108
|
+
fs.closeSync(out);
|
|
109
|
+
const output = fs.readFileSync(log, 'utf8');
|
|
110
|
+
// This process hears about the exit only once the blocking steps let it. The log's
|
|
111
|
+
// last write is when the command really finished.
|
|
112
|
+
const ended = Math.max(started, Math.min(Date.now(), fs.statSync(log).mtimeMs));
|
|
113
|
+
fs.rmSync(log, { force: true });
|
|
114
|
+
resolve({ ok, output, seconds: (ended - started) / 1000 });
|
|
115
|
+
};
|
|
116
|
+
child.on('error', () => finish(false));
|
|
117
|
+
child.on('close', (code) => finish(code === 0));
|
|
118
|
+
});
|
|
119
|
+
return { done };
|
|
82
120
|
}
|
|
83
121
|
|
|
84
122
|
function available(command, args = ['--version']) {
|
|
@@ -86,13 +124,18 @@ function available(command, args = ['--version']) {
|
|
|
86
124
|
return result.status === 0 ? `${result.stdout}${result.stderr}` : null;
|
|
87
125
|
}
|
|
88
126
|
|
|
127
|
+
const duration = (seconds) => (seconds >= 60 ? `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s` : `${seconds.toFixed(1)}s`);
|
|
128
|
+
|
|
89
129
|
async function step(label, work) {
|
|
90
130
|
const started = Date.now();
|
|
91
|
-
|
|
131
|
+
// The "in progress" line is rewritten in place when the step ends. That only works if
|
|
132
|
+
// it fits on one line; in a narrow terminal it would wrap and be left behind.
|
|
133
|
+
const inPlace = process.stdout.isTTY && label.length + 16 < (process.stdout.columns ?? 80);
|
|
134
|
+
if (inPlace) process.stdout.write(` ${dim('◇')} ${label}…`);
|
|
92
135
|
const result = await work();
|
|
93
|
-
const seconds = (Date.now() - started) / 1000;
|
|
94
|
-
const took = seconds >= 1 ? dim(` (${
|
|
95
|
-
process.stdout.write(
|
|
136
|
+
const seconds = result?.seconds ?? (Date.now() - started) / 1000;
|
|
137
|
+
const took = seconds >= 1 ? dim(` (${duration(seconds)})`) : '';
|
|
138
|
+
process.stdout.write(`${inPlace ? '\r' : ''} ${green('✔')} ${label}${took} \n`);
|
|
96
139
|
return result;
|
|
97
140
|
}
|
|
98
141
|
|
|
@@ -123,6 +166,17 @@ function parseArgs(argv) {
|
|
|
123
166
|
/** "my-shop" → "My Shop", for the dashboard's title. */
|
|
124
167
|
const titleCase = (slug) => slug.split(/[-_]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
|
|
125
168
|
|
|
169
|
+
/** The newest create-nevela on npm, when it is newer than this one. */
|
|
170
|
+
async function newerVersion() {
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch('https://registry.npmjs.org/create-nevela/latest', { signal: AbortSignal.timeout(4000) });
|
|
173
|
+
const latest = (await response.json()).version;
|
|
174
|
+
return isNewer(latest, VERSION) ? latest : null;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
126
180
|
function detectPackageManager(requested) {
|
|
127
181
|
const known = ['pnpm', 'npm', 'yarn', 'bun'];
|
|
128
182
|
if (requested) {
|
|
@@ -166,7 +220,7 @@ function write(file, contents) {
|
|
|
166
220
|
fs.writeFileSync(file, contents);
|
|
167
221
|
}
|
|
168
222
|
|
|
169
|
-
function writeRootFiles(root, { name, title, pm }) {
|
|
223
|
+
function writeRootFiles(root, { name, title, pm, admin }) {
|
|
170
224
|
write(path.join(root, 'package.json'), `${JSON.stringify({
|
|
171
225
|
name,
|
|
172
226
|
version: '0.1.0',
|
|
@@ -174,6 +228,7 @@ function writeRootFiles(root, { name, title, pm }) {
|
|
|
174
228
|
type: 'module',
|
|
175
229
|
scripts: {
|
|
176
230
|
dev: 'node scripts/dev.mjs',
|
|
231
|
+
nevela: 'php nevela',
|
|
177
232
|
'dev:api': 'cd apps/api && php artisan serve',
|
|
178
233
|
'dev:web': `cd apps/web && ${pm} run dev`,
|
|
179
234
|
},
|
|
@@ -236,23 +291,29 @@ ${pm} run dev
|
|
|
236
291
|
\`\`\`
|
|
237
292
|
|
|
238
293
|
That starts Laravel on http://127.0.0.1:8000 and the dashboard on http://localhost:3000. Sign in at http://localhost:3000/sign-in.
|
|
294
|
+
${admin ? `
|
|
295
|
+
The app starts with one account, in your local database only:
|
|
239
296
|
|
|
240
|
-
|
|
297
|
+
| | |
|
|
298
|
+
|---|---|
|
|
299
|
+
| Email | \`${admin.email}\` |
|
|
300
|
+
| Password | \`${admin.password}\` |
|
|
301
|
+
` : ''}
|
|
302
|
+
Everything else runs from this folder too, with \`php nevela\`:
|
|
241
303
|
|
|
242
304
|
\`\`\`sh
|
|
243
|
-
|
|
244
|
-
php
|
|
305
|
+
php nevela user # add someone who can sign in
|
|
306
|
+
php nevela update # update Nevela and the dashboard
|
|
307
|
+
php nevela # everything it can do
|
|
245
308
|
\`\`\`
|
|
246
309
|
|
|
247
310
|
## Add a resource
|
|
248
311
|
|
|
249
312
|
\`\`\`sh
|
|
250
|
-
|
|
251
|
-
php artisan nevela:resource Product --fields="name:string, sku:string!, price:money, notes:text?"
|
|
252
|
-
php artisan migrate
|
|
313
|
+
php nevela resource Product --fields="name:string, sku:string!, price:money, notes:text?"
|
|
253
314
|
\`\`\`
|
|
254
315
|
|
|
255
|
-
Reload the dashboard: Products is in the sidebar. Fill it with \`php
|
|
316
|
+
That creates the table as well. Reload the dashboard: Products is in the sidebar. Fill it with \`php nevela seed Product\`.
|
|
256
317
|
|
|
257
318
|
## Before real use
|
|
258
319
|
|
|
@@ -267,12 +328,21 @@ async function main() {
|
|
|
267
328
|
|
|
268
329
|
console.log(`\n ${indigo(bold('Nevela'))} ${dim(`v${VERSION}`)}\n`);
|
|
269
330
|
|
|
331
|
+
// A package manager may serve an older installer than the newest one: pnpm holds back
|
|
332
|
+
// versions published in the last day, and both pnpm and npm cache. Say so, with the fix.
|
|
333
|
+
const newer = await newerVersion();
|
|
334
|
+
const outdated = newer
|
|
335
|
+
? ` ${red('!')} create-nevela ${bold(newer)} is out, and this is ${VERSION}. To use it: ${bold(`pnpm create nevela@${newer} ${options.name ?? 'my-app'}`)}\n`
|
|
336
|
+
: null;
|
|
337
|
+
if (outdated) console.log(outdated);
|
|
338
|
+
|
|
270
339
|
let name = options.name;
|
|
271
|
-
const interactive = !options.yes && process.stdin.isTTY;
|
|
272
|
-
const prompt = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
273
340
|
if (!name) {
|
|
274
|
-
|
|
341
|
+
// The only question this ever asks, and only when the name was left off.
|
|
342
|
+
if (options.yes || !process.stdin.isTTY) fail('Give the app a name: pnpm create nevela my-app');
|
|
343
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
275
344
|
name = (await prompt.question(' What is the app called? ')).trim();
|
|
345
|
+
prompt.close();
|
|
276
346
|
}
|
|
277
347
|
if (!/^[a-z0-9][a-z0-9-_]*$/.test(name)) {
|
|
278
348
|
fail(`"${name}" can't be used as a name. Use lowercase letters, numbers and dashes, for example my-app.`);
|
|
@@ -286,78 +356,87 @@ async function main() {
|
|
|
286
356
|
const title = titleCase(name);
|
|
287
357
|
const api = path.join(root, 'apps', 'api');
|
|
288
358
|
const web = path.join(root, 'apps', 'web');
|
|
359
|
+
const began = Date.now();
|
|
289
360
|
fs.mkdirSync(path.join(root, 'apps'), { recursive: true });
|
|
290
361
|
|
|
362
|
+
// The dashboard first, so its packages can download while Composer works on Laravel.
|
|
363
|
+
// The two don't touch each other, and Composer is the long part.
|
|
364
|
+
copyWebTemplate(web, { name, title });
|
|
365
|
+
write(path.join(web, '.env.local'), '# Where the Laravel API is, including its prefix.\nNEVELA_API_URL=http://127.0.0.1:8000/api\n');
|
|
366
|
+
if (pm === 'pnpm') {
|
|
367
|
+
// Its own workspace file: lets sharp build, and keeps pnpm from adopting a workspace further up.
|
|
368
|
+
write(path.join(web, 'pnpm-workspace.yaml'), 'allowBuilds:\n sharp: true\n');
|
|
369
|
+
}
|
|
370
|
+
// Which dashboard this app started from: `php artisan nevela:update` compares against it
|
|
371
|
+
// to tell the files you changed from the ones you didn't.
|
|
372
|
+
write(path.join(web, '.nevela.json'), `${JSON.stringify({ template: VERSION }, null, 2)}\n`);
|
|
373
|
+
writeRootFiles(root, { name, title, pm, admin: options.user ? ADMIN : null });
|
|
374
|
+
const packages = options.install ? start(pm, ['install'], { cwd: web }) : null;
|
|
375
|
+
|
|
291
376
|
await step('Creating the Laravel app', () => {
|
|
292
377
|
run('composer', ['create-project', 'laravel/laravel', 'apps/api', '--no-interaction', '--no-progress', '--prefer-dist'], { cwd: root });
|
|
293
378
|
});
|
|
294
379
|
|
|
295
|
-
const tokens = await step('
|
|
296
|
-
run('php', ['artisan', 'install:api', '--no-interaction'], { cwd: api });
|
|
297
|
-
return addApiTokens(path.join(api, 'app', 'Models', 'User.php'));
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
await step('Installing Nevela', async () => {
|
|
380
|
+
const tokens = await step('Installing Nevela and token sign-in', async () => {
|
|
301
381
|
const wanted = VERSION.split('.').slice(0, 2).map(Number);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
// installer is placed in the app and installed by path.
|
|
382
|
+
let nevela = `nevela/laravel:^${wanted.join('.')}`;
|
|
383
|
+
if (options.bundledPackage || !(await onPackagist(wanted))) {
|
|
384
|
+
// No matching release on Packagist (or --bundled-package): the copy that travels with
|
|
385
|
+
// this installer is placed in the app and installed by path.
|
|
307
386
|
copyLaravelPackage(path.join(root, 'packages', 'nevela-laravel'));
|
|
308
387
|
run('composer', ['config', 'repositories.nevela', 'path', '../../packages/nevela-laravel'], { cwd: api });
|
|
309
|
-
|
|
388
|
+
nevela = 'nevela/laravel:@dev';
|
|
310
389
|
}
|
|
390
|
+
// One Composer run for both. `php artisan install:api` would add Sanctum in a run of
|
|
391
|
+
// its own, plus a routes/api.php this app doesn't use: Nevela registers its routes itself.
|
|
392
|
+
run('composer', ['require', 'laravel/sanctum', nevela, '--no-interaction', '--no-progress'], { cwd: api });
|
|
393
|
+
run('php', ['artisan', 'vendor:publish', '--tag=sanctum-migrations', '--no-interaction'], { cwd: api });
|
|
394
|
+
return addApiTokens(path.join(api, 'app', 'Models', 'User.php'));
|
|
311
395
|
});
|
|
312
396
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
write(path.join(web, '.env.local'), '# Where the Laravel API is, including its prefix.\nNEVELA_API_URL=http://127.0.0.1:8000/api\n');
|
|
316
|
-
if (pm === 'pnpm') {
|
|
317
|
-
// Its own workspace file: lets sharp build, and keeps pnpm from adopting a workspace further up.
|
|
318
|
-
write(path.join(web, 'pnpm-workspace.yaml'), 'allowBuilds:\n sharp: true\n');
|
|
319
|
-
}
|
|
320
|
-
writeRootFiles(root, { name, title, pm });
|
|
397
|
+
let admin = false;
|
|
398
|
+
await step('Setting up the database', () => {
|
|
321
399
|
// Writes routes/nevela.php and the dashboard's (empty) resource registry.
|
|
322
400
|
run('php', ['artisan', 'nevela:generate'], { cwd: api });
|
|
323
401
|
run('php', ['artisan', 'migrate', '--force', '--no-interaction'], { cwd: api });
|
|
402
|
+
if (options.user && tokens) {
|
|
403
|
+
admin = run('php', ['artisan', 'nevela:user', `--name=${ADMIN.name}`, `--email=${ADMIN.email}`, `--password=${ADMIN.password}`], { cwd: api, allowFailure: true }).ok;
|
|
404
|
+
}
|
|
324
405
|
});
|
|
325
406
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
});
|
|
407
|
+
let installed = false;
|
|
408
|
+
if (packages) {
|
|
409
|
+
// Usually finished long before Composer; the time shown is how long it took, not how long we waited here.
|
|
410
|
+
installed = (await step(`Installing dashboard packages (${pm})`, () => packages.done)).ok;
|
|
330
411
|
}
|
|
331
412
|
|
|
332
413
|
if (options.git && available('git') && !run('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root, allowFailure: true }).ok) {
|
|
333
414
|
run('git', ['init', '--quiet'], { cwd: root, allowFailure: true });
|
|
334
415
|
}
|
|
335
416
|
|
|
336
|
-
|
|
337
|
-
if (options.user && prompt) {
|
|
338
|
-
const answer = (await prompt.question(`\n Create a user to sign in with now? ${dim('(Y/n)')} `)).trim().toLowerCase();
|
|
339
|
-
prompt.close();
|
|
340
|
-
if (answer === '' || answer === 'y' || answer === 'yes') {
|
|
341
|
-
console.log('');
|
|
342
|
-
userCreated = run('php', ['artisan', 'nevela:user'], { cwd: api, interactive: true, allowFailure: true }).ok;
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
prompt?.close();
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
console.log(`\n ${green('✔')} ${bold(`Created ${name}`)}\n`);
|
|
417
|
+
console.log(`\n ${green('✔')} ${bold(`Created ${name}`)} ${dim(`in ${duration((Date.now() - began) / 1000)}`)}\n`);
|
|
349
418
|
if (!tokens) {
|
|
350
419
|
console.log(` ${red('!')} I couldn't add Sanctum's trait to apps/api/app/Models/User.php. Add it by hand:`);
|
|
351
420
|
console.log(` ${dim('use Laravel\\Sanctum\\HasApiTokens; and use HasApiTokens; inside the class')}\n`);
|
|
352
421
|
}
|
|
422
|
+
if (packages && !installed) {
|
|
423
|
+
console.log(` ${red('!')} The dashboard's packages didn't install. Run it yourself to see why: cd ${name}/apps/web && ${pm} install\n`);
|
|
424
|
+
}
|
|
353
425
|
console.log(' Next:\n');
|
|
354
426
|
console.log(` cd ${name}`);
|
|
355
427
|
if (!options.install) console.log(` cd apps/web && ${pm} install && cd ../..`);
|
|
356
|
-
if (!
|
|
428
|
+
if (!admin) console.log(` php nevela user ${dim('# someone to sign in as')}`);
|
|
357
429
|
console.log(` ${pm} run dev\n`);
|
|
358
|
-
console.log(` Then open ${indigo('http://localhost:3000/sign-in')}`);
|
|
359
|
-
|
|
430
|
+
console.log(` Then open ${indigo('http://localhost:3000/sign-in')}${admin ? ' and sign in with:' : ''}`);
|
|
431
|
+
if (admin) {
|
|
432
|
+
console.log(`\n Email ${bold(ADMIN.email)}`);
|
|
433
|
+
console.log(` Password ${bold(ADMIN.password)}\n`);
|
|
434
|
+
console.log(` ${dim('That account is in this app\'s local database only. Add your own: php nevela user')}`);
|
|
435
|
+
}
|
|
436
|
+
console.log(`\n Add your first resource: ${dim('php nevela resource Product --fields="name:string, price:money"')}`);
|
|
437
|
+
console.log(` Everything runs from this folder: ${dim('php nevela')}`);
|
|
360
438
|
console.log(` Docs: ${DOCS}/start/quickstart/\n`);
|
|
439
|
+
if (outdated) console.log(outdated);
|
|
361
440
|
}
|
|
362
441
|
|
|
363
442
|
main().catch((error) => fail(error?.message ?? String(error)));
|
package/package.json
CHANGED
package/packagist.mjs
CHANGED
|
@@ -1,25 +1,34 @@
|
|
|
1
|
-
// Deciding where a new app gets nevela/laravel from.
|
|
2
|
-
|
|
3
|
-
/** Whether `candidate` is a stable release Composer's `^major.minor` would accept. */
|
|
4
|
-
export function satisfiesCaret(candidate, [major, minor]) {
|
|
5
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(candidate);
|
|
6
|
-
if (!match) return false;
|
|
7
|
-
const [theirMajor, theirMinor] = match.slice(1).map(Number);
|
|
8
|
-
// Below 1.0 a caret holds the minor version too: ^0.1 means 0.1.x only.
|
|
9
|
-
return major === 0 ? theirMajor === 0 && theirMinor === minor : theirMajor === major && theirMinor >= minor;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
1
|
+
// Deciding where a new app gets nevela/laravel from.
|
|
2
|
+
|
|
3
|
+
/** Whether `candidate` is a stable release Composer's `^major.minor` would accept. */
|
|
4
|
+
export function satisfiesCaret(candidate, [major, minor]) {
|
|
5
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(candidate);
|
|
6
|
+
if (!match) return false;
|
|
7
|
+
const [theirMajor, theirMinor] = match.slice(1).map(Number);
|
|
8
|
+
// Below 1.0 a caret holds the minor version too: ^0.1 means 0.1.x only.
|
|
9
|
+
return major === 0 ? theirMajor === 0 && theirMinor === minor : theirMajor === major && theirMinor >= minor;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Whether stable version `a` is newer than `b`. Anything that isn't x.y.z is never newer. */
|
|
13
|
+
export function isNewer(a, b) {
|
|
14
|
+
const parse = (version) => /^(\d+)\.(\d+)\.(\d+)$/.exec(String(version))?.slice(1).map(Number);
|
|
15
|
+
const [left, right] = [parse(a), parse(b)];
|
|
16
|
+
if (!left || !right) return false;
|
|
17
|
+
for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] > right[i];
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether Packagist has a release of nevela/laravel that goes with this installer. Being
|
|
23
|
+
* listed isn't enough: with no tagged release yet, `composer require` would fail.
|
|
24
|
+
*/
|
|
25
|
+
export async function onPackagist(wanted) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch('https://repo.packagist.org/p2/nevela/laravel.json', { signal: AbortSignal.timeout(8000) });
|
|
28
|
+
if (!response.ok) return false;
|
|
29
|
+
const releases = (await response.json()).packages?.['nevela/laravel'] ?? [];
|
|
30
|
+
return releases.some((release) => satisfiesCaret(release.version, wanted));
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -43,6 +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:update` | Update the package, the generated code and the dashboard. |
|
|
46
47
|
|
|
47
48
|
Full documentation: https://nevela-docs.vercel.app
|
|
48
49
|
|
|
@@ -13,6 +13,13 @@ return [
|
|
|
13
13
|
*/
|
|
14
14
|
'web_path' => env('NEVELA_WEB_PATH', base_path('../web')),
|
|
15
15
|
|
|
16
|
+
/*
|
|
17
|
+
| The top of the project, where the `php nevela` launcher is written. Null works it
|
|
18
|
+
| out: two folders up when this app is at <project>/apps/<name>. Set a path if yours
|
|
19
|
+
| is somewhere else, or false to have no launcher.
|
|
20
|
+
*/
|
|
21
|
+
'root_path' => env('NEVELA_ROOT_PATH'),
|
|
22
|
+
|
|
16
23
|
/* URL prefix and middleware for generated resource routes (routes/nevela.php). */
|
|
17
24
|
'prefix' => 'api',
|
|
18
25
|
'middleware' => ['api', 'auth:sanctum'],
|