Skip to content

Commit

Permalink
Implement TypeScript synchronization
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Jan 12, 2025
1 parent 93c8528 commit 85ac7b7
Show file tree
Hide file tree
Showing 11 changed files with 351 additions and 47 deletions.
99 changes: 79 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ composer require cerbero/enum
* [🦾 Console commands](#-console-commands)
* [🗒️ annotate](#%EF%B8%8F-annotate)
* [🏗️ make](#%EF%B8%8F-make)
* [💙 ts](#%EF%B8%8F-ts)

To supercharge our enums with all the features provided by this package, we can let our enums use the `Enumerates` trait:

Expand Down Expand Up @@ -406,6 +407,24 @@ This package provides a handy binary, built to automate different tasks. To lear
./vendor/bin/enum
```

For the console commands to work properly, the application base path is automatically guessed. However, in case of issues, we can manually set it by creating an `enums.php` file in the root of our app:

```php
<?php

use Cerbero\Enum\Enums;

Enums::setBasePath(__DIR__);
```

Some commands support the option `--all` to reference all the enums of our application. We can set the paths where enums live in our app in the `enums.php` configuration file as well:

```php
Enums::setPaths('app/Enums', 'domain/*/Enums');
```

In the above example, enums are discovered in the `app/Enums` directory and in all `Enums` sub-folders belonging to `domain`, e.g. `domain/Posts/Enums`, `domain/Users/Enums`, etc.

#### 🗒️ annotate

The `annotate` command automatically adds method annotations to enums, making IDEs autocompletion possible:
Expand All @@ -432,28 +451,12 @@ Otherwise we can annotate all our enums at once by enabling the option `--all`:
./vendor/bin/enum annotate -a
```

For the option `--all` to work, we need to set the paths where enums live in our application:

```php
use Cerbero\Enum\Enums;

Enums::setPaths('app/Enums', 'domain/*/Enums');
```

In the above example, enums are discovered in the `app/Enums` directory and in all `Enums` sub-folders belonging to `domain`, e.g. `domain/Posts/Enums`, `domain/Users/Enums`, etc.

This package tries to automatically find the application base path. However if enums can't be discovered after setting their paths, we can manually set our application base path:

```php
Enums::setBasePath(__DIR__ . '/path/to/our/app');
```

If we want to overwrite method annotations already annotated on enums, we can add the option `--force`:

```bash
php artisan enum:annotate App/Enums/Enum --force
./vendor/bin/enum annotate App/Enums/Enum --force

php artisan enum:annotate App/Enums/Enum -f
./vendor/bin/enum annotate App/Enums/Enum -f
```

#### 🏗️ make
Expand Down Expand Up @@ -497,10 +500,66 @@ php artisan enum:make App/Enums/Enum CaseOne CaseTwo --force
php artisan enum:make App/Enums/Enum CaseOne CaseTwo -f
```

This package tries to automatically find the application base path. However if enums can't be successfully created, we can manually set our application base path:
Finally, we can generate the TypeScript counterpart of the newly created enum by adding the `--typescript` option:

```bash
php artisan enum:make App/Enums/Enum CaseOne CaseTwo --typescript

php artisan enum:make App/Enums/Enum CaseOne CaseTwo -t
```

#### 💙 ts

The `ts` command turns enums into their TypeScript counterpart, synchronizing backend with frontend:

```bash
./vendor/bin/enum ts App/Enums/Enum

./vendor/bin/enum ts "App\Enums\Enum"
```

We can provide more than one enum to synchronize in TypeScript, if needed:

```bash
./vendor/bin/enum ts App/Enums/Enum1 App/Enums/Enum2

./vendor/bin/enum ts "App\Enums\Enum1" "App\Enums\Enum2"
```

Otherwise we can synchronize all our enums at once by enabling the option `--all`:

```bash
./vendor/bin/enum ts --all

./vendor/bin/enum ts -a
```

By default enums are synchronized in `resources/js/enums/index.ts`, however we can customize it in our `enums.php` configuration file:

```php
Enums::setBasePath(__DIR__ . '/path/to/our/app');
<?php

use Cerbero\Enum\Enums;

// custom static path
Enums::setTypeScript('frontend/enums/index.ts');

// custom dynamic path
Enums::setTypeScript(function (string $enum) {
$domain = explode('\\', $enum)[1];

return "resources/js/modules/{$domain}/enums.ts";
});
```

As seen above, we can either set a static path for our TypeScript enums or dynamically set the TypeScript path of an enum depending on its namespace.

If we want to update previously synchronized enums, we can add the option `--force`:

```bash
./vendor/bin/enum ts App/Enums/Enum --force

./vendor/bin/enum ts App/Enums/Enum -f
```

## 📆 Change log
Expand Down
3 changes: 1 addition & 2 deletions bin/enum
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use function Cerbero\Enum\splitArgv;

is_file($autoload = dirname(__DIR__, 1) . '/vendor/autoload.php') && require $autoload;
is_file($autoload = dirname(__DIR__, 4) . '/vendor/autoload.php') && require $autoload;
is_file($autoload = dirname(__DIR__, 4) . '/enums.php') && require $autoload;

if (is_file($command = path(__DIR__ . '/../cli/' . ($argv[1] ?? null) . '.php'))) {
try {
Expand All @@ -24,5 +25,3 @@ if (is_file($command = path(__DIR__ . '/../cli/' . ($argv[1] ?? null) . '.php'))
}

require path(__DIR__ . '/../cli/help');

?>
21 changes: 21 additions & 0 deletions cli/help
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Available options:
--backed=VALUE How cases should be backed. VALUE is either:
snake|camel|kebab|upper|lower|int0|int1|bitwise
-f, --force Whether the existing enum should be overwritten
-t, --typescript Whether the enum should be synced in TypeScript

Examples:
enum make App/Enums/MyEnum Case1 Case2
Expand All @@ -34,3 +35,23 @@ Examples:
enum make App/Enums/MyEnum Case1 Case2 --backed=int1
enum make App/Enums/MyEnum Case1 Case2 --force
enum make App/Enums/MyEnum Case1 Case2 --backed=bitwise --force
enum make App/Enums/MyEnum Case1 Case2 --typescript

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Synchronize enums in TypeScript.

Usage: enum ts enum1 [enum2 ...]

Available options:

-a, --all Whether all enums should be synchronized
-f, --force Whether existing enums should be overwritten

Examples:
enum ts App/Enums/MyEnum
enum ts "App\Enums\MyEnum"
enum ts App/Enums/MyEnum1 App/Enums/MyEnum2
enum ts App/Enums/MyEnum --force
enum ts --all
enum ts --all --force
9 changes: 8 additions & 1 deletion cli/make.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use function Cerbero\Enum\fail;
use function Cerbero\Enum\option;
use function Cerbero\Enum\runAnnotate;
use function Cerbero\Enum\runTs;
use function Cerbero\Enum\succeed;

if (! $enum = strtr($arguments[0] ?? '', '/', '\\')) {
Expand All @@ -31,4 +32,10 @@
return fail('The option --backed supports only ' . implode(', ', Backed::names()));
}

return enumOutcome($enum, fn() => $generator->generate($force) && runAnnotate($enum, $force));
$typeScript = !! array_intersect(['--typescript', '-t'], $options);

return enumOutcome($enum, function () use ($generator, $enum, $force, $typeScript) {
return $generator->generate($force)
&& runAnnotate($enum, $force)
&& ($typeScript ? runTs($enum, $force) : true);
});
25 changes: 25 additions & 0 deletions cli/ts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

use Cerbero\Enum\Enums;
use Cerbero\Enum\Services\TypeScript;

use function Cerbero\Enum\enumOutcome;
use function Cerbero\Enum\normalizeEnums;
use function Cerbero\Enum\succeed;

$enums = array_intersect(['--all', '-a'], $options) ? [...Enums::namespaces()] : normalizeEnums($arguments);

if (empty($enums)) {
return succeed('No enums to synchronize.');
}

$succeeded = true;
$force = !! array_intersect(['--force', '-f'], $options);

foreach ($enums as $enum) {
$succeeded = enumOutcome($enum, fn() => (new TypeScript($enum))->sync($force)) && $succeeded;
}

return $succeeded;
24 changes: 21 additions & 3 deletions helpers/cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,31 @@ function runAnnotate(string $enum, bool $force = false): bool
*/
function cli(string $command, ?int &$status = null): bool
{
$cmd = vsprintf('"%s" "%s" %s --base-path="%s" --paths="%s" 2>&1', [
$cmd = vsprintf('"%s" "%s" %s 2>&1', [
PHP_BINARY,
path(__DIR__ . '/../bin/enum'),
$command,
Enums::basePath(),
implode(',', Enums::paths()),
]);

return passthru($cmd, $status) === null;
}

/**
* Synchronize the given enum in TypeScript within a new process.
*
* @param class-string<\UnitEnum> $enum
*/
function runTs(string $enum, bool $force = false): bool
{
// Once an enum is loaded, PHP accesses it from the memory and not from the disk.
// Since we are writing on the disk, the enum in memory might get out of sync.
// To make sure that we are synchronizing the current content of such enum,
// we spin a new process to load in memory the latest state of the enum.
ob_start();

$succeeded = cli("ts \"{$enum}\"" . ($force ? ' --force' : ''));

ob_end_clean();

return $succeeded;
}
12 changes: 12 additions & 0 deletions helpers/core.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,15 @@ function path(string $path): string

return $head . implode(DIRECTORY_SEPARATOR, $segments);
}

/**
* Create the directory for the given path if missing.
*/
function ensureParentDirectory(string $path): bool
{
if (file_exists($directory = dirname($path))) {
return true;
}

return mkdir($directory, 0755, recursive: true);
}
65 changes: 47 additions & 18 deletions src/Enums.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ class Enums
*/
protected static array $paths = [];

/**
* The TypeScript path to sync enums in.
*
* @var Closure(class-string<UnitEnum>|string $enum): string|string
*/
protected static Closure|string $typeScript = 'resources/js/enums/index.ts';

/**
* The logic to run when an inaccessible enum method is called.
*
Expand Down Expand Up @@ -85,6 +92,28 @@ public static function paths(): array
return static::$paths;
}

/**
* Set the TypeScript path to sync enums in.
*
* @param callable(class-string<UnitEnum>|string $enum): string|string $path
*/
public static function setTypeScript(callable|string $path): void
{
/** @phpstan-ignore assign.propertyType */
static::$typeScript = is_callable($path) ? $path(...) : $path;
}

/**
* Retrieve the TypeScript path, optionally for the given enum.
*
* @param class-string<UnitEnum>|string $enum
* @return string
*/
public static function typeScript(string $enum = ''): string
{
return static::$typeScript instanceof Closure ? (static::$typeScript)($enum) : static::$typeScript;
}

/**
* Yield the namespaces of all the application enums.
*
Expand Down Expand Up @@ -127,46 +156,46 @@ public static function onStaticCall(callable $callback): void
}

/**
* Set the logic to run when an inaccessible case method is called.
* Handle the call to an inaccessible enum method.
*
* @param callable(UnitEnum $case, string $name, array<array-key, mixed> $arguments): mixed $callback
* @param class-string<UnitEnum> $enum
* @param array<array-key, mixed> $arguments
*/
public static function onCall(callable $callback): void
public static function handleStaticCall(string $enum, string $name, array $arguments): mixed
{
static::$onCall = $callback(...);
return static::$onStaticCall
? (static::$onStaticCall)($enum, $name, $arguments)
: $enum::fromName($name)->value(); /** @phpstan-ignore method.nonObject */
}

/**
* Set the logic to run when a case is invoked.
* Set the logic to run when an inaccessible case method is called.
*
* @param callable(UnitEnum $case, mixed ...$arguments): mixed $callback
* @param callable(UnitEnum $case, string $name, array<array-key, mixed> $arguments): mixed $callback
*/
public static function onInvoke(callable $callback): void
public static function onCall(callable $callback): void
{
static::$onInvoke = $callback(...);
static::$onCall = $callback(...);
}

/**
* Handle the call to an inaccessible enum method.
* Handle the call to an inaccessible case method.
*
* @param class-string<UnitEnum> $enum
* @param array<array-key, mixed> $arguments
*/
public static function handleStaticCall(string $enum, string $name, array $arguments): mixed
public static function handleCall(UnitEnum $case, string $name, array $arguments): mixed
{
return static::$onStaticCall
? (static::$onStaticCall)($enum, $name, $arguments)
: $enum::fromName($name)->value(); /** @phpstan-ignore method.nonObject */
return static::$onCall ? (static::$onCall)($case, $name, $arguments) : $case->resolveMetaAttribute($name);
}

/**
* Handle the call to an inaccessible case method.
* Set the logic to run when a case is invoked.
*
* @param array<array-key, mixed> $arguments
* @param callable(UnitEnum $case, mixed ...$arguments): mixed $callback
*/
public static function handleCall(UnitEnum $case, string $name, array $arguments): mixed
public static function onInvoke(callable $callback): void
{
return static::$onCall ? (static::$onCall)($case, $name, $arguments) : $case->resolveMetaAttribute($name);
static::$onInvoke = $callback(...);
}

/**
Expand Down
Loading

0 comments on commit 85ac7b7

Please sign in to comment.