diff --git a/README.md b/README.md index e3e21a1..f83917f 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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 + diff --git a/cli/help b/cli/help index 94d152f..75c0e2f 100644 --- a/cli/help +++ b/cli/help @@ -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 @@ -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 diff --git a/cli/make.php b/cli/make.php index 23466e8..dc1d5a3 100644 --- a/cli/make.php +++ b/cli/make.php @@ -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] ?? '', '/', '\\')) { @@ -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); +}); diff --git a/cli/ts.php b/cli/ts.php new file mode 100644 index 0000000..5f2799e --- /dev/null +++ b/cli/ts.php @@ -0,0 +1,25 @@ + (new TypeScript($enum))->sync($force)) && $succeeded; +} + +return $succeeded; diff --git a/helpers/cli.php b/helpers/cli.php index 6b00dea..78aa715 100644 --- a/helpers/cli.php +++ b/helpers/cli.php @@ -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; +} diff --git a/helpers/core.php b/helpers/core.php index 84d8966..68693e1 100644 --- a/helpers/core.php +++ b/helpers/core.php @@ -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); +} diff --git a/src/Enums.php b/src/Enums.php index d6cffc1..c7783c6 100644 --- a/src/Enums.php +++ b/src/Enums.php @@ -28,6 +28,13 @@ class Enums */ protected static array $paths = []; + /** + * The TypeScript path to sync enums in. + * + * @var Closure(class-string|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. * @@ -85,6 +92,28 @@ public static function paths(): array return static::$paths; } + /** + * Set the TypeScript path to sync enums in. + * + * @param callable(class-string|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|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. * @@ -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 $arguments): mixed $callback + * @param class-string $enum + * @param array $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 $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 $enum * @param array $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 $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(...); } /** diff --git a/src/Services/Generator.php b/src/Services/Generator.php index f5b65e3..ea90164 100644 --- a/src/Services/Generator.php +++ b/src/Services/Generator.php @@ -7,6 +7,8 @@ use Cerbero\Enum\Data\GeneratingEnum; use Cerbero\Enum\Enums\Backed; +use function Cerbero\Enum\ensureParentDirectory; + /** * The enums generator. */ @@ -38,9 +40,7 @@ public function generate(bool $overwrite = false): bool return true; } - if (! file_exists($directory = dirname($this->enum->path))) { - mkdir($directory, 0755, recursive: true); - } + ensureParentDirectory($this->enum->path); $stub = (string) file_get_contents($this->stub()); $content = strtr($stub, $this->replacements()); diff --git a/src/Services/TypeScript.php b/src/Services/TypeScript.php new file mode 100644 index 0000000..e4600de --- /dev/null +++ b/src/Services/TypeScript.php @@ -0,0 +1,131 @@ + $enum + */ + public function __construct(protected readonly string $enum) + { + $this->path = Enums::basePath(Enums::typeScript($enum)); + } + + /** + * Synchronize the enum in TypeScript. + */ + public function sync(bool $overwrite = false): bool + { + return match (true) { + ! file_exists($this->path) => $this->createEnum(), + $this->enumIsMissing() => $this->appendEnum(), + $overwrite => $this->replaceEnum(), + default => true, + }; + } + + /** + * Create the TypeScript file for the enum. + */ + protected function createEnum(): bool + { + ensureParentDirectory($this->path); + + return file_put_contents($this->path, $this->transform()) !== false; + } + + /** + * Append the enum to the TypeScript file. + */ + protected function appendEnum(): bool + { + return file_put_contents($this->path, PHP_EOL . $this->transform(), flags: FILE_APPEND) !== false; + } + + /** + * Retrieved the enum transformed for TypeScript. + */ + public function transform(): string + { + $stub = (string) file_get_contents($this->stub()); + + return strtr($stub, $this->replacements()); + } + + /** + * Retrieve the path of the stub. + */ + protected function stub(): string + { + return __DIR__ . '/../../stubs/typescript.stub'; + } + + /** + * Retrieve the stub replacements. + * + * @return array + */ + protected function replacements(): array + { + return [ + '{{ name }}' => className($this->enum), + '{{ cases }}' => $this->formatCases(), + ]; + } + + /** + * Retrieve the enum cases formatted as a string + */ + protected function formatCases(): string + { + $cases = array_map(function (UnitEnum $case) { + /** @var string|int|null $value */ + $value = is_string($value = $case->value ?? null) ? "'{$value}'" : $value; + + return " {$case->name}" . ($value === null ? ',' : " = {$value},"); + }, $this->enum::cases()); + + return implode(PHP_EOL, $cases); + } + + /** + * Determine whether the enum is missing. + */ + protected function enumIsMissing(): bool + { + $name = className($this->enum); + + return preg_match("~^export enum {$name}~im", (string) file_get_contents($this->path)) === 0; + } + + /** + * Replace the enum in the TypeScript file. + */ + protected function replaceEnum(): bool + { + $name = className($this->enum); + $oldContent = (string) file_get_contents($this->path); + $newContent = preg_replace("~^(export enum {$name}[^}]+})~im", trim($this->transform()), $oldContent); + + return file_put_contents($this->path, $newContent) !== false; + } +} diff --git a/stubs/typescript.stub b/stubs/typescript.stub new file mode 100644 index 0000000..d6b6976 --- /dev/null +++ b/stubs/typescript.stub @@ -0,0 +1,3 @@ +export enum {{ name }} { +{{ cases }} +}