From be1557a800d82f0ca25290ffe0ab33bf93f5873a Mon Sep 17 00:00:00 2001 From: Rick Bongers Date: Tue, 17 Mar 2026 08:16:44 +0100 Subject: [PATCH] feat: extract php namespace to ts namespaces --- README.md | 7 ++++- src/Actions/TranspileToTypescript.php | 29 ++++++++++++++++----- src/Types/Types.php | 4 +-- src/Values/TypescriptType.php | 12 +++++++-- tests/Actions/TranspileToTypescriptTest.php | 8 +++--- tests/Converters/ResourceConverterTest.php | 2 +- tests/Types/TypesTest.php | 2 +- 7 files changed, 46 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ae6ee2d..7c6106a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ Generate TypeScript interfaces from Laravel Models & Resources This resource: ```PHP + +namespace App\Http\Resources; + use Illuminate\Http\Resources\Json\JsonResource; /** @mixin \App\Models\Product */ @@ -30,10 +33,12 @@ class ProductResource extends JsonResource will be converted into this interface: ```typescript -export interface ProductResourceType { +declare namespace App.Http.Resources { + export interface ProductResourceType { id: number; name: string; hidden?: boolean; + } } ``` diff --git a/src/Actions/TranspileToTypescript.php b/src/Actions/TranspileToTypescript.php index cc3936a..53f9605 100644 --- a/src/Actions/TranspileToTypescript.php +++ b/src/Actions/TranspileToTypescript.php @@ -19,30 +19,45 @@ public function __construct() /** @param Collection $types */ public function handle(Collection $types) { - $types->each(fn (TypescriptType $type) => $this->processType($type)); + $lines = $this->lines; + + $this->groupTypes($types)->each(function (Collection $groupedTypes, string $group) use (&$lines) { + $lines->push("declare namespace {$group} {"); + $groupedTypes->each(fn (TypescriptType $type) => $this->processType($type)); + $lines->push('}'.PHP_EOL); + }); return $this->lines->join(PHP_EOL); } - private function processType(TypescriptType $type): void + /** + * @param Collection $types + * @return Collection> + */ + private function groupTypes(Collection $types): Collection { - $this->lines->push("// {$type->getClass()}"); + return $types->mapToGroups(function (TypescriptType $type) { + return [TypescriptType::determineNamespace($type->getClass()) => $type]; + }); + } + private function processType(TypescriptType $type): void + { if ($type->listProperties()->isEmpty()) { - $this->lines->push("export type {$type->getName()} = any"); + $this->lines->push("\texport type {$type->getName()} = any"); return; } - $this->lines->push("export type {$type->getName()} = {"); + $this->lines->push("\texport type {$type->getName()} = {"); $type->listProperties()->each(fn (TypescriptProperty $property) => $this->processProperty($property)); - $this->lines->push('}'); + $this->lines->push("\t}"); } private function processProperty(TypescriptProperty $property): void { - $this->lines->push("{$property->getName()}:{$property->getType()}"); + $this->lines->push("\t\t{$property->getName()}: {$property->getType()};"); } } diff --git a/src/Types/Types.php b/src/Types/Types.php index c116cb3..71fbc2d 100644 --- a/src/Types/Types.php +++ b/src/Types/Types.php @@ -41,8 +41,8 @@ public function determineType(mixed $value) is_float($value) => self::NUMBER, is_array($value) => $this->processArray($value), $value instanceof \BackedEnum => $this->processEnum($value), - $value instanceof ResourceCollection => TypescriptType::determineName($value->collects).'[]', - $value instanceof JsonResource => TypescriptType::determineName(get_class($value)), + $value instanceof ResourceCollection => TypescriptType::determineNamespace($value->collects).'.'.TypescriptType::determineName($value->collects).'[]', + $value instanceof JsonResource => TypescriptType::determineNamespace(get_class($value)).'.'.TypescriptType::determineName(get_class($value)), $value instanceof Arrayable => $this->processArray($value->toArray()), // TODO: Test for this is_object($value) => $this->processArray((array) $value), default => self::UNKNOWN, diff --git a/src/Values/TypescriptType.php b/src/Values/TypescriptType.php index a2efa3d..07956ee 100644 --- a/src/Values/TypescriptType.php +++ b/src/Values/TypescriptType.php @@ -60,9 +60,17 @@ public function merge(TypescriptType $type, Collection $originalProperties): sel return $this; } - public static function determineName(string $class): string + public static function determineNamespace(string $className): string { - $class = new ReflectionClass($class); + $namespace = explode('\\', $className); + array_pop($namespace); + + return implode('.', $namespace); + } + + public static function determineName(string $className): string + { + $class = new ReflectionClass($className); return $class->getShortName().'Type'; } diff --git a/tests/Actions/TranspileToTypescriptTest.php b/tests/Actions/TranspileToTypescriptTest.php index 4769330..f2b9c0d 100644 --- a/tests/Actions/TranspileToTypescriptTest.php +++ b/tests/Actions/TranspileToTypescriptTest.php @@ -21,11 +21,11 @@ $generated = (new TranspileToTypescript)->handle(collect([$type])); $lines = explode(PHP_EOL, $generated); - expect($lines)->toContain('export type ProductResourceType = {'); - expect($lines)->toContain('}'); + expect($lines)->toContain("\texport type ProductResourceType = {"); + expect($lines)->toContain("\t}"); $type->listProperties()->each(function (TypescriptProperty $property) use ($lines) { - expect($lines)->toContain("{$property->getName()}:{$property->getType()}"); + expect($lines)->toContain("\t\t{$property->getName()}: {$property->getType()};"); }); }); @@ -35,5 +35,5 @@ $generated = (new TranspileToTypescript)->handle(collect([$type])); $lines = explode(PHP_EOL, $generated); - expect($lines)->toContain('export type ProductResourceType = any'); + expect($lines)->toContain("\texport type ProductResourceType = any"); }); diff --git a/tests/Converters/ResourceConverterTest.php b/tests/Converters/ResourceConverterTest.php index fdc748c..2f8cf05 100644 --- a/tests/Converters/ResourceConverterTest.php +++ b/tests/Converters/ResourceConverterTest.php @@ -38,7 +38,7 @@ $property = $processed->listProperties()->first(); expect($property->getName())->toBe('data'); - expect($property->getType())->toBe('ProductResourceType[]'); + expect($property->getType())->toBe('Vagebond.Runtype.Tests.Fakes.Resources.ProductResourceType[]'); }); it('can convert resources that use the user bound to the request', function () { diff --git a/tests/Types/TypesTest.php b/tests/Types/TypesTest.php index 11e614a..6dd734b 100644 --- a/tests/Types/TypesTest.php +++ b/tests/Types/TypesTest.php @@ -14,7 +14,7 @@ [-1, 'number'], [1.1, 'number'], [now(), 'string'], - [new ProductResource(new Product), 'ProductResourceType'], + [new ProductResource(new Product), 'Vagebond.Runtype.Tests.Fakes.Resources.ProductResourceType'], [[1, 2, 3], 'number[]'], [['name' => 'name', 'value' => 1], '{name:string,value:number}'], [['name' => 'name', 'values' => ['name' => 'value']], '{name:string,values:{name:string}}'],