create-nevela 0.1.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/index.mjs +77 -16
- package/package.json +2 -1
- package/project.mjs +20 -4
- package/selfupdate.mjs +13 -3
- package/template/laravel/README.md +1 -1
- package/template/laravel/composer.json +61 -59
- package/template/laravel/config/nevela.php +70 -0
- package/template/laravel/database/migrations/2026_10_04_000000_create_nevela_uploads_table.php +38 -0
- package/template/laravel/src/Console/GenerateCommand.php +25 -2
- package/template/laravel/src/Console/MakeResourceCommand.php +8 -0
- package/template/laravel/src/Console/SeedCommand.php +186 -124
- package/template/laravel/src/Console/StatusCommand.php +2 -2
- package/template/laravel/src/Console/UpdateCommand.php +76 -9
- package/template/laravel/src/Generator/ResourceGenerator.php +154 -21
- package/template/laravel/src/Http/ResourceRequest.php +4 -1
- package/template/laravel/src/Http/UploadController.php +147 -0
- package/template/laravel/src/Media/FileTypes.php +97 -0
- package/template/laravel/src/Media/ImageOptimizer.php +252 -0
- package/template/laravel/src/Media/Profile.php +94 -0
- package/template/laravel/src/Media/UploadRejected.php +8 -0
- package/template/laravel/src/Media/Uploads.php +325 -0
- package/template/laravel/src/Models/Upload.php +46 -0
- package/template/laravel/src/Nevela.php +20 -1
- package/template/laravel/src/NevelaServiceProvider.php +24 -0
- package/template/laravel/src/Rules/UploadKey.php +27 -0
- package/template/laravel/src/Support/Descriptor.php +3 -3
- package/template/laravel/src/Support/Fake.php +194 -171
- package/template/laravel/src/Support/Field.php +108 -12
- package/template/laravel/src/Support/TemplateSource.php +1 -1
- package/template/web/app/api/[...path]/route.ts +87 -0
- package/template/web/app/dashboard/actions.ts +26 -9
- package/template/web/components/dashboard/fields/file-cell.tsx +10 -47
- package/template/web/components/dashboard/fields/file-field.tsx +25 -31
- package/template/web/lib/files.ts +21 -0
- package/template/web/package.json +1 -1
- package/template.mjs +2 -2
- package/tool.mjs +58 -0
|
@@ -1,124 +1,186 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
namespace Nevela\Laravel\Console;
|
|
4
|
-
|
|
5
|
-
use Illuminate\Console\Command;
|
|
6
|
-
use Illuminate\Database\UniqueConstraintViolationException;
|
|
7
|
-
use Illuminate\Support\Facades\DB;
|
|
8
|
-
use Illuminate\Support\Str;
|
|
9
|
-
use InvalidArgumentException;
|
|
10
|
-
use Nevela\Laravel\
|
|
11
|
-
use Nevela\Laravel\
|
|
12
|
-
use Nevela\Laravel\Support\
|
|
13
|
-
use
|
|
14
|
-
use
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
{--
|
|
22
|
-
{--
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
$
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
$
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
$
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
$this->
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
$
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Nevela\Laravel\Console;
|
|
4
|
+
|
|
5
|
+
use Illuminate\Console\Command;
|
|
6
|
+
use Illuminate\Database\UniqueConstraintViolationException;
|
|
7
|
+
use Illuminate\Support\Facades\DB;
|
|
8
|
+
use Illuminate\Support\Str;
|
|
9
|
+
use InvalidArgumentException;
|
|
10
|
+
use Nevela\Laravel\Media\Uploads;
|
|
11
|
+
use Nevela\Laravel\Nevela;
|
|
12
|
+
use Nevela\Laravel\Support\Descriptor;
|
|
13
|
+
use Nevela\Laravel\Support\Fake;
|
|
14
|
+
use Symfony\Component\Console\Helper\ProgressBar;
|
|
15
|
+
use Throwable;
|
|
16
|
+
|
|
17
|
+
final class SeedCommand extends Command
|
|
18
|
+
{
|
|
19
|
+
protected $signature = 'nevela:seed
|
|
20
|
+
{name : The resource to fill, e.g. Product}
|
|
21
|
+
{--count=1000 : How many records to create}
|
|
22
|
+
{--chunk=500 : Rows per insert statement}
|
|
23
|
+
{--fresh : Delete the resource\'s existing records first}';
|
|
24
|
+
|
|
25
|
+
protected $description = 'Fill a resource with plausible records, and say how long it took';
|
|
26
|
+
|
|
27
|
+
private const MAX_COUNT = 5_000_000;
|
|
28
|
+
|
|
29
|
+
/** 400 blocks of MAX_COUNT stay under 2^31, so a unique integer column can hold the numbers. */
|
|
30
|
+
private const BLOCKS = 400;
|
|
31
|
+
|
|
32
|
+
private const ATTEMPTS = 5;
|
|
33
|
+
|
|
34
|
+
public function handle(): int
|
|
35
|
+
{
|
|
36
|
+
try {
|
|
37
|
+
$resource = Nevela::resource(Str::studly($this->argument('name')));
|
|
38
|
+
} catch (InvalidArgumentException $e) {
|
|
39
|
+
$this->components->error($e->getMessage());
|
|
40
|
+
|
|
41
|
+
return self::FAILURE;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
$count = filter_var($this->option('count'), FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => self::MAX_COUNT]]);
|
|
45
|
+
$chunk = filter_var($this->option('chunk'), FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 5000]]);
|
|
46
|
+
if ($count === false || $chunk === false) {
|
|
47
|
+
$this->components->error('--count must be between 1 and 5,000,000, and --chunk between 1 and 5,000.');
|
|
48
|
+
|
|
49
|
+
return self::FAILURE;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// SQLite allows 32,766 bound values per statement; stay well inside it on wide tables.
|
|
53
|
+
$chunk = min($chunk, max(1, intdiv(30_000, count($resource->fields) + 3)));
|
|
54
|
+
$fresh = (bool) $this->option('fresh');
|
|
55
|
+
|
|
56
|
+
$existing = $fresh ? 0 : (int) DB::table($resource->table)->count();
|
|
57
|
+
foreach ($resource->fields as $field) {
|
|
58
|
+
$capacity = Fake::capacity($field);
|
|
59
|
+
if ($capacity !== null && $existing + $count > $capacity) {
|
|
60
|
+
$room = number_format(max(0, $capacity - $existing));
|
|
61
|
+
$this->components->error("\"{$field->name}\" is unique and can only hold ".number_format($capacity)." different values, so there is room for {$room} more {$resource->pluralLabel}, not ".number_format($count).'.');
|
|
62
|
+
|
|
63
|
+
return self::FAILURE;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --fresh deletes first, and a record others are required to belong to can't be deleted.
|
|
68
|
+
// Said here, before any placeholder pictures are made for a run that can't happen.
|
|
69
|
+
if ($fresh) {
|
|
70
|
+
foreach (Nevela::all() as $other) {
|
|
71
|
+
foreach ($other->fields as $field) {
|
|
72
|
+
if ($field->kind === 'belongsTo' && $field->target === $resource->name && $field->required && $other->name !== $resource->name
|
|
73
|
+
&& ($children = (int) DB::table($other->table)->count()) > 0) {
|
|
74
|
+
$this->components->error(number_format($children)." {$other->pluralLabel} belong to the {$resource->pluralLabel} that --fresh would delete. Remove them first, or seed without --fresh.");
|
|
75
|
+
|
|
76
|
+
return self::FAILURE;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
$started = microtime(true);
|
|
83
|
+
try {
|
|
84
|
+
$pools = $this->pools($resource);
|
|
85
|
+
} catch (InvalidArgumentException $e) {
|
|
86
|
+
$this->components->error($e->getMessage());
|
|
87
|
+
|
|
88
|
+
return self::FAILURE;
|
|
89
|
+
}
|
|
90
|
+
$removed = 0;
|
|
91
|
+
$bar = $this->output->createProgressBar($count);
|
|
92
|
+
try {
|
|
93
|
+
for ($attempt = 1; ; $attempt++) {
|
|
94
|
+
// Unique fields are numbered from a block of MAX_COUNT picked per run (a run is at
|
|
95
|
+
// most that long). Two runs only meet if they pick the same block; then the insert
|
|
96
|
+
// fails, the transaction rolls back and another block is picked.
|
|
97
|
+
$offset = $fresh && $attempt === 1 ? 0 : mt_rand(0, self::BLOCKS - 1) * self::MAX_COUNT;
|
|
98
|
+
$bar->start();
|
|
99
|
+
try {
|
|
100
|
+
$removed = DB::transaction(fn () => $this->fill($resource, $count, $chunk, $offset, $fresh, $bar, $pools));
|
|
101
|
+
break;
|
|
102
|
+
} catch (UniqueConstraintViolationException $e) {
|
|
103
|
+
if ($attempt === self::ATTEMPTS) {
|
|
104
|
+
throw $e;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
$bar->finish();
|
|
109
|
+
$this->newLine(2);
|
|
110
|
+
} catch (Throwable $e) {
|
|
111
|
+
$this->newLine(2);
|
|
112
|
+
$this->components->error("Seeding {$resource->pluralLabel} failed, and the table is as it was: ".Str::limit($e->getMessage(), 300));
|
|
113
|
+
|
|
114
|
+
return self::FAILURE;
|
|
115
|
+
}
|
|
116
|
+
if ($fresh) {
|
|
117
|
+
$this->components->info('Removed '.number_format($removed)." existing {$resource->pluralLabel}.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$seconds = microtime(true) - $started;
|
|
121
|
+
$time = $seconds < 1 ? round($seconds * 1000).' ms' : number_format($seconds, 2).' s';
|
|
122
|
+
$rate = number_format($count / max($seconds, 0.001));
|
|
123
|
+
$this->components->info('Seeded '.number_format($count)." {$resource->pluralLabel} in {$time} ({$rate} rows/s).");
|
|
124
|
+
$this->components->twoColumnDetail("{$resource->pluralLabel} in the database", number_format(DB::table($resource->table)->count()));
|
|
125
|
+
|
|
126
|
+
return self::SUCCESS;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* What a relation or an image field can be filled with: the records that exist to point
|
|
131
|
+
* at, and a handful of pictures made for the purpose.
|
|
132
|
+
*
|
|
133
|
+
* @return array<string, list<string>>
|
|
134
|
+
*
|
|
135
|
+
* @throws InvalidArgumentException when a required field has nothing to draw from
|
|
136
|
+
*/
|
|
137
|
+
private function pools(Descriptor $resource): array
|
|
138
|
+
{
|
|
139
|
+
$pools = [];
|
|
140
|
+
foreach ($resource->fields as $field) {
|
|
141
|
+
if ($field->kind === 'belongsTo') {
|
|
142
|
+
$target = $field->target === $resource->name ? $resource : Nevela::resource((string) $field->target);
|
|
143
|
+
$ids = DB::table($target->table)->inRandomOrder()->limit(1000)->pluck('id')->map(fn ($id) => (string) $id)->all();
|
|
144
|
+
if ($field->unique) {
|
|
145
|
+
throw new InvalidArgumentException("\"{$field->name}\" is unique, so each {$target->label} can be used once. The seeder doesn't fill one-to-one relations; add those records yourself.");
|
|
146
|
+
}
|
|
147
|
+
if ($ids === [] && $field->required && $target->name !== $resource->name) {
|
|
148
|
+
throw new InvalidArgumentException("Every {$resource->label} needs a {$target->label}, and there are none yet. Seed them first: php artisan nevela:seed {$target->name}");
|
|
149
|
+
}
|
|
150
|
+
$pools[$field->name] = $ids;
|
|
151
|
+
} elseif ($field->kind === 'file') {
|
|
152
|
+
$pools[$field->name] = $field->isImage() ? Uploads::placeholders($resource, $field) : [];
|
|
153
|
+
if ($pools[$field->name] === [] && $field->required) {
|
|
154
|
+
throw new InvalidArgumentException($field->isImage()
|
|
155
|
+
? "\"{$field->name}\" is a required image, and PHP's GD extension isn't installed to make placeholder pictures with."
|
|
156
|
+
: "\"{$field->name}\" is a required file, which the seeder can't make up. Make it optional ({$field->name}:file(…)?) or add records yourself.");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return $pools;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Runs inside the transaction: the optional delete and every insert succeed or fail together.
|
|
166
|
+
*
|
|
167
|
+
* @param array<string, list<string>> $pools
|
|
168
|
+
*/
|
|
169
|
+
private function fill(Descriptor $resource, int $count, int $chunk, int $offset, bool $fresh, ProgressBar $bar, array $pools = []): int
|
|
170
|
+
{
|
|
171
|
+
$removed = $fresh ? DB::table($resource->table)->delete() : 0;
|
|
172
|
+
$now = time();
|
|
173
|
+
for ($done = 0; $done < $count; $done += $chunk) {
|
|
174
|
+
$rows = [];
|
|
175
|
+
for ($i = $done; $i < min($count, $done + $chunk); $i++) {
|
|
176
|
+
// Spread over the last 60 days, so the dashboard's "this week" and trend have something to show.
|
|
177
|
+
$created = date('Y-m-d H:i:s', $now - mt_rand(0, 60 * 86400));
|
|
178
|
+
$rows[] = ['id' => (string) Str::uuid7()] + Fake::row($resource, $offset + $i + 1, $pools) + ['created_at' => $created, 'updated_at' => $created];
|
|
179
|
+
}
|
|
180
|
+
DB::table($resource->table)->insert($rows);
|
|
181
|
+
$bar->advance(count($rows));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return $removed;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -30,7 +30,7 @@ final class StatusCommand extends Command
|
|
|
30
30
|
$latest = Releases::latest();
|
|
31
31
|
$row('Nevela', Nevela::VERSION.match (true) {
|
|
32
32
|
$latest === null => ' <fg=gray>(couldn\'t check for a newer one)</>',
|
|
33
|
-
version_compare($latest, Nevela::VERSION, '>') => " <fg=yellow>({$latest} is out: php nevela
|
|
33
|
+
version_compare($latest, Nevela::VERSION, '>') => " <fg=yellow>({$latest} is out: php nevela upgrade)</>",
|
|
34
34
|
default => ' <fg=green>(latest)</>',
|
|
35
35
|
});
|
|
36
36
|
$row('Laravel / PHP', app()->version().' / '.PHP_VERSION);
|
|
@@ -99,7 +99,7 @@ final class StatusCommand extends Command
|
|
|
99
99
|
// What this app has made its own. An update leaves every one of these alone.
|
|
100
100
|
$changes = $state->yourChanges($web);
|
|
101
101
|
if ($changes === null) {
|
|
102
|
-
$row('Your dashboard changes', '<fg=gray>not tracked yet: php nevela
|
|
102
|
+
$row('Your dashboard changes', '<fg=gray>not tracked yet: php nevela upgrade records them</>');
|
|
103
103
|
} else {
|
|
104
104
|
$count = count($changes['changed']) + count($changes['deleted']);
|
|
105
105
|
$row('Your dashboard changes', $count === 0 ? 'none' : "{$count} file(s), kept on every update");
|
|
@@ -16,12 +16,18 @@ use Throwable;
|
|
|
16
16
|
|
|
17
17
|
final class UpdateCommand extends Command
|
|
18
18
|
{
|
|
19
|
-
protected $signature = 'nevela:
|
|
19
|
+
protected $signature = 'nevela:upgrade
|
|
20
20
|
{--check : Show what would change, and change nothing}
|
|
21
|
-
{--skip-package : Leave the nevela/laravel package as it is;
|
|
22
|
-
{--undo : Put the dashboard back as it was before the last
|
|
21
|
+
{--skip-package : Leave the nevela/laravel package as it is; upgrade only the dashboard}
|
|
22
|
+
{--undo : Put the dashboard back as it was before the last upgrade}';
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The name this command had before 0.3.0, and still answers to. As in Grit, "update"
|
|
26
|
+
* is now what the nevela command does to itself, and "upgrade" is what it does to an app.
|
|
27
|
+
*/
|
|
28
|
+
protected $aliases = ['nevela:update'];
|
|
29
|
+
|
|
30
|
+
protected $description = 'Bring this app to the latest Nevela: the package, the generated code and the dashboard';
|
|
25
31
|
|
|
26
32
|
/** Apps created before the dashboard recorded its version all came from this template. */
|
|
27
33
|
private const FIRST_TRACKED = '0.1.1';
|
|
@@ -45,7 +51,48 @@ final class UpdateCommand extends Command
|
|
|
45
51
|
$this->modernizeDevScript();
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
|
|
54
|
+
$status = $this->updateDashboard($check);
|
|
55
|
+
|
|
56
|
+
// A new version may bring a table of its own (uploads did). Say so, don't run it:
|
|
57
|
+
// changing a database is the developer's call.
|
|
58
|
+
if (! $check && ($pending = $this->pendingMigrations()) > 0) {
|
|
59
|
+
$this->newLine();
|
|
60
|
+
$this->components->warn("{$pending} migration(s) have not been run. Run them before using the app: php nevela migrate");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return $status;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** What the app's composer.json asks for, or an empty string when it can't be read. */
|
|
67
|
+
public static function rootConstraint(string $composerJson): string
|
|
68
|
+
{
|
|
69
|
+
$manifest = json_decode($composerJson, true);
|
|
70
|
+
|
|
71
|
+
return is_array($manifest) && is_string($manifest['require']['nevela/laravel'] ?? null) ? trim($manifest['require']['nevela/laravel']) : '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether a constraint is the kind the installer writes ("^0.1", "^0.1.3") and nothing
|
|
76
|
+
* more: the only kind an update moves on its own.
|
|
77
|
+
*/
|
|
78
|
+
public static function isPlainCaret(string $constraint): bool
|
|
79
|
+
{
|
|
80
|
+
return preg_match('/^\^\d+\.\d+(\.\d+)?$/', $constraint) === 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private function pendingMigrations(): int
|
|
84
|
+
{
|
|
85
|
+
try {
|
|
86
|
+
$migrator = app('migrator');
|
|
87
|
+
if (! $migrator->repositoryExists()) {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
$files = $migrator->getMigrationFiles([...$migrator->paths(), database_path('migrations')]);
|
|
91
|
+
|
|
92
|
+
return count(array_diff(array_keys($files), $migrator->getRepository()->getRan()));
|
|
93
|
+
} catch (\Throwable) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
49
96
|
}
|
|
50
97
|
|
|
51
98
|
/**
|
|
@@ -115,15 +162,35 @@ final class UpdateCommand extends Command
|
|
|
115
162
|
$source = (string) @file_get_contents(base_path('vendor/nevela/laravel/src/Nevela.php'));
|
|
116
163
|
$now = preg_match("/const VERSION = '([^']+)'/", $source, $m) ? $m[1] : $installed;
|
|
117
164
|
if ($now === $installed) {
|
|
118
|
-
|
|
165
|
+
// composer.json's range doesn't reach the new version: below 1.0, "^0.1" stops
|
|
166
|
+
// at 0.1.x. Updating is asking for the newest, so ask Composer for the range
|
|
167
|
+
// that has it. `require` rewrites composer.json and installs in one step.
|
|
168
|
+
$range = '^'.implode('.', array_slice(explode('.', $latest), 0, 2));
|
|
169
|
+
$current = self::rootConstraint((string) @file_get_contents(base_path('composer.json')));
|
|
170
|
+
if (! self::isPlainCaret($current)) {
|
|
171
|
+
// A pin, a branch or a hand-written range is somebody's decision. It is left
|
|
172
|
+
// as it is, and the update stops here.
|
|
173
|
+
$this->components->warn("Composer kept {$installed}: composer.json asks for \"{$current}\", which doesn't allow {$latest}. That looks deliberate, so it was left alone. To move, set \"nevela/laravel\" to \"{$range}\" there and run this again.");
|
|
174
|
+
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
$this->components->info("composer.json asks for \"{$current}\", which doesn't reach {$latest}. Moving \"nevela/laravel\" to \"{$range}\".");
|
|
178
|
+
$require = new Process(['composer', 'require', "nevela/laravel:{$range}", '--no-interaction', '--no-progress', '--update-with-dependencies'], base_path(), null, null, null);
|
|
179
|
+
$require->run(fn ($type, $buffer) => $this->output->write($buffer));
|
|
119
180
|
|
|
120
|
-
|
|
181
|
+
$source = (string) @file_get_contents(base_path('vendor/nevela/laravel/src/Nevela.php'));
|
|
182
|
+
$now = preg_match("/const VERSION = '([^']+)'/", $source, $m) ? $m[1] : $installed;
|
|
183
|
+
if (! $require->isSuccessful() || $now === $installed) {
|
|
184
|
+
$this->components->warn("Composer kept {$installed}. Set \"nevela/laravel\" to \"{$range}\" in composer.json, see what Composer says about it, and run this again.");
|
|
185
|
+
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
121
188
|
}
|
|
122
189
|
|
|
123
190
|
// The package's own files have just been replaced. Finish in a new process, so the
|
|
124
191
|
// rest runs on the new code instead of a mix of old and new.
|
|
125
192
|
$php = (new PhpExecutableFinder)->find(false) ?: 'php';
|
|
126
|
-
$rest = new Process([$php, 'artisan', 'nevela:
|
|
193
|
+
$rest = new Process([$php, 'artisan', 'nevela:upgrade', '--skip-package'], base_path(), null, null, null);
|
|
127
194
|
$rest->run(fn ($type, $buffer) => $this->output->write($buffer));
|
|
128
195
|
|
|
129
196
|
return $rest->getExitCode() ?? self::FAILURE;
|
|
@@ -251,7 +318,7 @@ final class UpdateCommand extends Command
|
|
|
251
318
|
$state->write($web);
|
|
252
319
|
|
|
253
320
|
if ($touched) {
|
|
254
|
-
$this->line(" Every file this replaced was copied to <fg=cyan>{$this->relative($web)}/{$backup}</> first. To put it all back: <fg=cyan>php nevela
|
|
321
|
+
$this->line(" Every file this replaced was copied to <fg=cyan>{$this->relative($web)}/{$backup}</> first. To put it all back: <fg=cyan>php nevela upgrade --undo</>");
|
|
255
322
|
}
|
|
256
323
|
}
|
|
257
324
|
if ($done[DashboardUpdate::CONFLICT] !== []) {
|