Skip to content

Commit f9f4167

Browse files
authored
feat(vite): add Vite installer (#901)
1 parent ae6077a commit f9f4167

File tree

22 files changed

+472
-14
lines changed

22 files changed

+472
-14
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Console\Components\Renderers;
6+
7+
use Tempest\Support\StringHelper;
8+
use function Tempest\Support\arr;
9+
use function Tempest\Support\str;
10+
11+
final class InstructionsRenderer
12+
{
13+
private const int MAX_WIDTH = 175;
14+
15+
public function render(string|array $lines): string
16+
{
17+
$lines = arr($lines)
18+
->filter()
19+
->flatMap(fn (string $string) => str($string)->split(self::MAX_WIDTH)->toArray())
20+
->toArray();
21+
22+
$text = new StringHelper(PHP_EOL);
23+
24+
foreach ($lines as $line) {
25+
$text = $text->append(' ', '<style="bold fg-green">│</style>', ' ', trim($line), PHP_EOL);
26+
}
27+
28+
return $text->append(PHP_EOL)->toString();
29+
}
30+
}

src/Tempest/Console/src/Console.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public function success(string $lcontentsine, ?string $title = null): self;
7474

7575
public function keyValue(string $key, ?string $value = null): self;
7676

77+
public function instructions(array|string $lines): self;
78+
7779
/**
7880
* @param mixed|Closure(self): bool $condition
7981
* @param Closure(self): self $callback

src/Tempest/Console/src/GenericConsole.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Tempest\Console\Components\Interactive\TaskComponent;
2020
use Tempest\Console\Components\Interactive\TextInputComponent;
2121
use Tempest\Console\Components\InteractiveComponentRenderer;
22+
use Tempest\Console\Components\Renderers\InstructionsRenderer;
2223
use Tempest\Console\Components\Renderers\KeyValueRenderer;
2324
use Tempest\Console\Components\Renderers\MessageRenderer;
2425
use Tempest\Console\Exceptions\UnsupportedComponent;
@@ -116,6 +117,13 @@ public function header(string $header, ?string $subheader = null): static
116117
return $this;
117118
}
118119

120+
public function instructions(array|string $lines): static
121+
{
122+
$this->writeln((new InstructionsRenderer())->render(ArrayHelper::wrap($lines)));
123+
124+
return $this;
125+
}
126+
119127
public function writeln(string $line = ''): static
120128
{
121129
$this->write($line . PHP_EOL);

src/Tempest/Core/src/Commands/InstallCommand.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ public function __invoke(?string $installer = null): void
4242
}
4343

4444
$installer->install();
45-
46-
$this->success('Done.');
4745
}
4846

4947
private function resolveInstaller(?string $search): ?Installer

src/Tempest/Core/src/PublishesFiles.php

Lines changed: 93 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tempest\Core;
66

77
use Closure;
8+
use Exception;
89
use Tempest\Console\Exceptions\ConsoleException;
910
use Tempest\Console\HasConsole;
1011
use Tempest\Container\Inject;
@@ -15,12 +16,18 @@
1516
use Tempest\Generation\Exceptions\FileGenerationAbortedException;
1617
use Tempest\Generation\Exceptions\FileGenerationFailedException;
1718
use Tempest\Generation\StubFileGenerator;
19+
use Tempest\Reflection\FunctionReflector;
1820
use Tempest\Support\NamespaceHelper;
21+
use Tempest\Support\StringHelper;
1922
use Tempest\Validation\Rules\EndsWith;
2023
use Tempest\Validation\Rules\NotEmpty;
2124
use Throwable;
25+
use function strlen;
2226
use function Tempest\path;
27+
use function Tempest\root_path;
2328
use function Tempest\Support\str;
29+
use const JSON_PRETTY_PRINT;
30+
use const JSON_UNESCAPED_SLASHES;
2431

2532
/**
2633
* Provides a bunch of methods to publish and generate files and work with common user input.
@@ -45,14 +52,11 @@ trait PublishesFiles
4552
* @param string $destination The path to the destination file.
4653
* @param Closure(string $source, string $destination): void|null $callback A callback to run after the file is published.
4754
*/
48-
public function publish(
49-
string $source,
50-
string $destination,
51-
?Closure $callback = null,
52-
): void {
55+
public function publish(string $source, string $destination, ?Closure $callback = null): string|false
56+
{
5357
try {
5458
if (! $this->console->confirm(
55-
question: sprintf('Do you want to create <em>%s</em>?', $destination),
59+
question: sprintf('Do you want to create <file="%s" />?', $this->friendlyFileName($destination)),
5660
default: true,
5761
)) {
5862
throw new FileGenerationAbortedException('Skipped.');
@@ -96,7 +100,10 @@ public function publish(
96100
if ($callback !== null) {
97101
$callback($source, $destination);
98102
}
103+
104+
return $destination;
99105
} catch (FileGenerationAbortedException) {
106+
return false;
100107
} catch (Throwable $throwable) {
101108
if ($throwable instanceof ConsoleException) {
102109
throw $throwable;
@@ -183,9 +190,86 @@ public function askForOverride(string $targetPath): bool
183190
}
184191

185192
return $this->console->confirm(
186-
question: sprintf('The file <em>%s</em> already exists. Do you want to override it?', $targetPath),
187-
yes: 'Override',
188-
no: 'Cancel',
193+
question: sprintf('The file <file="%s" /> already exists. Do you want to override it?', $this->friendlyFileName($targetPath)),
189194
);
190195
}
196+
197+
/**
198+
* Updates the contents of a file at the given path.
199+
*
200+
* @param string $path The absolute path to the file to update.
201+
* @param Closure(string|StringHelper $contents): mixed $callback A callback that accepts the file contents and must return updated contents.
202+
* @param bool $ignoreNonExisting Whether to throw an exception if the file does not exist.
203+
*/
204+
public function update(string $path, Closure $callback, bool $ignoreNonExisting = false): void
205+
{
206+
if (! is_file($path)) {
207+
if ($ignoreNonExisting) {
208+
return;
209+
}
210+
211+
throw new Exception("The file at path [{$path}] does not exist.");
212+
}
213+
214+
$contents = file_get_contents($path);
215+
216+
$reflector = new FunctionReflector($callback);
217+
$type = $reflector->getParameters()->current()->getType();
218+
219+
$contents = match (true) {
220+
is_null($type),
221+
$type->equals(StringHelper::class) => (string) $callback(new StringHelper($contents)),
222+
$type->accepts('string') => (string) $callback($contents),
223+
default => throw new Exception('The callback must accept a string or StringHelper.'),
224+
};
225+
226+
file_put_contents($path, $contents);
227+
}
228+
229+
/**
230+
* Updates a JSON file, preserving indentation.
231+
*
232+
* @param string $path The absolute path to the file to update.
233+
* @param Closure(array): array $callback
234+
* @param bool $ignoreNonExisting Whether to throw an exception if the file does not exist.
235+
*/
236+
public function updateJson(string $path, Closure $callback, bool $ignoreNonExisting = false): void
237+
{
238+
$this->update($path, function (string $content) use ($callback) {
239+
$indent = $this->detectIndent($content);
240+
241+
$json = json_decode($content, associative: true);
242+
$json = $callback($json);
243+
244+
// PHP will output empty arrays for empty dependencies,
245+
// which is invalid and will make package managers crash.
246+
foreach (['dependencies', 'devDependencies', 'peerDependencies'] as $key) {
247+
if (isset($json[$key]) && empty($json[$key])) {
248+
unset($json[$key]);
249+
}
250+
}
251+
252+
$content = preg_replace_callback(
253+
'/^ +/m',
254+
fn ($m) => str_repeat($indent, strlen($m[0]) / 4),
255+
json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
256+
);
257+
258+
return "{$content}\n";
259+
}, $ignoreNonExisting);
260+
}
261+
262+
private function friendlyFileName(string $path): string
263+
{
264+
return str_replace(str(root_path())->finish('/')->toString(), '', $path);
265+
}
266+
267+
private function detectIndent(string $raw): string
268+
{
269+
try {
270+
return explode('"', explode("\n", $raw)[1])[0] ?: '';
271+
} catch (Throwable) {
272+
return ' ';
273+
}
274+
}
191275
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Support\JavaScript;
6+
7+
use Symfony\Component\Process\Process;
8+
use Tempest\Console\Console;
9+
use Tempest\Support\ArrayHelper;
10+
use Tempest\Validation\Rules\Enum;
11+
12+
/**
13+
* Helps with installing JavaScript dependencies in a directory.
14+
*/
15+
final class DependencyInstaller
16+
{
17+
public function __construct(
18+
private readonly Console $console,
19+
) {
20+
}
21+
22+
/**
23+
* Installs the specified JavaScript dependencies.
24+
* The package manager will be detected from the lockfile present in `$cwd`. If none found, it will be prompted to the user.
25+
*/
26+
public function installDependencies(string $cwd, string|array $dependencies, bool $dev = false): void
27+
{
28+
/** @var PackageManager */
29+
$packageManager = PackageManager::detect($cwd) ?? $this->console->ask(
30+
question: 'Which package manager do you wish to use?',
31+
options: PackageManager::class,
32+
default: PackageManager::BUN,
33+
validation: [
34+
new Enum(PackageManager::class),
35+
],
36+
);
37+
38+
$process = new Process([
39+
$packageManager->getBinaryName(),
40+
$packageManager->getInstallCommand(),
41+
$dev ? '-D' : '',
42+
...ArrayHelper::wrap($dependencies),
43+
], $cwd);
44+
45+
$this->console->task('Installing dependencies', $process);
46+
}
47+
48+
/**
49+
* Installs dependencies without interacting with the console.
50+
*/
51+
public function silentlyInstallDependencies(string $cwd, string|array $dependencies, bool $dev = false, ?PackageManager $defaultPackageManager = null): void
52+
{
53+
$install = $this->getInstallProcess(
54+
packageManager: PackageManager::detect($cwd) ?? $defaultPackageManager,
55+
cwd: $cwd,
56+
dependencies: $dependencies,
57+
dev: $dev,
58+
);
59+
60+
$install->mustRun();
61+
}
62+
63+
/**
64+
* Gets the `Process` instance that will install the specified dependencies.
65+
*/
66+
private function getInstallProcess(PackageManager $packageManager, string $cwd, string|array $dependencies, bool $dev = false): Process
67+
{
68+
return new Process([
69+
$packageManager->getBinaryName(),
70+
$packageManager->getInstallCommand(),
71+
$dev ? '-D' : '',
72+
...ArrayHelper::wrap($dependencies),
73+
], $cwd);
74+
}
75+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Support\JavaScript;
6+
7+
/**
8+
* Represents the major package managers in the JavaScript ecosystem.
9+
* This enum is backed for serialization purposes.
10+
*/
11+
enum PackageManager: string
12+
{
13+
case BUN = 'bun';
14+
case PNPM = 'pnpm';
15+
case YARN = 'yarn';
16+
case NPM = 'npm';
17+
18+
public function getLockFiles(): array
19+
{
20+
return match ($this) {
21+
self::BUN => ['bun.lock', 'bun.lockb'],
22+
self::NPM => ['package-lock.json'],
23+
self::YARN => ['yarn.lock'],
24+
self::PNPM => ['pnpm-lock.yaml'],
25+
};
26+
}
27+
28+
public function getBinaryName(): string
29+
{
30+
return match ($this) {
31+
self::BUN => 'bun',
32+
self::NPM => 'npm',
33+
self::YARN => 'yarn',
34+
self::PNPM => 'pnpm',
35+
};
36+
}
37+
38+
public function getInstallCommand(): string
39+
{
40+
return match ($this) {
41+
self::BUN => 'install',
42+
self::NPM => 'install',
43+
self::YARN => '',
44+
self::PNPM => 'install',
45+
};
46+
}
47+
48+
public static function detect(string $cwd): ?self
49+
{
50+
foreach (PackageManager::cases() as $packageManager) {
51+
foreach ($packageManager->getLockFiles() as $lockFile) {
52+
if (file_exists($cwd . '/' . $lockFile)) {
53+
return $packageManager;
54+
}
55+
}
56+
}
57+
58+
return null;
59+
}
60+
}

src/Tempest/Support/tests/JavaScript/Fixtures/bun-lock/bun.lock

Whitespace-only changes.

src/Tempest/Support/tests/JavaScript/Fixtures/bun-lockb/bun.lockb

Whitespace-only changes.

src/Tempest/Support/tests/JavaScript/Fixtures/empty/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)