create-nevela 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +52 -2
- package/index.mjs +363 -0
- package/package.json +37 -6
- package/packagist.mjs +25 -0
- package/template/laravel/LICENSE +21 -0
- package/template/laravel/README.md +55 -0
- package/template/laravel/composer.json +59 -0
- package/template/laravel/config/nevela.php +29 -0
- package/template/laravel/src/Console/GenerateCommand.php +77 -0
- package/template/laravel/src/Console/MakeResourceCommand.php +66 -0
- package/template/laravel/src/Console/SeedCommand.php +124 -0
- package/template/laravel/src/Console/UserCommand.php +59 -0
- package/template/laravel/src/Generator/GeneratedFile.php +31 -0
- package/template/laravel/src/Generator/ResourceGenerator.php +639 -0
- package/template/laravel/src/Generator/Writer.php +143 -0
- package/template/laravel/src/Http/FlareErrors.php +92 -0
- package/template/laravel/src/Http/ResourceRequest.php +67 -0
- package/template/laravel/src/Http/TokenController.php +65 -0
- package/template/laravel/src/Nevela.php +151 -0
- package/template/laravel/src/NevelaServiceProvider.php +65 -0
- package/template/laravel/src/Support/Descriptor.php +174 -0
- package/template/laravel/src/Support/Fake.php +171 -0
- package/template/laravel/src/Support/Field.php +146 -0
- package/template/laravel/src/Support/Issue.php +9 -0
- package/template/laravel/src/Support/ListQuery.php +125 -0
- package/template/laravel/src/Support/Naming.php +79 -0
- package/template/web/.env.example +2 -0
- package/template/web/LICENSE-flare +21 -0
- package/template/web/README.md +40 -0
- package/template/web/_gitignore +10 -0
- package/template/web/app/auth-actions.ts +47 -0
- package/template/web/app/dashboard/actions.ts +127 -0
- package/template/web/app/dashboard/error.tsx +37 -0
- package/template/web/app/dashboard/import-actions.ts +89 -0
- package/template/web/app/dashboard/layout.tsx +44 -0
- package/template/web/app/dashboard/loading.tsx +18 -0
- package/template/web/app/dashboard/not-found.tsx +30 -0
- package/template/web/app/dashboard/page.tsx +101 -0
- package/template/web/app/error.tsx +55 -0
- package/template/web/app/globals.css +255 -0
- package/template/web/app/layout.tsx +37 -0
- package/template/web/app/loading.tsx +22 -0
- package/template/web/app/not-found.tsx +48 -0
- package/template/web/app/page.tsx +142 -0
- package/template/web/app/sign-in/page.tsx +21 -0
- package/template/web/components/auth/auth-shell.tsx +284 -0
- package/template/web/components/auth/sign-in-form.tsx +50 -0
- package/template/web/components/auth/ui.tsx +96 -0
- package/template/web/components/dashboard/column-menu.tsx +66 -0
- package/template/web/components/dashboard/dashboard-header.tsx +115 -0
- package/template/web/components/dashboard/dashboard-sidebar.tsx +229 -0
- package/template/web/components/dashboard/dropzone.tsx +92 -0
- package/template/web/components/dashboard/editable.ts +12 -0
- package/template/web/components/dashboard/export-button.tsx +41 -0
- package/template/web/components/dashboard/fields/country-field.tsx +152 -0
- package/template/web/components/dashboard/fields/date-field.tsx +112 -0
- package/template/web/components/dashboard/fields/field-widget.tsx +318 -0
- package/template/web/components/dashboard/fields/file-cell.tsx +68 -0
- package/template/web/components/dashboard/fields/file-field.tsx +310 -0
- package/template/web/components/dashboard/fields/multi-select-field.tsx +46 -0
- package/template/web/components/dashboard/fields/phone-field.tsx +86 -0
- package/template/web/components/dashboard/fields/relation-field.tsx +154 -0
- package/template/web/components/dashboard/import-dialog.tsx +540 -0
- package/template/web/components/dashboard/inline-cell.tsx +165 -0
- package/template/web/components/dashboard/local-time.tsx +77 -0
- package/template/web/components/dashboard/page-header.tsx +57 -0
- package/template/web/components/dashboard/query.ts +27 -0
- package/template/web/components/dashboard/record-actions.tsx +113 -0
- package/template/web/components/dashboard/record-detail.tsx +182 -0
- package/template/web/components/dashboard/resource-form-page.tsx +64 -0
- package/template/web/components/dashboard/resource-form-sheet.tsx +86 -0
- package/template/web/components/dashboard/resource-form.tsx +261 -0
- package/template/web/components/dashboard/resource-icon.tsx +89 -0
- package/template/web/components/dashboard/resource-stats.tsx +49 -0
- package/template/web/components/dashboard/resource-table-toolbar.tsx +95 -0
- package/template/web/components/dashboard/resource-table.tsx +411 -0
- package/template/web/components/dashboard/row-actions.tsx +156 -0
- package/template/web/components/dashboard/skeletons.tsx +146 -0
- package/template/web/components/dashboard/stat-card.tsx +69 -0
- package/template/web/components/dashboard/table-selection.tsx +185 -0
- package/template/web/components/dashboard/user-menu.tsx +63 -0
- package/template/web/components/marketing/product-preview.tsx +70 -0
- package/template/web/components/marketing/site-header.tsx +39 -0
- package/template/web/components/sign-out-button.tsx +18 -0
- package/template/web/components/ui/alert-dialog.tsx +196 -0
- package/template/web/components/ui/alert.tsx +65 -0
- package/template/web/components/ui/avatar.tsx +108 -0
- package/template/web/components/ui/badge.tsx +53 -0
- package/template/web/components/ui/breadcrumb.tsx +110 -0
- package/template/web/components/ui/button.tsx +65 -0
- package/template/web/components/ui/calendar.tsx +220 -0
- package/template/web/components/ui/card.tsx +92 -0
- package/template/web/components/ui/checkbox.tsx +25 -0
- package/template/web/components/ui/command.tsx +184 -0
- package/template/web/components/ui/dialog.tsx +158 -0
- package/template/web/components/ui/dropdown-menu.tsx +256 -0
- package/template/web/components/ui/empty.tsx +103 -0
- package/template/web/components/ui/field.tsx +248 -0
- package/template/web/components/ui/input-group.tsx +170 -0
- package/template/web/components/ui/input.tsx +20 -0
- package/template/web/components/ui/label.tsx +23 -0
- package/template/web/components/ui/pagination.tsx +127 -0
- package/template/web/components/ui/popover.tsx +88 -0
- package/template/web/components/ui/progress.tsx +30 -0
- package/template/web/components/ui/radio-group.tsx +29 -0
- package/template/web/components/ui/select.tsx +189 -0
- package/template/web/components/ui/separator.tsx +27 -0
- package/template/web/components/ui/sheet.tsx +142 -0
- package/template/web/components/ui/sidebar.tsx +726 -0
- package/template/web/components/ui/skeleton.tsx +13 -0
- package/template/web/components/ui/sonner.tsx +58 -0
- package/template/web/components/ui/spinner.tsx +15 -0
- package/template/web/components/ui/switch.tsx +34 -0
- package/template/web/components/ui/table.tsx +115 -0
- package/template/web/components/ui/textarea.tsx +17 -0
- package/template/web/components/ui/tooltip.tsx +56 -0
- package/template/web/components.json +21 -0
- package/template/web/hooks/use-mobile.ts +19 -0
- package/template/web/lib/cache.ts +83 -0
- package/template/web/lib/csv.ts +24 -0
- package/template/web/lib/dashboard-nav.ts +17 -0
- package/template/web/lib/dashboard.ts +130 -0
- package/template/web/lib/laravel.ts +57 -0
- package/template/web/lib/number.ts +33 -0
- package/template/web/lib/resource/index.ts +4 -0
- package/template/web/lib/resource/query.ts +149 -0
- package/template/web/lib/resource/store.ts +149 -0
- package/template/web/lib/session.ts +42 -0
- package/template/web/lib/site.ts +56 -0
- package/template/web/lib/suggest.ts +35 -0
- package/template/web/lib/theme.ts +83 -0
- package/template/web/lib/utils.ts +6 -0
- package/template/web/lib/views.ts +14 -0
- package/template/web/next.config.ts +8 -0
- package/template/web/package.json +42 -0
- package/template/web/policies/index.ts +6 -0
- package/template/web/postcss.config.mjs +7 -0
- package/template/web/proxy.ts +20 -0
- package/template/web/tsconfig.json +46 -0
- package/template.mjs +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Cole MUKISA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,53 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-nevela
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create a new [Nevela](https://nevela-docs.vercel.app) app: a Laravel API and a Next.js dashboard, generated from one resource description.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm create nevela my-app
|
|
7
|
+
# or
|
|
8
|
+
npm create nevela@latest my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
You need PHP 8.3 or newer, Composer, and Node.js 20 or newer.
|
|
12
|
+
|
|
13
|
+
## What you get
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
my-app/
|
|
17
|
+
├─ apps/api/ a Laravel app with Sanctum and Nevela installed
|
|
18
|
+
├─ apps/web/ the Next.js dashboard, pointed at the API
|
|
19
|
+
└─ scripts/ `dev` runs both together
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
cd my-app
|
|
26
|
+
pnpm run dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Open http://localhost:3000/sign-in. Add your first resource from `apps/api`:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
php artisan nevela:resource Product --fields="name:string, sku:string!, price:money"
|
|
33
|
+
php artisan migrate
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
| Option | Meaning |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `--pm <pnpm\|npm\|yarn\|bun>` | Package manager for the dashboard. Default: the one you ran this with. |
|
|
41
|
+
| `--no-install` | Don't install the dashboard's dependencies. |
|
|
42
|
+
| `--no-user` | Don't ask to create the first user. |
|
|
43
|
+
| `--no-git` | Don't run `git init`. |
|
|
44
|
+
| `--bundled-package` | Use the copy of `nevela/laravel` inside this installer instead of the release on Packagist. |
|
|
45
|
+
| `-y`, `--yes` | Ask nothing; take the defaults. |
|
|
46
|
+
|
|
47
|
+
With npm, put options after `--`: `npm create nevela@latest my-app -- --no-install`.
|
|
48
|
+
|
|
49
|
+
## How it installs the Laravel package
|
|
50
|
+
|
|
51
|
+
`nevela/laravel` is installed from [Packagist](https://packagist.org/packages/nevela/laravel), at the release that matches this installer's version.
|
|
52
|
+
|
|
53
|
+
If Packagist has no matching release, or you pass `--bundled-package`, the copy that travels inside this installer is placed in `my-app/packages/nevela-laravel` and Composer installs it from there. That is how Nevela's own CI tests a change to the package before it is released.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Create a new Nevela app: a Laravel API and a Next.js dashboard, side by side.
|
|
3
|
+
//
|
|
4
|
+
// pnpm create nevela my-app
|
|
5
|
+
// npm create nevela@latest my-app
|
|
6
|
+
//
|
|
7
|
+
// No dependencies on purpose: this runs before anything is installed.
|
|
8
|
+
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import readline from 'node:readline/promises';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { onPackagist } from './packagist.mjs';
|
|
15
|
+
import { copyLaravelPackage, copyWebTemplate } from './template.mjs';
|
|
16
|
+
|
|
17
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(here, 'package.json'), 'utf8')).version;
|
|
19
|
+
const DOCS = 'https://nevela-docs.vercel.app';
|
|
20
|
+
const windows = process.platform === 'win32';
|
|
21
|
+
|
|
22
|
+
const colour = (code) => (text) => (process.stdout.isTTY ? `\x1b[${code}m${text}\x1b[0m` : text);
|
|
23
|
+
const bold = colour(1);
|
|
24
|
+
const dim = colour(2);
|
|
25
|
+
const green = colour(32);
|
|
26
|
+
const red = colour(31);
|
|
27
|
+
const indigo = colour('38;5;99');
|
|
28
|
+
|
|
29
|
+
const HELP = `
|
|
30
|
+
${bold('create-nevela')} ${dim(`v${VERSION}`)}
|
|
31
|
+
|
|
32
|
+
Create a new Nevela app: a Laravel API and a Next.js dashboard.
|
|
33
|
+
|
|
34
|
+
${bold('Usage')}
|
|
35
|
+
pnpm create nevela <name> [options]
|
|
36
|
+
npm create nevela@latest <name> -- [options]
|
|
37
|
+
|
|
38
|
+
${bold('Options')}
|
|
39
|
+
--pm <pnpm|npm|yarn|bun> Package manager for the dashboard. Default: the one you ran this with.
|
|
40
|
+
--no-install Don't install the dashboard's dependencies.
|
|
41
|
+
--no-user Don't ask to create the first user.
|
|
42
|
+
--no-git Don't run git init.
|
|
43
|
+
--bundled-package Use the copy of nevela/laravel that ships with this installer
|
|
44
|
+
instead of the release on Packagist.
|
|
45
|
+
-y, --yes Ask nothing; take the defaults.
|
|
46
|
+
-h, --help Show this.
|
|
47
|
+
-v, --version Show the version.
|
|
48
|
+
|
|
49
|
+
${bold('Needs')} PHP 8.3+, Composer, Node.js 20+
|
|
50
|
+
${bold('Docs')} ${DOCS}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
function fail(message, detail) {
|
|
54
|
+
console.error(`\n ${red('✖')} ${message}`);
|
|
55
|
+
if (detail) console.error(`\n${detail.trim().split('\n').map((line) => ` ${line}`).join('\n')}`);
|
|
56
|
+
console.error('');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Run a command. Quiet unless it fails, when its output is the error message. */
|
|
61
|
+
function run(command, args, { cwd, interactive = false, allowFailure = false } = {}) {
|
|
62
|
+
const result = spawnCommand(command, args, { cwd, stdio: interactive ? 'inherit' : 'pipe', encoding: 'utf8' });
|
|
63
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
|
|
64
|
+
if (result.error || result.status !== 0) {
|
|
65
|
+
if (allowFailure) return { ok: false, output };
|
|
66
|
+
fail(`\`${command} ${args.join(' ')}\` failed.`, output || String(result.error ?? ''));
|
|
67
|
+
}
|
|
68
|
+
return { ok: true, output };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* .bat and .cmd shims (composer, pnpm, Herd's php) only resolve through a shell on
|
|
73
|
+
* Windows. A shell takes one command string, so arguments are quoted here; none of them
|
|
74
|
+
* come from anywhere but this file and the validated app name.
|
|
75
|
+
*/
|
|
76
|
+
function spawnCommand(command, args, options) {
|
|
77
|
+
if (!windows) return spawnSync(command, args, options);
|
|
78
|
+
// `^` is not in the safe list on purpose: unquoted, cmd.exe treats it as an escape
|
|
79
|
+
// character and drops it, which turned the constraint "^0.1" into an exact "0.1".
|
|
80
|
+
const quote = (arg) => (/^[\w./:=@,-]+$/.test(arg) ? arg : `"${arg.replace(/"/g, '\\"')}"`);
|
|
81
|
+
return spawnSync([command, ...args.map(quote)].join(' '), { ...options, shell: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function available(command, args = ['--version']) {
|
|
85
|
+
const result = spawnCommand(command, args, { stdio: 'pipe', encoding: 'utf8' });
|
|
86
|
+
return result.status === 0 ? `${result.stdout}${result.stderr}` : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function step(label, work) {
|
|
90
|
+
const started = Date.now();
|
|
91
|
+
process.stdout.write(` ${dim('◇')} ${label}…`);
|
|
92
|
+
const result = await work();
|
|
93
|
+
const seconds = (Date.now() - started) / 1000;
|
|
94
|
+
const took = seconds >= 1 ? dim(` (${seconds >= 60 ? `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s` : `${seconds.toFixed(1)}s`})`) : '';
|
|
95
|
+
process.stdout.write(`\r ${green('✔')} ${label}${took} \n`);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseArgs(argv) {
|
|
100
|
+
const options = { name: undefined, pm: undefined, install: true, user: true, git: true, yes: false, bundledPackage: false };
|
|
101
|
+
for (let i = 0; i < argv.length; i++) {
|
|
102
|
+
const arg = argv[i];
|
|
103
|
+
if (arg === '-h' || arg === '--help') {
|
|
104
|
+
console.log(HELP);
|
|
105
|
+
process.exit(0);
|
|
106
|
+
} else if (arg === '-v' || arg === '--version') {
|
|
107
|
+
console.log(VERSION);
|
|
108
|
+
process.exit(0);
|
|
109
|
+
} else if (arg === '-y' || arg === '--yes') options.yes = true;
|
|
110
|
+
else if (arg === '--no-install') options.install = false;
|
|
111
|
+
else if (arg === '--no-user') options.user = false;
|
|
112
|
+
else if (arg === '--no-git') options.git = false;
|
|
113
|
+
else if (arg === '--bundled-package') options.bundledPackage = true;
|
|
114
|
+
else if (arg === '--pm') options.pm = argv[++i];
|
|
115
|
+
else if (arg.startsWith('--pm=')) options.pm = arg.slice(5);
|
|
116
|
+
else if (arg.startsWith('-')) fail(`Unknown option ${arg}. Run with --help to see the options.`);
|
|
117
|
+
else if (options.name === undefined) options.name = arg;
|
|
118
|
+
else fail(`Unexpected argument "${arg}". The app name is "${options.name}".`);
|
|
119
|
+
}
|
|
120
|
+
return options;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** "my-shop" → "My Shop", for the dashboard's title. */
|
|
124
|
+
const titleCase = (slug) => slug.split(/[-_]+/).filter(Boolean).map((word) => word[0].toUpperCase() + word.slice(1)).join(' ');
|
|
125
|
+
|
|
126
|
+
function detectPackageManager(requested) {
|
|
127
|
+
const known = ['pnpm', 'npm', 'yarn', 'bun'];
|
|
128
|
+
if (requested) {
|
|
129
|
+
if (!known.includes(requested)) fail(`--pm must be one of: ${known.join(', ')}.`);
|
|
130
|
+
if (!available(requested)) fail(`${requested} isn't installed, or isn't on your PATH.`);
|
|
131
|
+
return requested;
|
|
132
|
+
}
|
|
133
|
+
// "pnpm/10.1.0 npm/? node/v22…" when run through `pnpm create`.
|
|
134
|
+
const agent = (process.env.npm_config_user_agent ?? '').split('/')[0];
|
|
135
|
+
if (known.includes(agent)) return agent;
|
|
136
|
+
return available('pnpm') ? 'pnpm' : 'npm';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function checkRequirements() {
|
|
140
|
+
const node = process.versions.node.split('.').map(Number);
|
|
141
|
+
if (node[0] < 20) fail(`Nevela needs Node.js 20 or newer. You have ${process.versions.node}.`);
|
|
142
|
+
|
|
143
|
+
const php = available('php', ['-v']);
|
|
144
|
+
if (!php) fail('PHP isn\'t installed, or isn\'t on your PATH. Nevela needs PHP 8.3 or newer: https://www.php.net/downloads');
|
|
145
|
+
const version = /PHP (\d+)\.(\d+)/.exec(php);
|
|
146
|
+
if (!version || Number(version[1]) < 8 || (Number(version[1]) === 8 && Number(version[2]) < 3)) {
|
|
147
|
+
fail(`Nevela needs PHP 8.3 or newer. You have ${version ? `${version[1]}.${version[2]}` : 'a version I can\'t read'}.`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!available('composer')) fail('Composer isn\'t installed, or isn\'t on your PATH: https://getcomposer.org/download/');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Sanctum's trait on the User model. `install:api` asks for this by hand; do it for them. */
|
|
154
|
+
function addApiTokens(userModel) {
|
|
155
|
+
let source = fs.readFileSync(userModel, 'utf8');
|
|
156
|
+
if (source.includes('HasApiTokens')) return true;
|
|
157
|
+
const withImport = source.replace(/^(use Illuminate\\Notifications\\Notifiable;)$/m, '$1\nuse Laravel\\Sanctum\\HasApiTokens;');
|
|
158
|
+
const withTrait = withImport.replace(/^(\s+use )(HasFactory, Notifiable;)$/m, '$1HasApiTokens, $2');
|
|
159
|
+
if (withImport === source || withTrait === withImport) return false;
|
|
160
|
+
fs.writeFileSync(userModel, withTrait);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function write(file, contents) {
|
|
165
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
166
|
+
fs.writeFileSync(file, contents);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writeRootFiles(root, { name, title, pm }) {
|
|
170
|
+
write(path.join(root, 'package.json'), `${JSON.stringify({
|
|
171
|
+
name,
|
|
172
|
+
version: '0.1.0',
|
|
173
|
+
private: true,
|
|
174
|
+
type: 'module',
|
|
175
|
+
scripts: {
|
|
176
|
+
dev: 'node scripts/dev.mjs',
|
|
177
|
+
'dev:api': 'cd apps/api && php artisan serve',
|
|
178
|
+
'dev:web': `cd apps/web && ${pm} run dev`,
|
|
179
|
+
},
|
|
180
|
+
}, null, 2)}\n`);
|
|
181
|
+
|
|
182
|
+
write(path.join(root, '.gitignore'), 'node_modules/\nvendor/\n.env\n.env.*\n!.env.example\n.next/\n*.tsbuildinfo\nnext-env.d.ts\n');
|
|
183
|
+
|
|
184
|
+
write(path.join(root, 'scripts', 'dev.mjs'), `// Run the Laravel API and the Next.js dashboard together: \`${pm} run dev\`.
|
|
185
|
+
import { spawn } from 'node:child_process';
|
|
186
|
+
|
|
187
|
+
const shell = process.platform === 'win32';
|
|
188
|
+
const apps = [
|
|
189
|
+
{ name: 'api', colour: 35, command: 'php', args: ['artisan', 'serve'], cwd: 'apps/api' },
|
|
190
|
+
{ name: 'web', colour: 36, command: '${pm}', args: ['run', 'dev'], cwd: 'apps/web' },
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
const running = apps.map(({ name, colour, command, args, cwd }) => {
|
|
194
|
+
// One string when going through a shell: Node warns about (and doesn't escape) separate args there.
|
|
195
|
+
const child = shell
|
|
196
|
+
? spawn([command, ...args].join(' '), { cwd, shell, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
197
|
+
: spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
198
|
+
const prefix = \`\\x1b[\${colour}m\${name}\\x1b[0m │ \`;
|
|
199
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
200
|
+
let rest = '';
|
|
201
|
+
stream.on('data', (chunk) => {
|
|
202
|
+
const lines = (rest + chunk).split('\\n');
|
|
203
|
+
rest = lines.pop();
|
|
204
|
+
for (const line of lines) console.log(prefix + line);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
child.on('exit', (code) => {
|
|
208
|
+
console.log(\`\${prefix}stopped\${code ? \` (exit \${code})\` : ''}\`);
|
|
209
|
+
stop();
|
|
210
|
+
});
|
|
211
|
+
return child;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
let stopping = false;
|
|
215
|
+
function stop() {
|
|
216
|
+
if (stopping) return;
|
|
217
|
+
stopping = true;
|
|
218
|
+
for (const child of running) {
|
|
219
|
+
// On Windows the child is a shell; killing it alone would leave php and node running.
|
|
220
|
+
if (shell && child.pid) spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
221
|
+
else child.kill();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
process.on('SIGINT', stop);
|
|
225
|
+
process.on('SIGTERM', stop);
|
|
226
|
+
`);
|
|
227
|
+
|
|
228
|
+
write(path.join(root, 'README.md'), `# ${title}
|
|
229
|
+
|
|
230
|
+
A [Nevela](${DOCS}) app: a Laravel API in \`apps/api\` and a Next.js dashboard in \`apps/web\`.
|
|
231
|
+
|
|
232
|
+
## Run it
|
|
233
|
+
|
|
234
|
+
\`\`\`sh
|
|
235
|
+
${pm} run dev
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
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.
|
|
239
|
+
|
|
240
|
+
To create someone who can sign in:
|
|
241
|
+
|
|
242
|
+
\`\`\`sh
|
|
243
|
+
cd apps/api
|
|
244
|
+
php artisan nevela:user
|
|
245
|
+
\`\`\`
|
|
246
|
+
|
|
247
|
+
## Add a resource
|
|
248
|
+
|
|
249
|
+
\`\`\`sh
|
|
250
|
+
cd apps/api
|
|
251
|
+
php artisan nevela:resource Product --fields="name:string, sku:string!, price:money, notes:text?"
|
|
252
|
+
php artisan migrate
|
|
253
|
+
\`\`\`
|
|
254
|
+
|
|
255
|
+
Reload the dashboard: Products is in the sidebar. Fill it with \`php artisan nevela:seed Product\`.
|
|
256
|
+
|
|
257
|
+
## Before real use
|
|
258
|
+
|
|
259
|
+
Every policy in \`apps/api/app/Policies\` starts by allowing any signed-in user. Write your rules there.
|
|
260
|
+
|
|
261
|
+
Docs: ${DOCS}
|
|
262
|
+
`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function main() {
|
|
266
|
+
const options = parseArgs(process.argv.slice(2));
|
|
267
|
+
|
|
268
|
+
console.log(`\n ${indigo(bold('Nevela'))} ${dim(`v${VERSION}`)}\n`);
|
|
269
|
+
|
|
270
|
+
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
|
+
if (!name) {
|
|
274
|
+
if (!prompt) fail('Give the app a name: pnpm create nevela my-app');
|
|
275
|
+
name = (await prompt.question(' What is the app called? ')).trim();
|
|
276
|
+
}
|
|
277
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/.test(name)) {
|
|
278
|
+
fail(`"${name}" can't be used as a name. Use lowercase letters, numbers and dashes, for example my-app.`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const root = path.resolve(process.cwd(), name);
|
|
282
|
+
if (fs.existsSync(root) && fs.readdirSync(root).length > 0) fail(`${root} already exists and isn't empty.`);
|
|
283
|
+
|
|
284
|
+
checkRequirements();
|
|
285
|
+
const pm = detectPackageManager(options.pm);
|
|
286
|
+
const title = titleCase(name);
|
|
287
|
+
const api = path.join(root, 'apps', 'api');
|
|
288
|
+
const web = path.join(root, 'apps', 'web');
|
|
289
|
+
fs.mkdirSync(path.join(root, 'apps'), { recursive: true });
|
|
290
|
+
|
|
291
|
+
await step('Creating the Laravel app', () => {
|
|
292
|
+
run('composer', ['create-project', 'laravel/laravel', 'apps/api', '--no-interaction', '--no-progress', '--prefer-dist'], { cwd: root });
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const tokens = await step('Adding token sign-in (Sanctum)', () => {
|
|
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 () => {
|
|
301
|
+
const wanted = VERSION.split('.').slice(0, 2).map(Number);
|
|
302
|
+
if (!options.bundledPackage && (await onPackagist(wanted))) {
|
|
303
|
+
run('composer', ['require', `nevela/laravel:^${wanted.join('.')}`, '--no-interaction', '--no-progress'], { cwd: api });
|
|
304
|
+
} else {
|
|
305
|
+
// No matching release there (or --bundled-package): the copy that travels with this
|
|
306
|
+
// installer is placed in the app and installed by path.
|
|
307
|
+
copyLaravelPackage(path.join(root, 'packages', 'nevela-laravel'));
|
|
308
|
+
run('composer', ['config', 'repositories.nevela', 'path', '../../packages/nevela-laravel'], { cwd: api });
|
|
309
|
+
run('composer', ['require', 'nevela/laravel:@dev', '--no-interaction', '--no-progress'], { cwd: api });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await step('Adding the dashboard', () => {
|
|
314
|
+
copyWebTemplate(web, { name, title });
|
|
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 });
|
|
321
|
+
// Writes routes/nevela.php and the dashboard's (empty) resource registry.
|
|
322
|
+
run('php', ['artisan', 'nevela:generate'], { cwd: api });
|
|
323
|
+
run('php', ['artisan', 'migrate', '--force', '--no-interaction'], { cwd: api });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (options.install) {
|
|
327
|
+
await step(`Installing the dashboard's dependencies with ${pm}`, () => {
|
|
328
|
+
run(pm, ['install'], { cwd: web });
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (options.git && available('git') && !run('git', ['rev-parse', '--is-inside-work-tree'], { cwd: root, allowFailure: true }).ok) {
|
|
333
|
+
run('git', ['init', '--quiet'], { cwd: root, allowFailure: true });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let userCreated = false;
|
|
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`);
|
|
349
|
+
if (!tokens) {
|
|
350
|
+
console.log(` ${red('!')} I couldn't add Sanctum's trait to apps/api/app/Models/User.php. Add it by hand:`);
|
|
351
|
+
console.log(` ${dim('use Laravel\\Sanctum\\HasApiTokens; and use HasApiTokens; inside the class')}\n`);
|
|
352
|
+
}
|
|
353
|
+
console.log(' Next:\n');
|
|
354
|
+
console.log(` cd ${name}`);
|
|
355
|
+
if (!options.install) console.log(` cd apps/web && ${pm} install && cd ../..`);
|
|
356
|
+
if (!userCreated) console.log(` cd apps/api && php artisan nevela:user && cd ../.. ${dim('# someone to sign in as')}`);
|
|
357
|
+
console.log(` ${pm} run dev\n`);
|
|
358
|
+
console.log(` Then open ${indigo('http://localhost:3000/sign-in')}`);
|
|
359
|
+
console.log(` Add your first resource: ${dim('cd apps/api && php artisan nevela:resource Product --fields="name:string, price:money"')}`);
|
|
360
|
+
console.log(` Docs: ${DOCS}/start/quickstart/\n`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
main().catch((error) => fail(error?.message ?? String(error)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "create-nevela",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "create-nevela",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"template.mjs",
|
|
12
|
+
"packagist.mjs",
|
|
13
|
+
"template",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"prepack": "node build-template.mjs"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"nevela",
|
|
24
|
+
"laravel",
|
|
25
|
+
"nextjs",
|
|
26
|
+
"scaffold",
|
|
27
|
+
"create"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/MarkColeMukisa/nevela.git",
|
|
32
|
+
"directory": "packages/create-nevela"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://nevela-docs.vercel.app",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": "Mark Cole MUKISA"
|
|
37
|
+
}
|
package/packagist.mjs
ADDED
|
@@ -0,0 +1,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
|
+
/**
|
|
13
|
+
* Whether Packagist has a release of nevela/laravel that goes with this installer. Being
|
|
14
|
+
* listed isn't enough: with no tagged release yet, `composer require` would fail.
|
|
15
|
+
*/
|
|
16
|
+
export async function onPackagist(wanted) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch('https://repo.packagist.org/p2/nevela/laravel.json', { signal: AbortSignal.timeout(8000) });
|
|
19
|
+
if (!response.ok) return false;
|
|
20
|
+
const releases = (await response.json()).packages?.['nevela/laravel'] ?? [];
|
|
21
|
+
return releases.some((release) => satisfiesCaret(release.version, wanted));
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Cole MUKISA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# nevela/laravel
|
|
2
|
+
|
|
3
|
+
The Laravel side of [Nevela](https://nevela-docs.vercel.app): describe a resource once and get its model, migration, validation, policy and REST API, plus the Next.js dashboard screens for it.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
php artisan nevela:resource Product --fields="name:string, sku:string!, price:money, kind:enum(stock|digital), notes:text?"
|
|
7
|
+
php artisan migrate
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Start a new app
|
|
11
|
+
|
|
12
|
+
The quickest way is the create command, which sets up Laravel, this package and the dashboard together:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm create nevela my-app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Add it to an existing Laravel app
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
php artisan install:api # Sanctum, if you don't have it yet
|
|
22
|
+
composer require nevela/laravel
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Add Sanctum's trait to your user model:
|
|
26
|
+
|
|
27
|
+
```php
|
|
28
|
+
use Laravel\Sanctum\HasApiTokens;
|
|
29
|
+
|
|
30
|
+
class User extends Authenticatable
|
|
31
|
+
{
|
|
32
|
+
use HasApiTokens, HasFactory, Notifiable;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Needs PHP 8.2 or newer and Laravel 11, 12 or 13.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
| Command | What it does |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `nevela:resource` | Describe a resource and generate everything for it. |
|
|
43
|
+
| `nevela:generate` | Regenerate after changing a descriptor. Your own code is kept. |
|
|
44
|
+
| `nevela:seed` | Fill a resource with plausible records, and print how long it took. |
|
|
45
|
+
| `nevela:user` | Create someone who can sign in. |
|
|
46
|
+
|
|
47
|
+
Full documentation: https://nevela-docs.vercel.app
|
|
48
|
+
|
|
49
|
+
## Where this code lives
|
|
50
|
+
|
|
51
|
+
This repository is a read-only copy of `packages/laravel` from [MarkColeMukisa/nevela](https://github.com/MarkColeMukisa/nevela), kept so Composer can install the package. Open issues and pull requests there.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT. Copyright (c) 2026 Mark Cole MUKISA.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nevela/laravel",
|
|
3
|
+
"description": "Nevela for Laravel: describe a resource once and generate its model, migration, validation, policy and REST API, plus the Next.js dashboard screens for it.",
|
|
4
|
+
"type": "library",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"laravel",
|
|
8
|
+
"nevela",
|
|
9
|
+
"generator",
|
|
10
|
+
"crud",
|
|
11
|
+
"rest-api",
|
|
12
|
+
"nextjs",
|
|
13
|
+
"dashboard",
|
|
14
|
+
"scaffold"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://nevela-docs.vercel.app",
|
|
17
|
+
"authors": [
|
|
18
|
+
{
|
|
19
|
+
"name": "Mark Cole MUKISA",
|
|
20
|
+
"homepage": "https://github.com/MarkColeMukisa"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"support": {
|
|
24
|
+
"issues": "https://github.com/MarkColeMukisa/nevela/issues",
|
|
25
|
+
"source": "https://github.com/MarkColeMukisa/nevela",
|
|
26
|
+
"docs": "https://nevela-docs.vercel.app"
|
|
27
|
+
},
|
|
28
|
+
"require": {
|
|
29
|
+
"php": "^8.2",
|
|
30
|
+
"illuminate/support": "^11.0|^12.0|^13.0",
|
|
31
|
+
"illuminate/database": "^11.17|^12.0|^13.0",
|
|
32
|
+
"illuminate/http": "^11.0|^12.0|^13.0",
|
|
33
|
+
"illuminate/console": "^11.0|^12.0|^13.0"
|
|
34
|
+
},
|
|
35
|
+
"require-dev": {
|
|
36
|
+
"phpunit/phpunit": "^11.0"
|
|
37
|
+
},
|
|
38
|
+
"suggest": {
|
|
39
|
+
"laravel/sanctum": "Required for the token sign-in endpoints and the default auth:sanctum middleware (php artisan install:api)."
|
|
40
|
+
},
|
|
41
|
+
"autoload": {
|
|
42
|
+
"psr-4": {
|
|
43
|
+
"Nevela\\Laravel\\": "src/"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"autoload-dev": {
|
|
47
|
+
"psr-4": {
|
|
48
|
+
"Nevela\\Laravel\\Tests\\": "tests/"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"extra": {
|
|
52
|
+
"laravel": {
|
|
53
|
+
"providers": [
|
|
54
|
+
"Nevela\\Laravel\\NevelaServiceProvider"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"minimum-stability": "stable"
|
|
59
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
return [
|
|
4
|
+
/*
|
|
5
|
+
| Where resource descriptors live. One JSON file per resource; they are the
|
|
6
|
+
| generation input. Laravel code generated from them is the runtime authority.
|
|
7
|
+
*/
|
|
8
|
+
'descriptors_path' => base_path('nevela/resources'),
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
| The Flare-based Next.js app. Generated `.resource.ts` descriptors are written to
|
|
12
|
+
| {web_path}/resources. Set to null to skip frontend output.
|
|
13
|
+
*/
|
|
14
|
+
'web_path' => env('NEVELA_WEB_PATH', base_path('../web')),
|
|
15
|
+
|
|
16
|
+
/* URL prefix and middleware for generated resource routes (routes/nevela.php). */
|
|
17
|
+
'prefix' => 'api',
|
|
18
|
+
'middleware' => ['api', 'auth:sanctum'],
|
|
19
|
+
|
|
20
|
+
/* Token auth endpoints for the Next.js app: POST/DELETE {prefix}/auth/token, GET {prefix}/auth/me. */
|
|
21
|
+
'auth' => [
|
|
22
|
+
'enabled' => true,
|
|
23
|
+
'token_name' => 'nevela-web',
|
|
24
|
+
],
|
|
25
|
+
|
|
26
|
+
/* List defaults, matching Flare's client. */
|
|
27
|
+
'per_page' => 25,
|
|
28
|
+
'max_per_page' => 100,
|
|
29
|
+
];
|