create-nevela 0.1.2 → 0.1.4
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 +12 -3
- package/index.mjs +194 -115
- package/package.json +41 -37
- package/packagist.mjs +17 -0
- package/project.mjs +126 -0
- package/selfupdate.mjs +94 -0
- package/shell.mjs +21 -0
- package/template/laravel/src/Console/DevCommand.php +104 -0
- package/template/laravel/src/Console/MakeResourceCommand.php +11 -3
- package/template/laravel/src/Console/SetupCommand.php +55 -0
- package/template/laravel/src/Console/StatusCommand.php +195 -0
- package/template/laravel/src/Console/UpdateCommand.php +180 -113
- package/template/laravel/src/Console/VersionCommand.php +20 -0
- package/template/laravel/src/Generator/ResourceGenerator.php +8 -8
- package/template/laravel/src/Nevela.php +161 -151
- package/template/laravel/src/NevelaServiceProvider.php +15 -1
- package/template/laravel/src/Support/DashboardState.php +118 -0
- package/template/laravel/src/Support/DashboardUpdate.php +57 -20
- package/template/laravel/src/Support/Releases.php +45 -0
- package/template/laravel/src/Support/TemplateSource.php +144 -0
- package/template/web/app/auth-actions.ts +12 -4
- package/template/web/package.json +1 -1
- package/template.mjs +127 -94
package/README.md
CHANGED
|
@@ -16,14 +16,14 @@ You need PHP 8.3 or newer, Composer, and Node.js 20 or newer.
|
|
|
16
16
|
my-app/
|
|
17
17
|
├─ apps/api/ a Laravel app with Sanctum and Nevela installed
|
|
18
18
|
├─ apps/web/ the Next.js dashboard, pointed at the API
|
|
19
|
-
└─
|
|
19
|
+
└─ nevela every command, from this folder: php nevela
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Then:
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
25
|
cd my-app
|
|
26
|
-
|
|
26
|
+
php nevela dev
|
|
27
27
|
```
|
|
28
28
|
|
|
29
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`.
|
|
@@ -42,7 +42,15 @@ That writes the Laravel code and the dashboard pages, and creates the table. `ph
|
|
|
42
42
|
php nevela update
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
That updates the Laravel package, regenerates, and updates the dashboard files you have not changed.
|
|
45
|
+
That updates the Laravel package, regenerates, and updates the dashboard files you have not changed. Files you changed are kept, and everything it replaces is backed up first.
|
|
46
|
+
|
|
47
|
+
If `php` is not a command in your shell (Git Bash with Laravel Herd), or the app is an old one, run it through this package instead. It works in any shell and on an app of any age:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
npx create-nevela update
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Installed globally (`npm install -g create-nevela`), it gives you a `nevela` command: `nevela update`, `nevela dev`, `nevela status`, `nevela resource …`. See https://nevela-docs.vercel.app/guides/updating/.
|
|
46
54
|
|
|
47
55
|
## Options
|
|
48
56
|
|
|
@@ -50,6 +58,7 @@ That updates the Laravel package, regenerates, and updates the dashboard files y
|
|
|
50
58
|
|---|---|
|
|
51
59
|
| `--pm <pnpm\|npm\|yarn\|bun>` | Package manager for the dashboard. Default: the one you ran this with. |
|
|
52
60
|
| `--no-install` | Don't install the dashboard's dependencies. |
|
|
61
|
+
| `--fast` | Leave out PHPUnit, Pint and Laravel's other development packages. About a third quicker. |
|
|
53
62
|
| `--no-user` | Don't create the starter account. |
|
|
54
63
|
| `--no-git` | Don't run `git init`. |
|
|
55
64
|
| `--bundled-package` | Use the copy of `nevela/laravel` inside this installer instead of the release on Packagist. |
|
package/index.mjs
CHANGED
|
@@ -6,19 +6,21 @@
|
|
|
6
6
|
//
|
|
7
7
|
// No dependencies on purpose: this runs before anything is installed.
|
|
8
8
|
|
|
9
|
-
import { spawn
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
10
|
import fs from 'node:fs';
|
|
11
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 {
|
|
16
|
-
import {
|
|
15
|
+
import { latestRelease, onPackagist } from './packagist.mjs';
|
|
16
|
+
import { COMMANDS, findProject, forward, update } from './project.mjs';
|
|
17
|
+
import { shellLine, spawnCommand, windows } from './shell.mjs';
|
|
18
|
+
import { newerVersion, runVersion } from './selfupdate.mjs';
|
|
19
|
+
import { copyLaravelPackage, copyWebTemplate, fromRepository, templateRecord } from './template.mjs';
|
|
17
20
|
|
|
18
21
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
19
22
|
const VERSION = JSON.parse(fs.readFileSync(path.join(here, 'package.json'), 'utf8')).version;
|
|
20
23
|
const DOCS = 'https://nevela-docs.vercel.app';
|
|
21
|
-
const windows = process.platform === 'win32';
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* The account every new app starts with, so there is something to sign in as straight
|
|
@@ -39,14 +41,23 @@ const HELP = `
|
|
|
39
41
|
|
|
40
42
|
Create a new Nevela app: a Laravel API and a Next.js dashboard.
|
|
41
43
|
|
|
42
|
-
${bold('
|
|
44
|
+
${bold('Create an app')}
|
|
43
45
|
pnpm create nevela <name> [options]
|
|
44
46
|
npm create nevela@latest <name> -- [options]
|
|
45
47
|
|
|
48
|
+
${bold('Inside an app')} (also as: nevela <command>, once installed with npm install -g create-nevela)
|
|
49
|
+
nevela update Update Nevela and the dashboard to the latest version
|
|
50
|
+
nevela dev Run the API and the dashboard
|
|
51
|
+
nevela status Check versions, migrations, users and the dashboard
|
|
52
|
+
nevela resource <Name> --fields="…" [--seed]
|
|
53
|
+
nevela seed | user | generate | version | migrate | artisan <command>
|
|
54
|
+
|
|
46
55
|
${bold('Options')}
|
|
47
56
|
--pm <pnpm|npm|yarn|bun> Package manager for the dashboard. Default: the one you ran this with.
|
|
48
57
|
--no-install Don't install the dashboard's dependencies.
|
|
49
58
|
--no-user Don't create the starter admin account.
|
|
59
|
+
--fast Leave out PHPUnit, Pint and Laravel's other development packages.
|
|
60
|
+
About a third quicker. Add them later: cd apps/api && composer install
|
|
50
61
|
--no-git Don't run git init.
|
|
51
62
|
--bundled-package Use the copy of nevela/laravel that ships with this installer
|
|
52
63
|
instead of the release on Packagist.
|
|
@@ -70,27 +81,12 @@ function run(command, args, { cwd, interactive = false, allowFailure = false } =
|
|
|
70
81
|
const result = spawnCommand(command, args, { cwd, stdio: interactive ? 'inherit' : 'pipe', encoding: 'utf8' });
|
|
71
82
|
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
72
83
|
if (result.error || result.status !== 0) {
|
|
73
|
-
if (allowFailure) return { ok: false, output };
|
|
84
|
+
if (allowFailure) return { ok: false, output, status: result.status };
|
|
74
85
|
fail(`\`${command} ${args.join(' ')}\` failed.`, output || String(result.error ?? ''));
|
|
75
86
|
}
|
|
76
|
-
return { ok: true, output };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* .bat and .cmd shims (composer, pnpm, Herd's php) only resolve through a shell on
|
|
81
|
-
* Windows. A shell takes one command string, so arguments are quoted here; none of them
|
|
82
|
-
* come from anywhere but this file and the validated app name.
|
|
83
|
-
*/
|
|
84
|
-
function spawnCommand(command, args, options) {
|
|
85
|
-
if (!windows) return spawnSync(command, args, options);
|
|
86
|
-
return spawnSync(shellLine(command, args), { ...options, shell: true });
|
|
87
|
+
return { ok: true, output, status: 0 };
|
|
87
88
|
}
|
|
88
89
|
|
|
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
90
|
/**
|
|
95
91
|
* Start a command and carry on. Its output goes to a file rather than a pipe: the steps
|
|
96
92
|
* that follow block this process for minutes, and a pipe nobody is reading fills up and
|
|
@@ -119,6 +115,33 @@ function start(command, args, { cwd }) {
|
|
|
119
115
|
return { done };
|
|
120
116
|
}
|
|
121
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Run a command without blocking, handing each line of its output to `onLine`. For the one
|
|
120
|
+
* long step, so there is something to show while it works. Fails like run() does.
|
|
121
|
+
*/
|
|
122
|
+
function runLive(command, args, { cwd, onLine }) {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const child = windows
|
|
125
|
+
? spawn(shellLine(command, args), { cwd, shell: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
126
|
+
: spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
127
|
+
let output = '';
|
|
128
|
+
let rest = '';
|
|
129
|
+
const read = (chunk) => {
|
|
130
|
+
output += chunk;
|
|
131
|
+
const lines = (rest + chunk).split(/\r?\n/);
|
|
132
|
+
rest = lines.pop();
|
|
133
|
+
for (const line of lines) onLine?.(line);
|
|
134
|
+
};
|
|
135
|
+
child.stdout.setEncoding('utf8').on('data', read);
|
|
136
|
+
child.stderr.setEncoding('utf8').on('data', read);
|
|
137
|
+
child.on('error', (error) => fail(`\`${command} ${args.join(' ')}\` failed.`, String(error)));
|
|
138
|
+
child.on('close', (code) => {
|
|
139
|
+
if (code !== 0) fail(`\`${command} ${args.join(' ')}\` failed.`, output);
|
|
140
|
+
resolve(output);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
122
145
|
function available(command, args = ['--version']) {
|
|
123
146
|
const result = spawnCommand(command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
124
147
|
return result.status === 0 ? `${result.stdout}${result.stderr}` : null;
|
|
@@ -130,17 +153,21 @@ async function step(label, work) {
|
|
|
130
153
|
const started = Date.now();
|
|
131
154
|
// The "in progress" line is rewritten in place when the step ends. That only works if
|
|
132
155
|
// it fits on one line; in a narrow terminal it would wrap and be left behind.
|
|
133
|
-
const inPlace = process.stdout.isTTY && label.length +
|
|
156
|
+
const inPlace = process.stdout.isTTY && label.length + 30 < (process.stdout.columns ?? 80);
|
|
134
157
|
if (inPlace) process.stdout.write(` ${dim('◇')} ${label}…`);
|
|
135
|
-
|
|
158
|
+
// A step may say how far along it is; shown beside the label, rewritten in place.
|
|
159
|
+
const progress = (text) => {
|
|
160
|
+
if (inPlace) process.stdout.write(`\r ${dim('◇')} ${label}… ${dim(text)} `);
|
|
161
|
+
};
|
|
162
|
+
const result = await work(progress);
|
|
136
163
|
const seconds = result?.seconds ?? (Date.now() - started) / 1000;
|
|
137
164
|
const took = seconds >= 1 ? dim(` (${duration(seconds)})`) : '';
|
|
138
|
-
process.stdout.write(`${inPlace ? '\r' : ''} ${green('✔')} ${label}${took}
|
|
165
|
+
process.stdout.write(`${inPlace ? '\r' : ''} ${green('✔')} ${label}${took}${' '.repeat(24)}\n`);
|
|
139
166
|
return result;
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
function parseArgs(argv) {
|
|
143
|
-
const options = { name: undefined, pm: undefined, install: true, user: true, git: true, yes: false, bundledPackage: false };
|
|
170
|
+
const options = { name: undefined, pm: undefined, install: true, user: true, git: true, yes: false, bundledPackage: false, devTools: true };
|
|
144
171
|
for (let i = 0; i < argv.length; i++) {
|
|
145
172
|
const arg = argv[i];
|
|
146
173
|
if (arg === '-h' || arg === '--help') {
|
|
@@ -153,6 +180,7 @@ function parseArgs(argv) {
|
|
|
153
180
|
else if (arg === '--no-install') options.install = false;
|
|
154
181
|
else if (arg === '--no-user') options.user = false;
|
|
155
182
|
else if (arg === '--no-git') options.git = false;
|
|
183
|
+
else if (arg === '--no-dev-tools' || arg === '--fast') options.devTools = false;
|
|
156
184
|
else if (arg === '--bundled-package') options.bundledPackage = true;
|
|
157
185
|
else if (arg === '--pm') options.pm = argv[++i];
|
|
158
186
|
else if (arg.startsWith('--pm=')) options.pm = arg.slice(5);
|
|
@@ -163,20 +191,27 @@ function parseArgs(argv) {
|
|
|
163
191
|
return options;
|
|
164
192
|
}
|
|
165
193
|
|
|
166
|
-
/**
|
|
167
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Whether typing `php` works in the shell this was started from. In Git Bash it doesn't
|
|
196
|
+
* when PHP is a php.bat (Laravel Herd): bash won't run a .bat for the bare name. Windows'
|
|
197
|
+
* own shells do, and so does this installer, which is why it got this far.
|
|
198
|
+
*/
|
|
199
|
+
function phpIsTypeable() {
|
|
200
|
+
if (!windows || !process.env.MSYSTEM) return true;
|
|
201
|
+
const found = spawnCommand('where', ['php'], { stdio: 'pipe', encoding: 'utf8' });
|
|
202
|
+
const first = `${found.stdout ?? ''}`.split(/\r?\n/)[0].trim().toLowerCase();
|
|
203
|
+
return first.endsWith('.exe');
|
|
204
|
+
}
|
|
168
205
|
|
|
169
|
-
/**
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const latest = (await response.json()).version;
|
|
174
|
-
return isNewer(latest, VERSION) ? latest : null;
|
|
175
|
-
} catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
206
|
+
/** `nevela <command>` as someone should type it here: through PHP, or through the package manager. */
|
|
207
|
+
function nevelaCommand(pm) {
|
|
208
|
+
if (phpIsTypeable()) return 'php nevela';
|
|
209
|
+
return { pnpm: 'pnpm nevela', yarn: 'yarn nevela', bun: 'bun run nevela' }[pm] ?? 'npm run nevela';
|
|
178
210
|
}
|
|
179
211
|
|
|
212
|
+
/** "my-shop" → "My Shop", for the dashboard's title. */
|
|
213
|
+
const titleCase = (slug) => slug.split(/[-_]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
|
|
214
|
+
|
|
180
215
|
function detectPackageManager(requested) {
|
|
181
216
|
const known = ['pnpm', 'npm', 'yarn', 'bun'];
|
|
182
217
|
if (requested) {
|
|
@@ -227,7 +262,7 @@ function writeRootFiles(root, { name, title, pm, admin }) {
|
|
|
227
262
|
private: true,
|
|
228
263
|
type: 'module',
|
|
229
264
|
scripts: {
|
|
230
|
-
dev: '
|
|
265
|
+
dev: 'php nevela dev',
|
|
231
266
|
nevela: 'php nevela',
|
|
232
267
|
'dev:api': 'cd apps/api && php artisan serve',
|
|
233
268
|
'dev:web': `cd apps/web && ${pm} run dev`,
|
|
@@ -236,50 +271,6 @@ function writeRootFiles(root, { name, title, pm, admin }) {
|
|
|
236
271
|
|
|
237
272
|
write(path.join(root, '.gitignore'), 'node_modules/\nvendor/\n.env\n.env.*\n!.env.example\n.next/\n*.tsbuildinfo\nnext-env.d.ts\n');
|
|
238
273
|
|
|
239
|
-
write(path.join(root, 'scripts', 'dev.mjs'), `// Run the Laravel API and the Next.js dashboard together: \`${pm} run dev\`.
|
|
240
|
-
import { spawn } from 'node:child_process';
|
|
241
|
-
|
|
242
|
-
const shell = process.platform === 'win32';
|
|
243
|
-
const apps = [
|
|
244
|
-
{ name: 'api', colour: 35, command: 'php', args: ['artisan', 'serve'], cwd: 'apps/api' },
|
|
245
|
-
{ name: 'web', colour: 36, command: '${pm}', args: ['run', 'dev'], cwd: 'apps/web' },
|
|
246
|
-
];
|
|
247
|
-
|
|
248
|
-
const running = apps.map(({ name, colour, command, args, cwd }) => {
|
|
249
|
-
// One string when going through a shell: Node warns about (and doesn't escape) separate args there.
|
|
250
|
-
const child = shell
|
|
251
|
-
? spawn([command, ...args].join(' '), { cwd, shell, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
252
|
-
: spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
253
|
-
const prefix = \`\\x1b[\${colour}m\${name}\\x1b[0m │ \`;
|
|
254
|
-
for (const stream of [child.stdout, child.stderr]) {
|
|
255
|
-
let rest = '';
|
|
256
|
-
stream.on('data', (chunk) => {
|
|
257
|
-
const lines = (rest + chunk).split('\\n');
|
|
258
|
-
rest = lines.pop();
|
|
259
|
-
for (const line of lines) console.log(prefix + line);
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
child.on('exit', (code) => {
|
|
263
|
-
console.log(\`\${prefix}stopped\${code ? \` (exit \${code})\` : ''}\`);
|
|
264
|
-
stop();
|
|
265
|
-
});
|
|
266
|
-
return child;
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
let stopping = false;
|
|
270
|
-
function stop() {
|
|
271
|
-
if (stopping) return;
|
|
272
|
-
stopping = true;
|
|
273
|
-
for (const child of running) {
|
|
274
|
-
// On Windows the child is a shell; killing it alone would leave php and node running.
|
|
275
|
-
if (shell && child.pid) spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
276
|
-
else child.kill();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
process.on('SIGINT', stop);
|
|
280
|
-
process.on('SIGTERM', stop);
|
|
281
|
-
`);
|
|
282
|
-
|
|
283
274
|
write(path.join(root, 'README.md'), `# ${title}
|
|
284
275
|
|
|
285
276
|
A [Nevela](${DOCS}) app: a Laravel API in \`apps/api\` and a Next.js dashboard in \`apps/web\`.
|
|
@@ -287,10 +278,16 @@ A [Nevela](${DOCS}) app: a Laravel API in \`apps/api\` and a Next.js dashboard i
|
|
|
287
278
|
## Run it
|
|
288
279
|
|
|
289
280
|
\`\`\`sh
|
|
290
|
-
|
|
281
|
+
php nevela dev
|
|
291
282
|
\`\`\`
|
|
292
283
|
|
|
293
|
-
That starts Laravel
|
|
284
|
+
That starts the Laravel API and the dashboard together and prints where each is: normally http://127.0.0.1:8000 and http://localhost:3000. If another program already has a port, it uses the next free one. Sign in at http://localhost:3000/sign-in.
|
|
285
|
+
|
|
286
|
+
To check the app at any time (versions, migrations, users, and whether the dashboard is reaching this API):
|
|
287
|
+
|
|
288
|
+
\`\`\`sh
|
|
289
|
+
php nevela status
|
|
290
|
+
\`\`\`
|
|
294
291
|
${admin ? `
|
|
295
292
|
The app starts with one account, in your local database only:
|
|
296
293
|
|
|
@@ -303,6 +300,7 @@ Everything else runs from this folder too, with \`php nevela\`:
|
|
|
303
300
|
|
|
304
301
|
\`\`\`sh
|
|
305
302
|
php nevela user # add someone who can sign in
|
|
303
|
+
php nevela status # versions, migrations, users, and what the dashboard is talking to
|
|
306
304
|
php nevela update # update Nevela and the dashboard
|
|
307
305
|
php nevela # everything it can do
|
|
308
306
|
\`\`\`
|
|
@@ -323,18 +321,51 @@ Docs: ${DOCS}
|
|
|
323
321
|
`);
|
|
324
322
|
}
|
|
325
323
|
|
|
324
|
+
/** `nevela <command>` inside an existing app. Never returns. */
|
|
325
|
+
async function inProject(name, args) {
|
|
326
|
+
const project = findProject(process.cwd());
|
|
327
|
+
if (!project) {
|
|
328
|
+
fail(`"${name}" runs inside a Nevela app, and this folder isn't in one.`, `To create an app: pnpm create nevela my-app`);
|
|
329
|
+
}
|
|
330
|
+
if (name === 'version') console.log(`create-nevela ${VERSION}`);
|
|
331
|
+
if (name !== 'update') process.exit(forward(project, name, args));
|
|
332
|
+
|
|
333
|
+
// Updating is the one command that must not run on stale code itself.
|
|
334
|
+
if (!process.env.NEVELA_NO_SELF_UPDATE && !fromRepository) {
|
|
335
|
+
const newer = await newerVersion(VERSION);
|
|
336
|
+
if (newer) {
|
|
337
|
+
const status = await runVersion(newer, ['update', ...args]);
|
|
338
|
+
if (status !== null) process.exit(status);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log(`\n ${indigo(bold('Nevela'))} ${dim('update')}\n`);
|
|
342
|
+
const releases = await latestRelease();
|
|
343
|
+
const status = await update(project, args, { latest: releases, say: (line) => console.log(` ${green('✔')} ${line}`) });
|
|
344
|
+
process.exit(status);
|
|
345
|
+
}
|
|
346
|
+
|
|
326
347
|
async function main() {
|
|
327
|
-
const
|
|
348
|
+
const argv = process.argv.slice(2);
|
|
349
|
+
if (argv[0] === 'new') argv.shift();
|
|
350
|
+
else if (COMMANDS.includes(argv[0])) await inProject(argv[0], argv.slice(1));
|
|
351
|
+
const options = parseArgs(argv);
|
|
328
352
|
|
|
329
353
|
console.log(`\n ${indigo(bold('Nevela'))} ${dim(`v${VERSION}`)}\n`);
|
|
330
354
|
|
|
331
|
-
// A package manager may
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
355
|
+
// A package manager may hand over an older installer than the newest: pnpm skips versions
|
|
356
|
+
// published in the last few hours, and both pnpm and npm cache. Fetch the newest and let
|
|
357
|
+
// it do the work. Skipped when running from the repository, which is always its own version.
|
|
358
|
+
let outdated = null;
|
|
359
|
+
if (!process.env.NEVELA_NO_SELF_UPDATE && !fromRepository) {
|
|
360
|
+
const newer = await newerVersion(VERSION);
|
|
361
|
+
if (newer) {
|
|
362
|
+
console.log(` ${dim(`Your package manager served ${VERSION}. Fetching ${newer}, the latest…`)}`);
|
|
363
|
+
const status = await runVersion(newer, argv);
|
|
364
|
+
if (status !== null) process.exit(status);
|
|
365
|
+
outdated = ` ${red('!')} Couldn't fetch create-nevela ${bold(newer)}, so this is ${VERSION}. To get it: ${bold(`npm create nevela@latest ${options.name ?? 'my-app'}`)}\n`;
|
|
366
|
+
console.log(outdated);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
338
369
|
|
|
339
370
|
let name = options.name;
|
|
340
371
|
if (!name) {
|
|
@@ -367,41 +398,82 @@ async function main() {
|
|
|
367
398
|
// Its own workspace file: lets sharp build, and keeps pnpm from adopting a workspace further up.
|
|
368
399
|
write(path.join(web, 'pnpm-workspace.yaml'), 'allowBuilds:\n sharp: true\n');
|
|
369
400
|
}
|
|
370
|
-
// Which dashboard this app started from
|
|
371
|
-
// to tell the files you changed from the ones
|
|
372
|
-
|
|
401
|
+
// Which dashboard this app started from, with a fingerprint of every file in it.
|
|
402
|
+
// `nevela update` compares against these to tell the files you changed from the ones
|
|
403
|
+
// you didn't, so an update never overwrites your work.
|
|
404
|
+
write(path.join(web, '.nevela.json'), `${JSON.stringify(templateRecord(VERSION), null, 4)}\n`);
|
|
373
405
|
writeRootFiles(root, { name, title, pm, admin: options.user ? ADMIN : null });
|
|
374
406
|
const packages = options.install ? start(pm, ['install'], { cwd: web }) : null;
|
|
375
407
|
|
|
376
|
-
await step('Creating the Laravel app', () => {
|
|
377
|
-
|
|
378
|
-
|
|
408
|
+
const tokens = await step('Creating the Laravel app', async (progress) => {
|
|
409
|
+
// The skeleton only. Its packages are installed below, once, together with Nevela's.
|
|
410
|
+
run('composer', ['create-project', 'laravel/laravel', 'apps/api', '--no-install', '--no-scripts', '--no-interaction', '--no-progress', '--prefer-dist'], { cwd: root });
|
|
379
411
|
|
|
380
|
-
const tokens = await step('Installing Nevela and token sign-in', async () => {
|
|
381
412
|
const wanted = VERSION.split('.').slice(0, 2).map(Number);
|
|
382
|
-
let nevela =
|
|
413
|
+
let nevela = `^${wanted.join('.')}`;
|
|
414
|
+
const manifestFile = path.join(api, 'composer.json');
|
|
415
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
416
|
+
// Laravel asks Composer for an "optimized" autoloader, which means reading every file
|
|
417
|
+
// of every package to index it. On a fresh install each of those ~9,000 files is being
|
|
418
|
+
// read for the first time, and antivirus software scans each one: that single step
|
|
419
|
+
// took over five minutes on Windows. An app in development doesn't need the index.
|
|
420
|
+
// Production gets it with `composer install --no-dev --optimize-autoloader`.
|
|
421
|
+
manifest.config = { ...manifest.config, 'optimize-autoloader': false };
|
|
383
422
|
if (options.bundledPackage || !(await onPackagist(wanted))) {
|
|
384
423
|
// No matching release on Packagist (or --bundled-package): the copy that travels with
|
|
385
424
|
// this installer is placed in the app and installed by path.
|
|
386
425
|
copyLaravelPackage(path.join(root, 'packages', 'nevela-laravel'));
|
|
387
|
-
|
|
388
|
-
nevela = '
|
|
426
|
+
manifest.repositories = [...(manifest.repositories ?? []), { type: 'path', url: '../../packages/nevela-laravel' }];
|
|
427
|
+
nevela = '@dev';
|
|
389
428
|
}
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
429
|
+
// Sanctum and Nevela go into composer.json here, so the one install below does
|
|
430
|
+
// everything. (`php artisan install:api` would add Sanctum in a Composer run of its own,
|
|
431
|
+
// plus a routes/api.php this app doesn't use: Nevela registers its routes itself.)
|
|
432
|
+
manifest.require = { ...manifest.require, 'laravel/sanctum': '^4.0', 'nevela/laravel': nevela };
|
|
433
|
+
fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 4)}\n`);
|
|
434
|
+
|
|
435
|
+
let total = 0;
|
|
436
|
+
let done = 0;
|
|
437
|
+
await runLive('composer', ['install', '--no-interaction', '--no-progress', '--no-scripts', ...(options.devTools ? [] : ['--no-dev'])], {
|
|
438
|
+
cwd: api,
|
|
439
|
+
onLine: (line) => {
|
|
440
|
+
const operations = /Package operations: (\d+) installs?/.exec(line);
|
|
441
|
+
if (operations) total = Number(operations[1]);
|
|
442
|
+
if (/- Installing .*Extracting archive|- Installing .*Symlinking|- Installing .*Junctioning/.test(line)) done++;
|
|
443
|
+
if (total) progress(`${Math.min(done, total)}/${total} packages`);
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// What Composer's create-project scripts would have done, without starting Composer
|
|
448
|
+
// again for each: the .env file, the SQLite database and the app key. The first
|
|
449
|
+
// artisan command is slow for the same reason the index was: files read for the first time.
|
|
450
|
+
const env = path.join(api, '.env');
|
|
451
|
+
if (!fs.existsSync(env) && fs.existsSync(`${env}.example`)) fs.copyFileSync(`${env}.example`, env);
|
|
452
|
+
const sqlite = path.join(api, 'database', 'database.sqlite');
|
|
453
|
+
if (/^DB_CONNECTION=sqlite\s*$/m.test(fs.readFileSync(env, 'utf8')) && !fs.existsSync(sqlite)) fs.writeFileSync(sqlite, '');
|
|
394
454
|
return addApiTokens(path.join(api, 'app', 'Models', 'User.php'));
|
|
395
455
|
});
|
|
396
456
|
|
|
397
457
|
let admin = false;
|
|
398
458
|
await step('Setting up the database', () => {
|
|
399
|
-
//
|
|
459
|
+
// One artisan process for the app key, Sanctum's migration, the generated files, the
|
|
460
|
+
// migrations and the starter account: each separate call would start Laravel again.
|
|
461
|
+
const account = options.user && tokens ? [`--name=${ADMIN.name}`, `--email=${ADMIN.email}`, `--password=${ADMIN.password}`] : [];
|
|
462
|
+
const setup = run('php', ['artisan', 'nevela:setup', ...account], { cwd: api, allowFailure: true });
|
|
463
|
+
if (setup.ok) {
|
|
464
|
+
admin = account.length > 0;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
// 2: the app is set up and only the starter account could not be created.
|
|
468
|
+
if (setup.status === 2) return;
|
|
469
|
+
if (!/"nevela:setup" is not defined/.test(setup.output)) fail('Setting up the database failed.', setup.output);
|
|
470
|
+
|
|
471
|
+
// A nevela/laravel from before `nevela:setup` existed: the same steps, one at a time.
|
|
472
|
+
run('php', ['artisan', 'key:generate', '--force', '--no-interaction'], { cwd: api });
|
|
473
|
+
run('php', ['artisan', 'vendor:publish', '--tag=sanctum-migrations', '--no-interaction'], { cwd: api });
|
|
400
474
|
run('php', ['artisan', 'nevela:generate'], { cwd: api });
|
|
401
475
|
run('php', ['artisan', 'migrate', '--force', '--no-interaction'], { cwd: api });
|
|
402
|
-
if (
|
|
403
|
-
admin = run('php', ['artisan', 'nevela:user', `--name=${ADMIN.name}`, `--email=${ADMIN.email}`, `--password=${ADMIN.password}`], { cwd: api, allowFailure: true }).ok;
|
|
404
|
-
}
|
|
476
|
+
if (account.length) admin = run('php', ['artisan', 'nevela:user', ...account], { cwd: api, allowFailure: true }).ok;
|
|
405
477
|
});
|
|
406
478
|
|
|
407
479
|
let installed = false;
|
|
@@ -419,22 +491,29 @@ async function main() {
|
|
|
419
491
|
console.log(` ${red('!')} I couldn't add Sanctum's trait to apps/api/app/Models/User.php. Add it by hand:`);
|
|
420
492
|
console.log(` ${dim('use Laravel\\Sanctum\\HasApiTokens; and use HasApiTokens; inside the class')}\n`);
|
|
421
493
|
}
|
|
494
|
+
if (!options.devTools) {
|
|
495
|
+
console.log(` ${dim('PHPUnit, Pint and the other development packages were left out. Add them: cd apps/api && composer install')}\n`);
|
|
496
|
+
}
|
|
422
497
|
if (packages && !installed) {
|
|
423
498
|
console.log(` ${red('!')} The dashboard's packages didn't install. Run it yourself to see why: cd ${name}/apps/web && ${pm} install\n`);
|
|
424
499
|
}
|
|
425
500
|
console.log(' Next:\n');
|
|
426
501
|
console.log(` cd ${name}`);
|
|
427
502
|
if (!options.install) console.log(` cd apps/web && ${pm} install && cd ../..`);
|
|
428
|
-
|
|
429
|
-
console.log(` ${
|
|
503
|
+
const nevela = nevelaCommand(pm);
|
|
504
|
+
if (!admin) console.log(` ${nevela} user ${dim('# someone to sign in as')}`);
|
|
505
|
+
console.log(` ${nevela} dev\n`);
|
|
430
506
|
console.log(` Then open ${indigo('http://localhost:3000/sign-in')}${admin ? ' and sign in with:' : ''}`);
|
|
431
507
|
if (admin) {
|
|
432
508
|
console.log(`\n Email ${bold(ADMIN.email)}`);
|
|
433
509
|
console.log(` Password ${bold(ADMIN.password)}\n`);
|
|
434
|
-
console.log(` ${dim(
|
|
510
|
+
console.log(` ${dim(`That account is in this app's local database only. Add your own: ${nevela} user`)}`);
|
|
511
|
+
}
|
|
512
|
+
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`)} Update it later: ${dim(`${nevela} update`)} Everything: ${dim(nevela)}`);
|
|
514
|
+
if (nevela !== 'php nevela') {
|
|
515
|
+
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.`)}`);
|
|
435
516
|
}
|
|
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')}`);
|
|
438
517
|
console.log(` Docs: ${DOCS}/start/quickstart/\n`);
|
|
439
518
|
if (outdated) console.log(outdated);
|
|
440
519
|
}
|
package/package.json
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-nevela",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Create a new Nevela app: a Laravel API and a Next.js dashboard from one resource description.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"create-nevela": "index.mjs"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "create-nevela",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Create a new Nevela app: a Laravel API and a Next.js dashboard from one resource description.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-nevela": "index.mjs",
|
|
8
|
+
"nevela": "index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"template.mjs",
|
|
13
|
+
"packagist.mjs",
|
|
14
|
+
"selfupdate.mjs",
|
|
15
|
+
"shell.mjs",
|
|
16
|
+
"project.mjs",
|
|
17
|
+
"template",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"prepack": "node build-template.mjs"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"nevela",
|
|
28
|
+
"laravel",
|
|
29
|
+
"nextjs",
|
|
30
|
+
"scaffold",
|
|
31
|
+
"create"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/MarkColeMukisa/nevela.git",
|
|
36
|
+
"directory": "packages/create-nevela"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://nevela-docs.vercel.app",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"author": "Mark Cole MUKISA"
|
|
41
|
+
}
|
package/packagist.mjs
CHANGED
|
@@ -32,3 +32,20 @@ export async function onPackagist(wanted) {
|
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/** The newest stable nevela/laravel on Packagist, as "x.y.z", or null when it can't be reached. */
|
|
37
|
+
export async function latestRelease() {
|
|
38
|
+
try {
|
|
39
|
+
const response = await fetch('https://repo.packagist.org/p2/nevela/laravel.json', { signal: AbortSignal.timeout(8000) });
|
|
40
|
+
if (!response.ok) return null;
|
|
41
|
+
const releases = (await response.json()).packages?.['nevela/laravel'] ?? [];
|
|
42
|
+
let latest = null;
|
|
43
|
+
for (const release of releases) {
|
|
44
|
+
const version = /^v?(\d+\.\d+\.\d+)$/.exec(release.version)?.[1];
|
|
45
|
+
if (version && (latest === null || isNewer(version, latest))) latest = version;
|
|
46
|
+
}
|
|
47
|
+
return latest;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|