From ee44437170bbbe8ffdb0e32fe029ba48809447f9 Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Fri, 6 Jun 2025 04:26:01 +0700 Subject: [PATCH 1/9] Draft compactor integration --- src/Collectors/Collector.php | 4 ++ src/Collectors/DefaultCollector.php | 1 + src/Compactors/Compactor.php | 11 ++++ src/Compactors/ConfigCompactor.php | 73 +++++++++++++++++++++ src/Compactors/IdentityCompactor.php | 12 ++++ src/Structures/TransformedType.php | 15 ++++- src/Transformers/DtoTransformer.php | 6 +- src/Transformers/EnumTransformer.php | 8 ++- src/Transformers/MyclabsEnumTransformer.php | 8 ++- src/Transformers/SpatieEnumTransformer.php | 8 ++- src/TypeScriptTransformerConfig.php | 50 +++++++++++++- src/Writers/TypeDefinitionWriter.php | 10 +++ tests/Fakes/FakeTransformedType.php | 7 +- tests/Fakes/FakeTypeScriptCollector.php | 1 + tests/Transformers/DtoTransformerTest.php | 58 ++++++++++++++++ tests/TypeScriptTransformerConfigTest.php | 6 ++ 16 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 src/Compactors/Compactor.php create mode 100644 src/Compactors/ConfigCompactor.php create mode 100644 src/Compactors/IdentityCompactor.php diff --git a/src/Collectors/Collector.php b/src/Collectors/Collector.php index bde78941..8ec5a4d0 100644 --- a/src/Collectors/Collector.php +++ b/src/Collectors/Collector.php @@ -3,6 +3,7 @@ namespace Spatie\TypeScriptTransformer\Collectors; use ReflectionClass; +use Spatie\TypeScriptTransformer\Compactors\ConfigCompactor; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -10,9 +11,12 @@ abstract class Collector { protected TypeScriptTransformerConfig $config; + protected ConfigCompactor $compactor; + public function __construct(TypeScriptTransformerConfig $config) { $this->config = $config; + $this->compactor = new ConfigCompactor($config); } abstract public function getTransformedType(ReflectionClass $class): ?TransformedType; diff --git a/src/Collectors/DefaultCollector.php b/src/Collectors/DefaultCollector.php index 5fcb2a11..311ab5e9 100644 --- a/src/Collectors/DefaultCollector.php +++ b/src/Collectors/DefaultCollector.php @@ -49,6 +49,7 @@ protected function resolveAlreadyTransformedType(ClassTypeReflector $reflector): $reflector->getReflectionClass(), $reflector->getName(), $transpiler->execute($reflector->getType()), + $this->compactor, $missingSymbols ); } diff --git a/src/Compactors/Compactor.php b/src/Compactors/Compactor.php new file mode 100644 index 00000000..f149afa0 --- /dev/null +++ b/src/Compactors/Compactor.php @@ -0,0 +1,11 @@ +config = $config; + } + + /** + * @return string[] + */ + protected function getPrefixes(): array { + if ($this->prefixes === null) { + $this->prefixes = array_map( + function(string $prefix): string { + $prefix = str_replace("\\", ".", $prefix); + if (!str_ends_with($prefix, ".")) { + $prefix .= "."; + } + return $prefix; + }, + $this->config->getCompactorPrefixes() + ); + } + return $this->prefixes; + } + + /** + * @return string[] + */ + protected function getSuffixes(): array { + if ($this->suffixes === null) { + $this->suffixes = $this->config->getCompactorSuffixes(); + } + return $this->suffixes; + } + + public function compact( + string $typescriptIdentifier + ): string { + $matchingPrefix = ''; + $matchingSuffix = ''; + foreach ($this->getPrefixes() as $prefix) { + if (str_starts_with($typescriptIdentifier, $prefix)) { + $matchingPrefix = $prefix; + break; + } + } + foreach ($this->getSuffixes() as $suffix) { + if (str_ends_with($typescriptIdentifier, $suffix)) { + $matchingSuffix = $suffix; + break; + } + } + if ($matchingSuffix !== '') { + $typescriptIdentifier = substr($typescriptIdentifier, 0, -strlen($matchingSuffix)); + } + $substr = substr($typescriptIdentifier, strlen($matchingPrefix)); + return $substr; + } + +} \ No newline at end of file diff --git a/src/Compactors/IdentityCompactor.php b/src/Compactors/IdentityCompactor.php new file mode 100644 index 00000000..e6d136f5 --- /dev/null +++ b/src/Compactors/IdentityCompactor.php @@ -0,0 +1,12 @@ +compact($name), $transformed, $compactor, $missingSymbols ?? new MissingSymbolsCollection(), $inline, $keyword, $trailingSemicolon); } public static function createInline( ReflectionClass $class, string $transformed, + Compactor $compactor, ?MissingSymbolsCollection $missingSymbols = null ): self { - return new self($class, null, $transformed, $missingSymbols ?? new MissingSymbolsCollection(), true); + return new self($class, null, $transformed, $compactor, $missingSymbols ?? new MissingSymbolsCollection(), true); } public function __construct( ReflectionClass $class, ?string $name, string $transformed, + Compactor $compactor, MissingSymbolsCollection $missingSymbols, bool $isInline, string $keyword = 'type', @@ -53,6 +59,7 @@ public function __construct( $this->name = $name; $this->transformed = $transformed; $this->missingSymbols = $missingSymbols; + $this->compactor = $compactor; $this->isInline = $isInline; $this->keyword = $keyword; $this->trailingSemicolon = $trailingSemicolon; @@ -84,7 +91,9 @@ public function getTypeScriptName($fullyQualified = true): string [$this->name] ); - return implode('.', $segments); + return $this->compactor->compact( + implode('.', $segments) + ); } public function replaceSymbol(string $class, string $replacement): void diff --git a/src/Transformers/DtoTransformer.php b/src/Transformers/DtoTransformer.php index a39fbdee..b4ba00bd 100644 --- a/src/Transformers/DtoTransformer.php +++ b/src/Transformers/DtoTransformer.php @@ -6,6 +6,7 @@ use ReflectionProperty; use Spatie\TypeScriptTransformer\Attributes\Hidden; use Spatie\TypeScriptTransformer\Attributes\Optional; +use Spatie\TypeScriptTransformer\Compactors\ConfigCompactor; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\TypeProcessors\DtoCollectionTypeProcessor; @@ -18,9 +19,12 @@ class DtoTransformer implements Transformer protected TypeScriptTransformerConfig $config; + protected ConfigCompactor $compactor; + public function __construct(TypeScriptTransformerConfig $config) { $this->config = $config; + $this->compactor = new ConfigCompactor($config); } public function transform(ReflectionClass $class, string $name): ?TransformedType @@ -30,7 +34,6 @@ public function transform(ReflectionClass $class, string $name): ?TransformedTyp } $missingSymbols = new MissingSymbolsCollection(); - $type = join([ $this->transformProperties($class, $missingSymbols), $this->transformMethods($class, $missingSymbols), @@ -41,6 +44,7 @@ public function transform(ReflectionClass $class, string $name): ?TransformedTyp $class, $name, "{" . PHP_EOL . $type . "}", + $this->compactor, $missingSymbols ); } diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index d9fdc351..5e3e3cf2 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -5,13 +5,17 @@ use ReflectionClass; use ReflectionEnum; use ReflectionEnumBackedCase; +use Spatie\TypeScriptTransformer\Compactors\ConfigCompactor; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class EnumTransformer implements Transformer { + protected ConfigCompactor $compactor; + public function __construct(protected TypeScriptTransformerConfig $config) { + $this->compactor = new ConfigCompactor($config); } public function transform(ReflectionClass $class, string $name): ?TransformedType @@ -46,6 +50,7 @@ protected function toEnum(ReflectionEnum $enum, string $name): TransformedType $enum, $name, implode(', ', $options), + $this->compactor, keyword: 'enum' ); } @@ -60,7 +65,8 @@ protected function toType(ReflectionEnum $enum, string $name): TransformedType return TransformedType::create( $enum, $name, - implode(' | ', $options) + implode(' | ', $options), + $this->compactor ); } diff --git a/src/Transformers/MyclabsEnumTransformer.php b/src/Transformers/MyclabsEnumTransformer.php index cab53b62..2fa6d8be 100644 --- a/src/Transformers/MyclabsEnumTransformer.php +++ b/src/Transformers/MyclabsEnumTransformer.php @@ -4,13 +4,17 @@ use MyCLabs\Enum\Enum; use ReflectionClass; +use Spatie\TypeScriptTransformer\Compactors\ConfigCompactor; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class MyclabsEnumTransformer implements Transformer { + protected ConfigCompactor $compactor; + public function __construct(protected TypeScriptTransformerConfig $config) { + $this->compactor = new ConfigCompactor($config); } public function transform(ReflectionClass $class, string $name): ?TransformedType @@ -39,6 +43,7 @@ protected function toEnum(ReflectionClass $class, string $name): TransformedType $class, $name, implode(', ', $options), + $this->compactor, keyword: 'enum' ); } @@ -56,7 +61,8 @@ protected function toType(ReflectionClass $class, string $name): TransformedType return TransformedType::create( $class, $name, - implode(' | ', $options) + implode(' | ', $options), + $this->compactor ); } } diff --git a/src/Transformers/SpatieEnumTransformer.php b/src/Transformers/SpatieEnumTransformer.php index 5824c67b..3042ed93 100644 --- a/src/Transformers/SpatieEnumTransformer.php +++ b/src/Transformers/SpatieEnumTransformer.php @@ -4,13 +4,17 @@ use ReflectionClass; use Spatie\Enum\Enum; +use Spatie\TypeScriptTransformer\Compactors\ConfigCompactor; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class SpatieEnumTransformer implements Transformer { + protected ConfigCompactor $compactor; + public function __construct(protected TypeScriptTransformerConfig $config) { + $this->compactor = new ConfigCompactor($config); } public function transform(ReflectionClass $class, string $name): ?TransformedType @@ -39,6 +43,7 @@ protected function toEnum(ReflectionClass $class, string $name): TransformedType $class, $name, implode(', ', $options), + $this->compactor, keyword: 'enum' ); } @@ -56,7 +61,8 @@ private function toType(ReflectionClass $class, string $name): TransformedType return TransformedType::create( $class, $name, - implode(' | ', $options) + implode(' | ', $options), + $this->compactor ); } } diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 68ec6002..3f4ba099 100644 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -27,6 +27,10 @@ class TypeScriptTransformerConfig private ?string $formatter = null; + private array $compactorPrefixes = []; + + private array $compactorSuffixes = []; + private bool $transformToNativeEnums = false; private bool $nullToOptional = false; @@ -85,6 +89,34 @@ public function formatter(?string $formatter): self return $this; } + /** + * @param string[]|string $prefixes + * @return $this + */ + public function compactorPrefixes(array|string $prefixes): self + { + if (!is_array($prefixes)) { + $prefixes = [$prefixes]; + } + $this->compactorPrefixes = $prefixes; + + return $this; + } + + /** + * @param string[]|string $suffixes + * @return $this + */ + public function compactorSuffixes(array|string $suffixes): self + { + if (!is_array($suffixes)) { + $suffixes = [$suffixes]; + } + $this->compactorSuffixes = $suffixes; + + return $this; + } + public function transformToNativeEnums(bool $transformToNativeEnums = true): self { $this->transformToNativeEnums = $transformToNativeEnums; @@ -122,7 +154,7 @@ public function buildTransformer(string $transformer): Transformer public function getWriter(): Writer { - return new $this->writer; + return new $this->writer($this); } public function getOutputFile(): string @@ -167,6 +199,22 @@ public function getFormatter(): ?Formatter return new $this->formatter; } + /** + * @return string[] + */ + public function getCompactorPrefixes(): array + { + return $this->compactorPrefixes ?? []; + } + + /** + * @return string[] + */ + public function getCompactorSuffixes(): array + { + return $this->compactorSuffixes ?? []; + } + public function shouldTransformToNativeEnums(): bool { return $this->transformToNativeEnums; diff --git a/src/Writers/TypeDefinitionWriter.php b/src/Writers/TypeDefinitionWriter.php index 1a029894..2f511749 100644 --- a/src/Writers/TypeDefinitionWriter.php +++ b/src/Writers/TypeDefinitionWriter.php @@ -3,11 +3,20 @@ namespace Spatie\TypeScriptTransformer\Writers; use Spatie\TypeScriptTransformer\Actions\ReplaceSymbolsInCollectionAction; +use Spatie\TypeScriptTransformer\Compactors\Compactor; +use Spatie\TypeScriptTransformer\Compactors\ConfigCompactor; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\Structures\TypesCollection; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class TypeDefinitionWriter implements Writer { + protected Compactor $compactor; + + public function __construct(TypeScriptTransformerConfig $config) { + $this->compactor = new ConfigCompactor($config); + } + public function format(TypesCollection $collection): string { (new ReplaceSymbolsInCollectionAction())->execute($collection); @@ -18,6 +27,7 @@ public function format(TypesCollection $collection): string foreach ($namespaces as $namespace => $types) { asort($types); + $namespace = $this->compactor->compact($namespace); $output .= "declare namespace {$namespace} {".PHP_EOL; diff --git a/tests/Fakes/FakeTransformedType.php b/tests/Fakes/FakeTransformedType.php index 80fc4f41..71ab2b40 100644 --- a/tests/Fakes/FakeTransformedType.php +++ b/tests/Fakes/FakeTransformedType.php @@ -4,17 +4,19 @@ use Exception; use ReflectionClass; +use Spatie\TypeScriptTransformer\Compactors\Compactor; +use Spatie\TypeScriptTransformer\Compactors\IdentityCompactor; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\Structures\TransformedType; class FakeTransformedType extends TransformedType { - public static function create(ReflectionClass $class, string $name, string $transformed, ?MissingSymbolsCollection $missingSymbols = null, bool $inline = false, string $keyword = 'type', bool $trailingSemicolon = true): TransformedType + public static function create(ReflectionClass $class, string $name, string $transformed, Compactor $compactor, ?MissingSymbolsCollection $missingSymbols = null, bool $inline = false, string $keyword = 'type', bool $trailingSemicolon = true): TransformedType { throw new Exception("Fake type"); } - public static function createInline(ReflectionClass $class, string $transformed, ?MissingSymbolsCollection $missingSymbols = null): TransformedType + public static function createInline(ReflectionClass $class, string $transformed, Compactor $compactor, ?MissingSymbolsCollection $missingSymbols = null): TransformedType { throw new Exception("Fake type"); } @@ -27,6 +29,7 @@ public static function fake(?string $name = null): self FakeReflectionClass::create()->withName($name), $name, 'fake-transformed', + new IdentityCompactor(), new MissingSymbolsCollection(), false ); diff --git a/tests/Fakes/FakeTypeScriptCollector.php b/tests/Fakes/FakeTypeScriptCollector.php index 1e646870..b345ff99 100644 --- a/tests/Fakes/FakeTypeScriptCollector.php +++ b/tests/Fakes/FakeTypeScriptCollector.php @@ -21,6 +21,7 @@ public function getTransformedType(ReflectionClass $class): TransformedType $class, $class->getShortName(), 'fake-collected-class', + $this->compactor, new MissingSymbolsCollection(), false ); diff --git a/tests/Transformers/DtoTransformerTest.php b/tests/Transformers/DtoTransformerTest.php index 5ba756b6..e3231e64 100644 --- a/tests/Transformers/DtoTransformerTest.php +++ b/tests/Transformers/DtoTransformerTest.php @@ -161,3 +161,61 @@ class DummyOptionalDto $this->assertMatchesSnapshot($type->transformed); }); + +it('compacts namespaces', function () { + $reflectionClass = new ReflectionClass(Dto::class); + assertEquals( + 'Dto', + (new DtoTransformer( + TypeScriptTransformerConfig::create() + ->compactorPrefixes([ + "Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration" + ]) + ))->transform( + $reflectionClass, + 'Dto' + )->getTypeScriptName(true) + ); + + assertEquals( + 'Integration.Dto', + (new DtoTransformer( + TypeScriptTransformerConfig::create() + ->compactorPrefixes([ + "Spatie.TypeScriptTransformer.Tests.FakeClasses" + ]) + ))->transform( + $reflectionClass, + 'Dto' + )->getTypeScriptName(true) + ); + assertEquals( + 'Integration.Dto', + (new DtoTransformer( + TypeScriptTransformerConfig::create() + ->compactorPrefixes([ + "Spatie.TypeScriptTransformer.Tests.RealClasses", + "Spatie.TypeScriptTransformer.Tests.FakeClasses" + ]) + ))->transform( + $reflectionClass, + 'Dto' + )->getTypeScriptName(true) + ); +}); +it('compacts type names', function () { + $reflectionClass = new ReflectionClass(DtoWithChildren::class); + assertEquals( + 'Dto', + (new DtoTransformer( + TypeScriptTransformerConfig::create() + ->compactorPrefixes("Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration") + ->compactorSuffixes([ + 'WithChildren' + ]) + ))->transform( + $reflectionClass, + 'DtoWithChildren' + )->getTypeScriptName(true) + ); +}); diff --git a/tests/TypeScriptTransformerConfigTest.php b/tests/TypeScriptTransformerConfigTest.php index 03a0cddd..16a9d7cc 100644 --- a/tests/TypeScriptTransformerConfigTest.php +++ b/tests/TypeScriptTransformerConfigTest.php @@ -68,3 +68,9 @@ $config->getDefaultTypeReplacements() ); }); + +it('can handle string as compactor_prefixes parameter', function () { + $config = TypeScriptTransformerConfig::create()->compactorPrefixes('asdf.asdf'); + + assertEquals(['asdf.asdf'], $config->getCompactorPrefixes()); +}); From e79921b556e24648875a68b64b884e669c2d5bae Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Sun, 8 Jun 2025 02:30:04 +0700 Subject: [PATCH 2/9] Implement splitting types per file per namespace --- src/Actions/FormatTypeScriptAction.php | 8 +- src/Actions/PersistTypesCollectionAction.php | 8 +- .../ResolveSplitTypesCollectionsAction.php | 93 +++++++++++ src/TypeScriptTransformer.php | 45 +++++- src/TypeScriptTransformerConfig.php | 14 ++ tests/Actions/FormatTypeScriptActionTest.php | 14 +- .../PersistTypesCollectionActionTest.php | 8 +- ...ResolveSplitTypesCollectionsActionTest.php | 146 ++++++++++++++++++ 8 files changed, 315 insertions(+), 21 deletions(-) create mode 100644 src/Actions/ResolveSplitTypesCollectionsAction.php create mode 100644 tests/Actions/ResolveSplitTypesCollectionsActionTest.php diff --git a/src/Actions/FormatTypeScriptAction.php b/src/Actions/FormatTypeScriptAction.php index 17e977f7..95f87d6c 100644 --- a/src/Actions/FormatTypeScriptAction.php +++ b/src/Actions/FormatTypeScriptAction.php @@ -8,8 +8,10 @@ class FormatTypeScriptAction { protected TypeScriptTransformerConfig $config; - public function __construct(TypeScriptTransformerConfig $config) - { + public function __construct( + TypeScriptTransformerConfig $config, + protected string $outputFile + ) { $this->config = $config; } @@ -21,6 +23,6 @@ public function execute(): void return; } - $formatter->format($this->config->getOutputFile()); + $formatter->format($this->outputFile); } } diff --git a/src/Actions/PersistTypesCollectionAction.php b/src/Actions/PersistTypesCollectionAction.php index 271638d5..bf362e8a 100644 --- a/src/Actions/PersistTypesCollectionAction.php +++ b/src/Actions/PersistTypesCollectionAction.php @@ -9,7 +9,7 @@ class PersistTypesCollectionAction { protected TypeScriptTransformerConfig $config; - public function __construct(TypeScriptTransformerConfig $config) + public function __construct(TypeScriptTransformerConfig $config, protected string $outputFile) { $this->config = $config; } @@ -26,15 +26,15 @@ public function execute(TypesCollection $collection): void ); file_put_contents( - $this->config->getOutputFile(), + $this->outputFile, $writer->format($collection) ); } protected function ensureOutputFileExists(): void { - if (! file_exists(pathinfo($this->config->getOutputFile(), PATHINFO_DIRNAME))) { - mkdir(pathinfo($this->config->getOutputFile(), PATHINFO_DIRNAME), 0755, true); + if (! file_exists(pathinfo($this->outputFile, PATHINFO_DIRNAME))) { + mkdir(pathinfo($this->outputFile, PATHINFO_DIRNAME), 0755, true); } } } diff --git a/src/Actions/ResolveSplitTypesCollectionsAction.php b/src/Actions/ResolveSplitTypesCollectionsAction.php new file mode 100644 index 00000000..92e27f63 --- /dev/null +++ b/src/Actions/ResolveSplitTypesCollectionsAction.php @@ -0,0 +1,93 @@ +finder = $finder; + + $this->config = $config; + + $this->collectors = $config->getCollectors(); + } + + /** + * @return TypesCollection[] + * @throws NoAutoDiscoverTypesPathsDefined + */ + public function execute(): array + { + $collections = []; + + $paths = $this->config->getAutoDiscoverTypesPaths(); + + if (empty($paths)) { + throw NoAutoDiscoverTypesPathsDefined::create(); + } + + foreach ($this->resolveIterator($paths) as $class) { + $transformedType = $this->resolveTransformedType($class); + + if ($transformedType === null) { + continue; + } + $namespace = implode('/', $transformedType->getNamespaceSegments()); + if (!isset($collections[$namespace])) { + $collections[$namespace] = new TypesCollection(); + } + $collections[$namespace][] = $transformedType; + } + + return $collections; + } + + protected function resolveIterator(array $paths): Generator + { + $paths = array_map( + fn (string $path) => is_dir($path) ? $path : dirname($path), + $paths + ); + + foreach ($this->finder->in($paths) as $fileInfo) { + try { + $classes = (new ResolveClassesInPhpFileAction())->execute($fileInfo); + + foreach ($classes as $name) { + yield $name => new ReflectionClass($name); + } + } catch (Exception $exception) { + } + } + } + + protected function resolveTransformedType(ReflectionClass $class): ?TransformedType + { + foreach ($this->collectors as $collector) { + $transformedType = $collector->getTransformedType($class); + + if ($transformedType !== null) { + return $transformedType; + } + } + + return null; + } +} diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index d7c23eac..5b600802 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -2,8 +2,10 @@ namespace Spatie\TypeScriptTransformer; +use Spatie\TemporaryDirectory\TemporaryDirectory; use Spatie\TypeScriptTransformer\Actions\FormatTypeScriptAction; use Spatie\TypeScriptTransformer\Actions\PersistTypesCollectionAction; +use Spatie\TypeScriptTransformer\Actions\ResolveSplitTypesCollectionsAction; use Spatie\TypeScriptTransformer\Actions\ResolveTypesCollectionAction; use Spatie\TypeScriptTransformer\Structures\TypesCollection; use Symfony\Component\Finder\Finder; @@ -24,15 +26,44 @@ public function __construct(TypeScriptTransformerConfig $config) public function transform(): TypesCollection { - $typesCollection = (new ResolveTypesCollectionAction( - new Finder(), - $this->config, - ))->execute(); + if (($baseDir = $this->config->getSplitModulesBaseDir()) !== null) { + (new TemporaryDirectory($baseDir))->delete(); - (new PersistTypesCollectionAction($this->config))->execute($typesCollection); + $sumCollection = new TypesCollection(); - (new FormatTypeScriptAction($this->config))->execute(); + $typesCollections = (new ResolveSplitTypesCollectionsAction( + new Finder(), + $this->config, + ))->execute(); - return $typesCollection; + foreach ($typesCollections as $namespace => $typesCollection) { + $outputFile = rtrim($baseDir, '/') . '/' . $namespace . '.ts'; + + (new PersistTypesCollectionAction( + $this->config, + $outputFile, + ))->execute($typesCollection); + + (new FormatTypeScriptAction($this->config, $outputFile))->execute(); + + foreach ($typesCollection as $type) { + $sumCollection[] = $type; + } + } + + return $sumCollection; + } else { + $typesCollection = (new ResolveTypesCollectionAction( + new Finder(), + $this->config, + ))->execute(); + + (new PersistTypesCollectionAction($this->config, $this->config->getOutputFile()))->execute($typesCollection); + + (new FormatTypeScriptAction($this->config, $this->config->getOutputFile()))->execute(); + + return $typesCollection; + } } + } diff --git a/src/TypeScriptTransformerConfig.php b/src/TypeScriptTransformerConfig.php index 68ec6002..4276bd40 100644 --- a/src/TypeScriptTransformerConfig.php +++ b/src/TypeScriptTransformerConfig.php @@ -31,6 +31,8 @@ class TypeScriptTransformerConfig private bool $nullToOptional = false; + private string|null $splitModulesBaseDir = null; + public static function create(): self { return new self(); @@ -99,6 +101,13 @@ public function nullToOptional(bool $nullToOptional = false): self return $this; } + public function splitModulesBaseDir(string|null $splitModules = null): self + { + $this->splitModulesBaseDir = $splitModules; + + return $this; + } + public function getAutoDiscoverTypesPaths(): array { return $this->autoDiscoverTypesPaths; @@ -176,4 +185,9 @@ public function shouldConsiderNullAsOptional(): bool { return $this->nullToOptional; } + + public function getSplitModulesBaseDir(): string|null + { + return $this->splitModulesBaseDir; + } } diff --git a/tests/Actions/FormatTypeScriptActionTest.php b/tests/Actions/FormatTypeScriptActionTest.php index b5d82768..e7d724ec 100644 --- a/tests/Actions/FormatTypeScriptActionTest.php +++ b/tests/Actions/FormatTypeScriptActionTest.php @@ -21,10 +21,13 @@ public function format(string $file): void } }; - $action = new FormatTypeScriptAction( - TypeScriptTransformerConfig::create() + $config = TypeScriptTransformerConfig::create() ->formatter($formatter::class) - ->outputFile($this->outputFile) + ->outputFile($this->outputFile); + + $action = new FormatTypeScriptAction( + $config, + $config->getOutputFile() ); file_put_contents( @@ -38,8 +41,11 @@ public function format(string $file): void }); it('can disable formatting', function () { + $config = TypeScriptTransformerConfig::create()->outputFile($this->outputFile); + $action = new FormatTypeScriptAction( - TypeScriptTransformerConfig::create()->outputFile($this->outputFile) + $config, + $config->getOutputFile() ); file_put_contents( diff --git a/tests/Actions/PersistTypesCollectionActionTest.php b/tests/Actions/PersistTypesCollectionActionTest.php index dacaf4f5..0dbddecf 100644 --- a/tests/Actions/PersistTypesCollectionActionTest.php +++ b/tests/Actions/PersistTypesCollectionActionTest.php @@ -11,11 +11,13 @@ beforeEach(function () { $this->temporaryDirectory = (new TemporaryDirectory())->create(); - $this->action = new PersistTypesCollectionAction( - TypeScriptTransformerConfig::create() + $config = TypeScriptTransformerConfig::create() ->autoDiscoverTypes(__DIR__ . '/../FakeClasses') ->transformers([MyclabsEnumTransformer::class]) - ->outputFile($this->temporaryDirectory->path('types.d.ts')) + ->outputFile($this->temporaryDirectory->path('types.d.ts')); + $this->action = new PersistTypesCollectionAction( + $config, + $config->getOutputFile() ); }); diff --git a/tests/Actions/ResolveSplitTypesCollectionsActionTest.php b/tests/Actions/ResolveSplitTypesCollectionsActionTest.php new file mode 100644 index 00000000..a652e33a --- /dev/null +++ b/tests/Actions/ResolveSplitTypesCollectionsActionTest.php @@ -0,0 +1,146 @@ +action = new ResolveSplitTypesCollectionsAction( + new Finder(), + TypeScriptTransformerConfig::create() + ->autoDiscoverTypes(__DIR__ . '/../FakeClasses/Enum') + ->transformers([MyclabsEnumTransformer::class]) + ->collectors([DefaultCollector::class]) + ->splitModulesBaseDir('data') + ); +}); + +it('will construct the type collections correctly', function () { + $typesCollections = $this->action->execute(); + + assertCount(1, $typesCollections); +}); + +it('will check if auto discover types paths are defined', function () { + $this->expectException(NoAutoDiscoverTypesPathsDefined::class); + + $action = new ResolveSplitTypesCollectionsAction( + new Finder(), + TypeScriptTransformerConfig::create() + ); + + $action->execute(); +}); + +it('parses a typescript enum correctly', function () { + $collections = $this->action->execute(); + $type = $collections[array_key_first($collections)][TypeScriptEnum::class]; + + assertEquals(new ReflectionClass(new TypeScriptEnum('js')), $type->reflection); + assertEquals('TypeScriptEnum', $type->name); + assertEquals("'js'", $type->transformed); + assertTrue($type->missingSymbols->isEmpty()); +}); + +it('parses a typescript enum with name correctly', function () { + $collections = $this->action->execute(); + $type = $collections[array_key_first($collections)][TypeScriptEnumWithName::class]; + + assertCount(1, $collections); + assertEquals(new ReflectionClass(new TypeScriptEnumWithName('js')), $type->reflection); + assertEquals('EnumWithName', $type->name); + assertEquals("'js'", $type->transformed); + assertTrue($type->missingSymbols->isEmpty()); +}); + +it('parses a typescript enum with custom transformer correctly', function () { + $collections = $this->action->execute(); + $type = $collections[array_key_first($collections)][TypeScriptEnumWithCustomTransformer::class]; + + assertEquals(new ReflectionClass(new TypeScriptEnumWithCustomTransformer('js')), $type->reflection); + assertEquals('TypeScriptEnumWithCustomTransformer', $type->name); + assertEquals("fake", $type->transformed); + assertTrue($type->missingSymbols->isEmpty()); +}); + +it('can parse multiple directories', function () { + $this->action = new ResolveSplitTypesCollectionsAction( + new Finder(), + TypeScriptTransformerConfig::create() + ->autoDiscoverTypes( + __DIR__ . '/../FakeClasses/Enum/', + __DIR__ . '/../FakeClasses/Integration/' + ) + ->transformers([MyclabsEnumTransformer::class, DtoTransformer::class]) + ->collectors([DefaultCollector::class]) + ->outputFile('types.d.ts') + ); + + $collections = $this->action->execute(); + + $types = new TypesCollection(); + foreach ($collections as $collection) { + foreach ($collection as $type) { + $types[] = $type; + } + } + + assertCount(3, $collections); + assertArrayHasKey("Spatie/TypeScriptTransformer/Tests/FakeClasses/Enum", $collections); + assertArrayHasKey("Spatie/TypeScriptTransformer/Tests/FakeClasses/Integration", $collections); + assertArrayHasKey("Spatie/TypeScriptTransformer/Tests/FakeClasses/Integration/LevelUp", $collections); + assertCount(9, $types); + + assertArrayHasKey(TypeScriptEnum::class, $types); + assertArrayHasKey(TypeScriptEnumWithCustomTransformer::class, $types); + assertArrayHasKey(TypeScriptEnumWithName::class, $types); + + assertArrayHasKey(Dto::class, $types); + assertArrayHasKey(DtoWithChildren::class, $types); + assertArrayHasKey(Enum::class, $types); + assertArrayHasKey(OtherDto::class, $types); + assertArrayHasKey(OtherDtoCollection::class, $types); + assertArrayHasKey(YetAnotherDto::class, $types); +}); + +it('can add a collector for types', function () { + $this->action = new ResolveSplitTypesCollectionsAction( + new Finder(), + TypeScriptTransformerConfig::create() + ->autoDiscoverTypes(__DIR__ . '/../FakeClasses/Enum') + ->collectors([FakeTypeScriptCollector::class]) + ->outputFile('types.d.ts') + ); + + $collections = $this->action->execute(); + $types = $collections[array_key_first($collections)]; + + assertCount(1, $collections); + assertCount(4, $types); + assertArrayHasKey(RegularEnum::class, $types); + assertArrayHasKey(TypeScriptEnum::class, $types); + assertArrayHasKey(TypeScriptEnumWithCustomTransformer::class, $types); + assertArrayHasKey(TypeScriptEnumWithName::class, $types); +}); From 9771dca302c5da2e7f5093729ad781ec77437ce4 Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Sun, 8 Jun 2025 03:07:07 +0700 Subject: [PATCH 3/9] Compact namespace at TransformedType->getNamespaceSegments() instead of TransformedType->getTypescriptName() --- src/Compactors/Compactor.php | 4 ++- src/Compactors/ConfigCompactor.php | 37 ++++++++++++----------- src/Compactors/IdentityCompactor.php | 7 +++-- src/Structures/TransformedType.php | 12 +++++--- src/Writers/TypeDefinitionWriter.php | 2 +- tests/Transformers/DtoTransformerTest.php | 10 +++--- 6 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/Compactors/Compactor.php b/src/Compactors/Compactor.php index f149afa0..9a6c1459 100644 --- a/src/Compactors/Compactor.php +++ b/src/Compactors/Compactor.php @@ -7,5 +7,7 @@ */ interface Compactor { - public function compact(string $typescriptIdentifier): string; + public function removeSuffix(string $typeName): string; + + public function removePrefix(string $namespace): string; } \ No newline at end of file diff --git a/src/Compactors/ConfigCompactor.php b/src/Compactors/ConfigCompactor.php index 2198581a..13e0c425 100644 --- a/src/Compactors/ConfigCompactor.php +++ b/src/Compactors/ConfigCompactor.php @@ -23,10 +23,9 @@ public function __construct(TypeScriptTransformerConfig $config) { protected function getPrefixes(): array { if ($this->prefixes === null) { $this->prefixes = array_map( - function(string $prefix): string { - $prefix = str_replace("\\", ".", $prefix); - if (!str_ends_with($prefix, ".")) { - $prefix .= "."; + function (string $prefix): string { + if (str_ends_with($prefix, "\\")) { + $prefix = rtrim($prefix, "\\"); } return $prefix; }, @@ -46,28 +45,30 @@ protected function getSuffixes(): array { return $this->suffixes; } - public function compact( - string $typescriptIdentifier - ): string { - $matchingPrefix = ''; + public function removeSuffix(string $typeName): string { $matchingSuffix = ''; - foreach ($this->getPrefixes() as $prefix) { - if (str_starts_with($typescriptIdentifier, $prefix)) { - $matchingPrefix = $prefix; - break; - } - } foreach ($this->getSuffixes() as $suffix) { - if (str_ends_with($typescriptIdentifier, $suffix)) { + if (str_ends_with($typeName, $suffix)) { $matchingSuffix = $suffix; break; } } if ($matchingSuffix !== '') { - $typescriptIdentifier = substr($typescriptIdentifier, 0, -strlen($matchingSuffix)); + $typeName = substr($typeName, 0, -strlen($matchingSuffix)); + } + return $typeName; + } + + public function removePrefix(string $namespace): string { + $matchingPrefix = ''; + foreach ($this->getPrefixes() as $prefix) { + if (str_starts_with($namespace, $prefix)) { + $matchingPrefix = $prefix; + break; + } } - $substr = substr($typescriptIdentifier, strlen($matchingPrefix)); - return $substr; + $substr = substr($namespace, strlen($matchingPrefix)); + return ltrim($substr, '\\'); } } \ No newline at end of file diff --git a/src/Compactors/IdentityCompactor.php b/src/Compactors/IdentityCompactor.php index e6d136f5..2d75f78e 100644 --- a/src/Compactors/IdentityCompactor.php +++ b/src/Compactors/IdentityCompactor.php @@ -5,8 +5,11 @@ class IdentityCompactor implements Compactor { - public function compact(string $typescriptIdentifier): string { - return $typescriptIdentifier; + public function removeSuffix(string $typeName): string { + return $typeName; } + public function removePrefix(string $namespace): string { + return $namespace; + } } \ No newline at end of file diff --git a/src/Structures/TransformedType.php b/src/Structures/TransformedType.php index 806d0d4f..ce7d6146 100644 --- a/src/Structures/TransformedType.php +++ b/src/Structures/TransformedType.php @@ -33,7 +33,7 @@ public static function create( string $keyword = 'type', bool $trailingSemicolon = true, ): self { - return new self($class, $compactor->compact($name), $transformed, $compactor, $missingSymbols ?? new MissingSymbolsCollection(), $inline, $keyword, $trailingSemicolon); + return new self($class, $compactor->removeSuffix($name), $transformed, $compactor, $missingSymbols ?? new MissingSymbolsCollection(), $inline, $keyword, $trailingSemicolon); } public static function createInline( @@ -77,6 +77,12 @@ public function getNamespaceSegments(): array return []; } + $namespace = $this->compactor->removePrefix($namespace); + + if ($namespace === '') { + return []; + } + return explode('\\', $namespace); } @@ -91,9 +97,7 @@ public function getTypeScriptName($fullyQualified = true): string [$this->name] ); - return $this->compactor->compact( - implode('.', $segments) - ); + return implode('.', $segments); } public function replaceSymbol(string $class, string $replacement): void diff --git a/src/Writers/TypeDefinitionWriter.php b/src/Writers/TypeDefinitionWriter.php index 2f511749..401aa9c0 100644 --- a/src/Writers/TypeDefinitionWriter.php +++ b/src/Writers/TypeDefinitionWriter.php @@ -27,7 +27,7 @@ public function format(TypesCollection $collection): string foreach ($namespaces as $namespace => $types) { asort($types); - $namespace = $this->compactor->compact($namespace); + $namespace = $this->compactor->removePrefix($namespace); $output .= "declare namespace {$namespace} {".PHP_EOL; diff --git a/tests/Transformers/DtoTransformerTest.php b/tests/Transformers/DtoTransformerTest.php index e3231e64..75f4c0f0 100644 --- a/tests/Transformers/DtoTransformerTest.php +++ b/tests/Transformers/DtoTransformerTest.php @@ -169,7 +169,7 @@ class DummyOptionalDto (new DtoTransformer( TypeScriptTransformerConfig::create() ->compactorPrefixes([ - "Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration" + "Spatie\\TypeScriptTransformer\\Tests\\FakeClasses\\Integration" ]) ))->transform( $reflectionClass, @@ -182,7 +182,7 @@ class DummyOptionalDto (new DtoTransformer( TypeScriptTransformerConfig::create() ->compactorPrefixes([ - "Spatie.TypeScriptTransformer.Tests.FakeClasses" + "Spatie\\TypeScriptTransformer\\Tests\\FakeClasses" ]) ))->transform( $reflectionClass, @@ -194,8 +194,8 @@ class DummyOptionalDto (new DtoTransformer( TypeScriptTransformerConfig::create() ->compactorPrefixes([ - "Spatie.TypeScriptTransformer.Tests.RealClasses", - "Spatie.TypeScriptTransformer.Tests.FakeClasses" + "Spatie\\TypeScriptTransformer\\Tests\\RealClasses", + "Spatie\\TypeScriptTransformer\\Tests\\FakeClasses" ]) ))->transform( $reflectionClass, @@ -209,7 +209,7 @@ class DummyOptionalDto 'Dto', (new DtoTransformer( TypeScriptTransformerConfig::create() - ->compactorPrefixes("Spatie.TypeScriptTransformer.Tests.FakeClasses.Integration") + ->compactorPrefixes("Spatie\\TypeScriptTransformer\\Tests\\FakeClasses\\Integration") ->compactorSuffixes([ 'WithChildren' ]) From 9ce55c3ade531368f19b818892acb0c0a7565f59 Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Tue, 10 Jun 2025 10:09:18 +0700 Subject: [PATCH 4/9] Implement importing modules with multi-module setup --- src/Actions/PersistTypesCollectionAction.php | 12 +- ...ceIntermoduleSymbolsInCollectionAction.php | 24 ++++ src/Actions/ReplaceSymbolsInTypeAction.php | 3 +- .../TranspileTypeToTypeScriptAction.php | 134 ++++++++++++------ src/Structures/NamespacedType.php | 50 +++++++ src/Structures/TransformedType.php | 12 +- src/Structures/TranspilationResult.php | 29 ++++ src/Transformers/DtoTransformer.php | 51 ++++--- src/Transformers/EnumTransformer.php | 15 +- src/Transformers/InterfaceTransformer.php | 22 +-- src/Transformers/MyclabsEnumTransformer.php | 9 +- src/Transformers/SpatieEnumTransformer.php | 9 +- src/Transformers/TransformsTypes.php | 5 +- src/TypeScriptTransformer.php | 16 ++- src/Writers/ModuleWriter.php | 56 +++++++- .../PersistTypesCollectionActionTest.php | 4 +- .../TranspileTypeToTypeScriptActionTest.php | 2 +- tests/Collectors/DefaultCollectorTest.php | 12 +- tests/Fakes/FakeTransformedType.php | 14 +- tests/Fakes/FakeTypeScriptCollector.php | 5 +- tests/Fakes/FakeTypeScriptTransformer.php | 7 +- tests/Structures/NamespacedTypeTest.php | 23 +++ ...with_optional_attribute_to_optional__1.yml | 6 + ...o_optional_ones_according_to_config__1.yml | 5 + ...en_ones_when_using_hidden_attribute__1.yml | 5 + ..._ones_when_using_optional_attribute__1.yml | 5 + ..._typescript_attributes_into_account__1.yml | 10 ++ ...est__it_can_transform_to_es_modules__1.txt | 2 + ...nTest__it_can_resolve_a_struct_type__1.yml | 3 + 29 files changed, 432 insertions(+), 118 deletions(-) create mode 100644 src/Actions/ReplaceIntermoduleSymbolsInCollectionAction.php create mode 100644 src/Structures/NamespacedType.php create mode 100644 src/Structures/TranspilationResult.php create mode 100644 tests/Structures/NamespacedTypeTest.php create mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_all_properties_of_a_class_with_optional_attribute_to_optional__1.yml create mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_nullable_properties_to_optional_ones_according_to_config__1.yml create mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_hidden_ones_when_using_hidden_attribute__1.yml create mode 100644 tests/__snapshots__/DtoTransformerTest__it_transforms_properties_to_optional_ones_when_using_optional_attribute__1.yml create mode 100644 tests/__snapshots__/DtoTransformerTest__it_will_take_transform_as_typescript_attributes_into_account__1.yml create mode 100644 tests/__snapshots__/TranspileTypeToTypeScriptActionTest__it_can_resolve_a_struct_type__1.yml diff --git a/src/Actions/PersistTypesCollectionAction.php b/src/Actions/PersistTypesCollectionAction.php index bf362e8a..71ccc97b 100644 --- a/src/Actions/PersistTypesCollectionAction.php +++ b/src/Actions/PersistTypesCollectionAction.php @@ -14,20 +14,24 @@ public function __construct(TypeScriptTransformerConfig $config, protected strin $this->config = $config; } - public function execute(TypesCollection $collection): void + public function execute(TypesCollection $moduleCollection, ?TypesCollection $totalCollection = null): void { + if ($totalCollection === null) { + $totalCollection = $moduleCollection; + } $this->ensureOutputFileExists(); $writer = $this->config->getWriter(); - (new ReplaceSymbolsInCollectionAction())->execute( - $collection, + (new ReplaceIntermoduleSymbolsInCollectionAction())->execute( + $moduleCollection, + $totalCollection, $writer->replacesSymbolsWithFullyQualifiedIdentifiers() ); file_put_contents( $this->outputFile, - $writer->format($collection) + $writer->format($moduleCollection) ); } diff --git a/src/Actions/ReplaceIntermoduleSymbolsInCollectionAction.php b/src/Actions/ReplaceIntermoduleSymbolsInCollectionAction.php new file mode 100644 index 00000000..bf3e5f06 --- /dev/null +++ b/src/Actions/ReplaceIntermoduleSymbolsInCollectionAction.php @@ -0,0 +1,24 @@ +transformed = $replaceSymbolsInTypeAction->execute($type); + } + + return $moduleCollection; + } +} diff --git a/src/Actions/ReplaceSymbolsInTypeAction.php b/src/Actions/ReplaceSymbolsInTypeAction.php index 12605944..0e3f7851 100644 --- a/src/Actions/ReplaceSymbolsInTypeAction.php +++ b/src/Actions/ReplaceSymbolsInTypeAction.php @@ -4,6 +4,7 @@ use Spatie\TypeScriptTransformer\Exceptions\CircularDependencyChain; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\Structures\TypesCollection; class ReplaceSymbolsInTypeAction @@ -18,7 +19,7 @@ public function __construct(TypesCollection $collection, $withFullyQualifiedName $this->withFullyQualifiedNames = $withFullyQualifiedNames; } - public function execute(TransformedType $type, array $chain = []): string + public function execute(TransformedType $type, array $chain = []): TranspilationResult { if (in_array($type->getTypeScriptName(), $chain)) { $chain = array_merge($chain, [$type->getTypeScriptName()]); diff --git a/src/Actions/TranspileTypeToTypeScriptAction.php b/src/Actions/TranspileTypeToTypeScriptAction.php index 448e6f09..beb7d31c 100644 --- a/src/Actions/TranspileTypeToTypeScriptAction.php +++ b/src/Actions/TranspileTypeToTypeScriptAction.php @@ -21,7 +21,9 @@ use phpDocumentor\Reflection\Types\String_; use phpDocumentor\Reflection\Types\This; use phpDocumentor\Reflection\Types\Void_; +use SebastianBergmann\ObjectReflector\ObjectReflector; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\Types\RecordType; use Spatie\TypeScriptTransformer\Types\StructType; use Spatie\TypeScriptTransformer\Types\TypeScriptType; @@ -44,96 +46,144 @@ public function __construct( $this->currentClass = $currentClass; } - public function execute(Type $type): string - { - return match (true) { + public function execute(Type $type): TranspilationResult { + $result = match (true) { $type instanceof Compound => $this->resolveCompoundType($type), $type instanceof AbstractList => $this->resolveListType($type), $type instanceof Nullable => $this->resolveNullableType($type), $type instanceof Object_ => $this->resolveObjectType($type), $type instanceof StructType => $this->resolveStructType($type), $type instanceof RecordType => $this->resolveRecordType($type), - $type instanceof TypeScriptType => (string) $type, - $type instanceof Boolean => 'boolean', - $type instanceof Float_, $type instanceof Integer => 'number', - $type instanceof String_, $type instanceof ClassString => 'string', - $type instanceof Null_ => 'null', + $type instanceof TypeScriptType => TranspilationResult::noDeps((string)$type), + $type instanceof Boolean => TranspilationResult::noDeps('boolean'), + $type instanceof Float_, $type instanceof Integer => TranspilationResult::noDeps('number'), + $type instanceof String_, $type instanceof ClassString => TranspilationResult::noDeps('string'), + $type instanceof Null_ => TranspilationResult::noDeps('null'), $type instanceof Self_, $type instanceof Static_, $type instanceof This => $this->resolveSelfReferenceType(), - $type instanceof Scalar => 'string|number|boolean', - $type instanceof Mixed_ => 'any', - $type instanceof Void_ => 'void', + $type instanceof Scalar => TranspilationResult::noDeps('string|number|boolean'), + $type instanceof Mixed_ => TranspilationResult::noDeps('any'), + $type instanceof Void_ => TranspilationResult::noDeps('void'), $type instanceof PseudoType => $this->execute($type->underlyingType()), default => throw new Exception("Could not transform type: {$type}") }; + return new TranspilationResult( + array_merge( + ( + $type instanceof Object_ + && $type->__toString() !== 'object' + ) + ? [$type] + : [], + $result->dependencies + ), + $result->typescript + ); } - private function resolveCompoundType(Compound $compound): string - { + private function resolveCompoundType(Compound $compound): TranspilationResult { $transformed = array_map( - fn (Type $type) => $this->execute($type), + fn(Type $type) => $this->execute($type), iterator_to_array($compound->getIterator()) ); - return join(' | ', array_unique($transformed)); + return new TranspilationResult( + array_reduce( + $transformed, + fn(array $carry, TranspilationResult $item) => array_merge( + $carry, + $item->dependencies + ), + [] + ), + join( + ' | ', + array_unique( + array_map( + fn(TranspilationResult $result) => $result->typescript, + $transformed + ) + ) + ) + ); } - private function resolveListType(AbstractList $list): string - { + private function resolveListType(AbstractList $list): TranspilationResult { + $valueTransResult = $this->execute($list->getValueType()); if ($this->isTypeScriptArray($list->getKeyType())) { - return "Array<{$this->execute($list->getValueType())}>"; + return new TranspilationResult( + $valueTransResult->dependencies, + "Array<$valueTransResult->typescript>" + ); } - return "{ [key: {$this->execute($list->getKeyType())}]: {$this->execute($list->getValueType())} }"; + $keyTransResult = $this->execute($list->getKeyType()); + $typescript = "{ [key: $keyTransResult->typescript]: {$valueTransResult->typescript} }"; + return new TranspilationResult( + array_merge($valueTransResult->dependencies, $keyTransResult->dependencies), + $typescript + ); } - private function resolveNullableType(Nullable $nullable): string - { + private function resolveNullableType(Nullable $nullable): TranspilationResult { if ($this->nullablesAreOptional) { return $this->execute($nullable->getActualType()); } - return "{$this->execute($nullable->getActualType())} | null"; + $transpilationResult = $this->execute($nullable->getActualType()); + return new TranspilationResult( + $transpilationResult->dependencies, + "$transpilationResult->typescript | null" + ); } - private function resolveObjectType(Object_ $object): string - { + private function resolveObjectType(Object_ $object): TranspilationResult { if ($object->getFqsen() === null) { - return 'object'; + return TranspilationResult::noDeps('object'); } - return $this->missingSymbolsCollection->add( - (string) $object->getFqsen() + return TranspilationResult::noDeps( + $this->missingSymbolsCollection->add( + (string)$object->getFqsen() + ) ); } - private function resolveStructType(StructType $type): string - { + private function resolveStructType(StructType $type): TranspilationResult { $transformed = "{"; + $dependencies = []; foreach ($type->getTypes() as $name => $type) { - $transformed .= "{$name}:{$this->execute($type)};"; + $trRes = $this->execute($type); + $transformed .= "{$name}:{$trRes->typescript};"; + foreach ($trRes->dependencies as $dependency) { + $dependencies[] = $dependency; + } } - return "{$transformed}}"; + return new TranspilationResult($dependencies, "{$transformed}}"); } - private function resolveRecordType(RecordType $type): string - { - return "Record<{$this->execute($type->getKeyType())}, {$this->execute($type->getValueType())}>"; + private function resolveRecordType(RecordType $type): TranspilationResult { + $keyTr = $this->execute($type->getKeyType()); + $valueTr = $this->execute($type->getValueType()); + return new TranspilationResult( + array_merge($keyTr->dependencies, $valueTr->dependencies), + "Record<{$keyTr->typescript}, {$valueTr->typescript}>" + ); } - private function resolveSelfReferenceType(): string - { + private function resolveSelfReferenceType(): TranspilationResult { if ($this->currentClass === null) { - return 'any'; + return TranspilationResult::noDeps('any'); } - return $this->missingSymbolsCollection->add($this->currentClass); + return TranspilationResult::noDeps( + $this->missingSymbolsCollection->add($this->currentClass) + ); } - private function isTypeScriptArray(Type $keyType): bool - { - if (! $keyType instanceof Compound) { + private function isTypeScriptArray(Type $keyType): bool { + if (!$keyType instanceof Compound) { return false; } @@ -141,7 +191,7 @@ private function isTypeScriptArray(Type $keyType): bool return false; } - if (! $keyType->contains(new String_()) || ! $keyType->contains(new Integer())) { + if (!$keyType->contains(new String_()) || !$keyType->contains(new Integer())) { return false; } diff --git a/src/Structures/NamespacedType.php b/src/Structures/NamespacedType.php new file mode 100644 index 00000000..05a429b3 --- /dev/null +++ b/src/Structures/NamespacedType.php @@ -0,0 +1,50 @@ +namespace = self::namespace($type); + $this->shortName = self::shortName($type); + } + + + public static function namespace(string|ReflectionClass $type): string { + if ($type instanceof ReflectionClass) { + $type = $type->getName(); + } + return substr($type, 0, strrpos($type, '\\')); + } + + public static function commonPrefix(string $a, string $b): string { + $length = min(strlen($a), strlen($b)); + $i = 0; + + while ($i < $length && $a[$i] === $b[$i]) { + $i++; + } + + return substr($a, 0, $i); + } + + public static function shortName(string|ReflectionClass $type): string { + if ($type instanceof ReflectionClass) { + $type = $type->getName(); + } + + $pos = strrpos($type, '\\'); + + return $pos !== false ? substr($type, $pos + 1) : $type; + } + +} diff --git a/src/Structures/TransformedType.php b/src/Structures/TransformedType.php index 49322a33..f17f572e 100644 --- a/src/Structures/TransformedType.php +++ b/src/Structures/TransformedType.php @@ -10,7 +10,7 @@ class TransformedType public ?string $name = null; - public string $transformed; + public TranspilationResult $transformed; public MissingSymbolsCollection $missingSymbols; @@ -23,7 +23,7 @@ class TransformedType public static function create( ReflectionClass $class, string $name, - string $transformed, + TranspilationResult $transformed, ?MissingSymbolsCollection $missingSymbols = null, bool $inline = false, string $keyword = 'type', @@ -34,7 +34,7 @@ public static function create( public static function createInline( ReflectionClass $class, - string $transformed, + TranspilationResult $transformed, ?MissingSymbolsCollection $missingSymbols = null ): self { return new self($class, null, $transformed, $missingSymbols ?? new MissingSymbolsCollection(), true); @@ -43,7 +43,7 @@ public static function createInline( public function __construct( ReflectionClass $class, ?string $name, - string $transformed, + TranspilationResult $transformed, MissingSymbolsCollection $missingSymbols, bool $isInline, string $keyword = 'type', @@ -91,10 +91,10 @@ public function replaceSymbol(string $class, string $replacement): void { $this->missingSymbols->remove($class); - $this->transformed = str_replace( + $this->transformed->typescript = str_replace( "{%{$class}%}", $replacement, - $this->transformed + $this->transformed->typescript ); } diff --git a/src/Structures/TranspilationResult.php b/src/Structures/TranspilationResult.php new file mode 100644 index 00000000..c6b4e9cf --- /dev/null +++ b/src/Structures/TranspilationResult.php @@ -0,0 +1,29 @@ +dependencies = array_unique($dependencies); + } + + public static function noDeps(string $typescript): static { + return new TranspilationResult([], $typescript); + } + + public static function empty(): static { + return new TranspilationResult([], ''); + } + + public function __toString(): string { + return $this->typescript; + } + +} \ No newline at end of file diff --git a/src/Transformers/DtoTransformer.php b/src/Transformers/DtoTransformer.php index a39fbdee..0d7feb38 100644 --- a/src/Transformers/DtoTransformer.php +++ b/src/Transformers/DtoTransformer.php @@ -8,6 +8,7 @@ use Spatie\TypeScriptTransformer\Attributes\Optional; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\TypeProcessors\DtoCollectionTypeProcessor; use Spatie\TypeScriptTransformer\TypeProcessors\ReplaceDefaultsTypeProcessor; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; @@ -31,16 +32,26 @@ public function transform(ReflectionClass $class, string $name): ?TransformedTyp $missingSymbols = new MissingSymbolsCollection(); - $type = join([ - $this->transformProperties($class, $missingSymbols), - $this->transformMethods($class, $missingSymbols), - $this->transformExtra($class, $missingSymbols), + $trProps = $this->transformProperties($class, $missingSymbols); + $trMethods = $this->transformMethods($class, $missingSymbols); + $trExtra = $this->transformExtra($class, $missingSymbols); + $type = join('', [ + $trProps->typescript, + $trMethods->typescript, + $trExtra->typescript, ]); return TransformedType::create( $class, $name, - "{" . PHP_EOL . $type . "}", + new TranspilationResult( + array_merge( + $trProps->dependencies, + $trMethods->dependencies, + $trExtra->dependencies + ), + "{" . PHP_EOL . $type . "}" + ), $missingSymbols ); } @@ -53,13 +64,13 @@ protected function canTransform(ReflectionClass $class): bool protected function transformProperties( ReflectionClass $class, MissingSymbolsCollection $missingSymbols - ): string { + ): TranspilationResult { $isClassOptional = ! empty($class->getAttributes(Optional::class)); $nullablesAreOptional = $this->config->shouldConsiderNullAsOptional(); return array_reduce( $this->resolveProperties($class), - function (string $carry, ReflectionProperty $property) use ($isClassOptional, $missingSymbols, $nullablesAreOptional) { + function (TranspilationResult $carry, ReflectionProperty $property) use ($isClassOptional, $missingSymbols, $nullablesAreOptional) { $isHidden = ! empty($property->getAttributes(Hidden::class)); if ($isHidden) { @@ -83,33 +94,39 @@ function (string $carry, ReflectionProperty $property) use ($isClassOptional, $m $propertyName = $this->transformPropertyName($property, $missingSymbols); - return $isOptional - ? "{$carry}{$propertyName}?: {$transformed};" . PHP_EOL - : "{$carry}{$propertyName}: {$transformed};" . PHP_EOL; + return new TranspilationResult( + array_merge( + $carry->dependencies, + $transformed->dependencies + ), + $isOptional + ? "{$carry->typescript}{$propertyName}?: {$transformed->typescript};" . PHP_EOL + : "{$carry->typescript}{$propertyName}: {$transformed->typescript};" . PHP_EOL + ); }, - '' + new TranspilationResult([], '') ); } protected function transformMethods( ReflectionClass $class, MissingSymbolsCollection $missingSymbols - ): string { - return ''; + ): TranspilationResult { + return new TranspilationResult([], ''); } protected function transformExtra( ReflectionClass $class, MissingSymbolsCollection $missingSymbols - ): string { - return ''; + ): TranspilationResult { + return new TranspilationResult([], ''); } protected function transformPropertyName( ReflectionProperty $property, MissingSymbolsCollection $missingSymbols - ): string { - return $property->getName(); + ): TranspilationResult { + return new TranspilationResult([], $property->getName()); } protected function typeProcessors(): array diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index d9fdc351..0c784056 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -6,6 +6,7 @@ use ReflectionEnum; use ReflectionEnumBackedCase; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class EnumTransformer implements Transformer @@ -45,7 +46,9 @@ protected function toEnum(ReflectionEnum $enum, string $name): TransformedType return TransformedType::create( $enum, $name, - implode(', ', $options), + TranspilationResult::noDeps( + implode(', ', $options), + ), keyword: 'enum' ); } @@ -60,16 +63,18 @@ protected function toType(ReflectionEnum $enum, string $name): TransformedType return TransformedType::create( $enum, $name, - implode(' | ', $options) + TranspilationResult::noDeps( + implode(' | ', $options) + ) ); } - protected function toEnumValue(ReflectionEnumBackedCase $case): string + protected function toEnumValue(ReflectionEnumBackedCase $case): TranspilationResult { $value = $case->getBackingValue(); if (! is_string($value)) { - return "{$value}"; + return TranspilationResult::noDeps("{$value}"); } $escaped = strtr($value, [ @@ -77,6 +82,6 @@ protected function toEnumValue(ReflectionEnumBackedCase $case): string '\'' => '\\\'', ]); - return "'{$escaped}'"; + return TranspilationResult::noDeps("'{$escaped}'"); } } diff --git a/src/Transformers/InterfaceTransformer.php b/src/Transformers/InterfaceTransformer.php index c406cf6b..302ec4d9 100644 --- a/src/Transformers/InterfaceTransformer.php +++ b/src/Transformers/InterfaceTransformer.php @@ -6,6 +6,7 @@ use ReflectionMethod; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; class InterfaceTransformer extends DtoTransformer implements Transformer { @@ -25,13 +26,13 @@ public function transform(ReflectionClass $class, string $name): ?TransformedTyp protected function transformMethods( ReflectionClass $class, MissingSymbolsCollection $missingSymbols - ): string { + ): TranspilationResult { return array_reduce( $class->getMethods(ReflectionMethod::IS_PUBLIC), - function (string $carry, ReflectionMethod $method) use ($missingSymbols) { + function (TranspilationResult $carry, ReflectionMethod $method) use ($missingSymbols) { $transformedParameters = \array_reduce( $method->getParameters(), - function (string $parameterCarry, \ReflectionParameter $parameter) use ($missingSymbols) { + function (string $parameterCarry, \ReflectionParameter $parameter) use ($method, $missingSymbols) { $type = $this->reflectionToTypeScript( $parameter, $missingSymbols, @@ -44,9 +45,12 @@ function (string $parameterCarry, \ReflectionParameter $parameter) use ($missing $output .= ', '; } - return "{$parameterCarry}{$output}{$parameter->getName()}: {$type}"; + return new TranspilationResult( + $type->dependencies, + "{$parameterCarry}{$output}{$parameter->getName()}: {$type}" + ); }, - '' + TranspilationResult::empty() ); $returnType = 'any'; @@ -59,16 +63,16 @@ function (string $parameterCarry, \ReflectionParameter $parameter) use ($missing ); } - return "{$carry}{$method->getName()}({$transformedParameters}): {$returnType};" . PHP_EOL; + return TranspilationResult::noDeps("{$carry}{$method->getName()}({$transformedParameters}): {$returnType};" . PHP_EOL); }, - '' + TranspilationResult::empty() ); } protected function transformProperties( ReflectionClass $class, MissingSymbolsCollection $missingSymbols - ): string { - return ''; + ): TranspilationResult { + return TranspilationResult::empty(); } } diff --git a/src/Transformers/MyclabsEnumTransformer.php b/src/Transformers/MyclabsEnumTransformer.php index cab53b62..bf3591a1 100644 --- a/src/Transformers/MyclabsEnumTransformer.php +++ b/src/Transformers/MyclabsEnumTransformer.php @@ -5,6 +5,7 @@ use MyCLabs\Enum\Enum; use ReflectionClass; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class MyclabsEnumTransformer implements Transformer @@ -38,7 +39,9 @@ protected function toEnum(ReflectionClass $class, string $name): TransformedType return TransformedType::create( $class, $name, - implode(', ', $options), + TranspilationResult::noDeps( + implode(', ', $options), + ), keyword: 'enum' ); } @@ -56,7 +59,9 @@ protected function toType(ReflectionClass $class, string $name): TransformedType return TransformedType::create( $class, $name, - implode(' | ', $options) + TranspilationResult::noDeps( + implode(' | ', $options) + ) ); } } diff --git a/src/Transformers/SpatieEnumTransformer.php b/src/Transformers/SpatieEnumTransformer.php index 5824c67b..6a01330f 100644 --- a/src/Transformers/SpatieEnumTransformer.php +++ b/src/Transformers/SpatieEnumTransformer.php @@ -5,6 +5,7 @@ use ReflectionClass; use Spatie\Enum\Enum; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class SpatieEnumTransformer implements Transformer @@ -38,7 +39,9 @@ protected function toEnum(ReflectionClass $class, string $name): TransformedType return TransformedType::create( $class, $name, - implode(', ', $options), + TranspilationResult::noDeps( + implode(', ', $options), + ), keyword: 'enum' ); } @@ -56,7 +59,9 @@ private function toType(ReflectionClass $class, string $name): TransformedType return TransformedType::create( $class, $name, - implode(' | ', $options) + TranspilationResult::noDeps( + implode(' | ', $options) + ) ); } } diff --git a/src/Transformers/TransformsTypes.php b/src/Transformers/TransformsTypes.php index a9abeb8b..4b713c00 100644 --- a/src/Transformers/TransformsTypes.php +++ b/src/Transformers/TransformsTypes.php @@ -8,6 +8,7 @@ use ReflectionProperty; use Spatie\TypeScriptTransformer\Actions\TranspileTypeToTypeScriptAction; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\TypeProcessors\TypeProcessor; use Spatie\TypeScriptTransformer\TypeReflectors\TypeReflector; @@ -18,7 +19,7 @@ protected function reflectionToTypeScript( MissingSymbolsCollection $missingSymbolsCollection, bool $nullablesAreOptional = false, TypeProcessor ...$typeProcessors - ): ?string { + ): ?TranspilationResult { $type = $this->reflectionToType( $reflection, $missingSymbolsCollection, @@ -64,7 +65,7 @@ protected function typeToTypeScript( MissingSymbolsCollection $missingSymbolsCollection, bool $nullablesAreOptional = false, ?string $currentClass = null, - ): string { + ): TranspilationResult { $transpiler = new TranspileTypeToTypeScriptAction( $missingSymbolsCollection, $nullablesAreOptional, diff --git a/src/TypeScriptTransformer.php b/src/TypeScriptTransformer.php index 5b600802..1678232c 100644 --- a/src/TypeScriptTransformer.php +++ b/src/TypeScriptTransformer.php @@ -29,26 +29,28 @@ public function transform(): TypesCollection if (($baseDir = $this->config->getSplitModulesBaseDir()) !== null) { (new TemporaryDirectory($baseDir))->delete(); - $sumCollection = new TypesCollection(); - $typesCollections = (new ResolveSplitTypesCollectionsAction( new Finder(), $this->config, ))->execute(); + $sumCollection = new TypesCollection(); + foreach ($typesCollections as $namespace => $typesCollection) { + foreach ($typesCollection as $type) { + $sumCollection[] = $type; + } + } + foreach ($typesCollections as $namespace => $typesCollection) { $outputFile = rtrim($baseDir, '/') . '/' . $namespace . '.ts'; (new PersistTypesCollectionAction( $this->config, $outputFile, - ))->execute($typesCollection); + ))->execute($typesCollection, $sumCollection); (new FormatTypeScriptAction($this->config, $outputFile))->execute(); - foreach ($typesCollection as $type) { - $sumCollection[] = $type; - } } return $sumCollection; @@ -58,7 +60,7 @@ public function transform(): TypesCollection $this->config, ))->execute(); - (new PersistTypesCollectionAction($this->config, $this->config->getOutputFile()))->execute($typesCollection); + (new PersistTypesCollectionAction($this->config, $this->config->getOutputFile()))->execute($typesCollection, $typesCollection); (new FormatTypeScriptAction($this->config, $this->config->getOutputFile()))->execute(); diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index e0c0c462..79e134e3 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -2,13 +2,13 @@ namespace Spatie\TypeScriptTransformer\Writers; +use Spatie\TypeScriptTransformer\Structures\NamespacedType; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\Structures\TypesCollection; class ModuleWriter implements Writer { - public function format(TypesCollection $collection): string - { + public function format(TypesCollection $collection): string { $output = ''; /** @var \ArrayIterator $iterator */ @@ -18,19 +18,63 @@ public function format(TypesCollection $collection): string return strcmp($a->name, $b->name); }); + $currentModuleNamespace = null; + /** @var NamespacedType[] $typesByNamespace */ + $typesByNamespace = []; foreach ($iterator as $type) { + /** @var TransformedType $type */ if ($type->isInline) { continue; } + if ($currentModuleNamespace === null) { + $currentModuleNamespace = trim(NamespacedType::namespace($type->reflection->name), '\\'); + } + + $output .= "export {$type->toString()}" . PHP_EOL; + + if ($type->reflection->isUserDefined() && !$type->reflection->isInternal()) { + foreach ($type->transformed->dependencies as $dependency) { + $namespacedType = new NamespacedType($dependency); + $typesByNamespace[$namespacedType->namespace][] = $namespacedType; + } + } + } + + $import = ''; + foreach ($typesByNamespace as $namespace => $types) { + $namespace = trim($namespace, '\\'); + if ($namespace === $currentModuleNamespace) { + continue; + } + $import .= 'import {'; + $import .= join( + ', ', + array_map( + fn(NamespacedType $type) => $type->shortName, + $types + ) + ); + $commonPrefix = NamespacedType::commonPrefix($namespace, $currentModuleNamespace); + $thatRest = ltrim(substr($namespace, strlen($commonPrefix)), '\\'); + $currentRest = ltrim(substr($currentModuleNamespace, strlen($commonPrefix)), '\\'); + $sourceModulePath = + join( + '/', + array_fill(0, substr_count($currentRest, '\\') + 1, '..') + ) + . '/' + . join( + '/', + explode('\\', $thatRest) + ); - $output .= "export {$type->toString()}".PHP_EOL; + $import .= '} from "' . $sourceModulePath . "\";\n"; } - return $output; + return $import . $output; } - public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool - { + public function replacesSymbolsWithFullyQualifiedIdentifiers(): bool { return false; } } diff --git a/tests/Actions/PersistTypesCollectionActionTest.php b/tests/Actions/PersistTypesCollectionActionTest.php index 0dbddecf..7c056048 100644 --- a/tests/Actions/PersistTypesCollectionActionTest.php +++ b/tests/Actions/PersistTypesCollectionActionTest.php @@ -28,7 +28,7 @@ $collection[] = FakeTransformedType::fake('Enum')->withNamespace('test'); $collection[] = FakeTransformedType::fake('Enum')->withNamespace('test\test'); - $this->action->execute($collection); + $this->action->execute($collection, $collection); assertMatchesFileSnapshot($this->temporaryDirectory->path("types.d.ts")); }); @@ -41,7 +41,7 @@ $collection[] = FakeTransformedType::fake('Enum')->withTransformed('transformed test\Enum')->withNamespace('test'); $collection[] = FakeTransformedType::fake('OtherEnum')->withTransformed('transformed test\OtherEnum')->withNamespace('test'); - $this->action->execute($collection); + $this->action->execute($collection, $collection); assertMatchesFileSnapshot($this->temporaryDirectory->path("types.d.ts")); }); diff --git a/tests/Actions/TranspileTypeToTypeScriptActionTest.php b/tests/Actions/TranspileTypeToTypeScriptActionTest.php index 07529274..ec4b180e 100644 --- a/tests/Actions/TranspileTypeToTypeScriptActionTest.php +++ b/tests/Actions/TranspileTypeToTypeScriptActionTest.php @@ -62,7 +62,7 @@ it('can resolve pseudo types', function () { $transformed = $this->action->execute($this->typeResolver->resolve('array-key')); - expect($transformed)->toBe('string | number'); + expect($transformed->typescript)->toBe('string | number'); }); it('does not add nullable unions to optional properties', function () { diff --git a/tests/Collectors/DefaultCollectorTest.php b/tests/Collectors/DefaultCollectorTest.php index 50b4f445..b75d55f9 100644 --- a/tests/Collectors/DefaultCollectorTest.php +++ b/tests/Collectors/DefaultCollectorTest.php @@ -50,7 +50,7 @@ assertNotNull($transformedType); assertEquals( "'a' | 'yes' | 'no'", - $transformedType->transformed, + $transformedType->transformed->typescript, ); }); @@ -70,7 +70,7 @@ assertEquals('EnumTransformed', $transformedType->name); assertEquals( "'a' | 'yes' | 'no'", - $transformedType->transformed, + $transformedType->transformed->typescript, ); }); @@ -95,7 +95,7 @@ assertEquals('DtoTransformed', $transformedType->name); assertEquals( '{'.PHP_EOL.'an_integer: number;'.PHP_EOL.'}', - $transformedType->transformed, + $transformedType->transformed->typescript, ); }); @@ -120,7 +120,7 @@ assertEquals('WithTypeScriptAttribute', $transformedType->name); assertEquals( "'a' | 'b'", - $transformedType->transformed, + $transformedType->transformed->typescript, ); }); @@ -133,7 +133,7 @@ assertEquals('WithTypeScriptTransformerAttribute', $transformedType->name); assertEquals( '{'.PHP_EOL.'an_int: number;'.PHP_EOL.'}', - $transformedType->transformed, + $transformedType->transformed->typescript, ); }); @@ -145,7 +145,7 @@ assertNotNull($transformedType); assertEquals( '{an_int:number;a_bool:boolean;}', - $transformedType->transformed, + $transformedType->transformed->typescript, ); }); diff --git a/tests/Fakes/FakeTransformedType.php b/tests/Fakes/FakeTransformedType.php index 80fc4f41..01558470 100644 --- a/tests/Fakes/FakeTransformedType.php +++ b/tests/Fakes/FakeTransformedType.php @@ -6,15 +6,16 @@ use ReflectionClass; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; class FakeTransformedType extends TransformedType { - public static function create(ReflectionClass $class, string $name, string $transformed, ?MissingSymbolsCollection $missingSymbols = null, bool $inline = false, string $keyword = 'type', bool $trailingSemicolon = true): TransformedType + public static function create(ReflectionClass $class, string $name, TranspilationResult $transformed, ?MissingSymbolsCollection $missingSymbols = null, bool $inline = false, string $keyword = 'type', bool $trailingSemicolon = true): TransformedType { throw new Exception("Fake type"); } - public static function createInline(ReflectionClass $class, string $transformed, ?MissingSymbolsCollection $missingSymbols = null): TransformedType + public static function createInline(ReflectionClass $class, TranspilationResult $transformed, ?MissingSymbolsCollection $missingSymbols = null): TransformedType { throw new Exception("Fake type"); } @@ -26,7 +27,9 @@ public static function fake(?string $name = null): self return new self( FakeReflectionClass::create()->withName($name), $name, - 'fake-transformed', + TranspilationResult::noDeps( + 'fake-transformed' + ), new MissingSymbolsCollection(), false ); @@ -53,8 +56,11 @@ public function withoutNamespace(): self return $this; } - public function withTransformed(string $transformed): self + public function withTransformed(TranspilationResult|string $transformed): self { + if (is_string($transformed)) { + $transformed = TranspilationResult::noDeps($transformed); + } $this->transformed = $transformed; return $this; diff --git a/tests/Fakes/FakeTypeScriptCollector.php b/tests/Fakes/FakeTypeScriptCollector.php index 1e646870..95492e70 100644 --- a/tests/Fakes/FakeTypeScriptCollector.php +++ b/tests/Fakes/FakeTypeScriptCollector.php @@ -7,6 +7,7 @@ use Spatie\TypeScriptTransformer\Collectors\Collector; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; class FakeTypeScriptCollector extends Collector { @@ -20,7 +21,9 @@ public function getTransformedType(ReflectionClass $class): TransformedType return new TransformedType( $class, $class->getShortName(), - 'fake-collected-class', + TranspilationResult::noDeps( + 'fake-collected-class' + ), new MissingSymbolsCollection(), false ); diff --git a/tests/Fakes/FakeTypeScriptTransformer.php b/tests/Fakes/FakeTypeScriptTransformer.php index 38093ba5..671ac29c 100644 --- a/tests/Fakes/FakeTypeScriptTransformer.php +++ b/tests/Fakes/FakeTypeScriptTransformer.php @@ -5,11 +5,16 @@ use MyCLabs\Enum\Enum; use ReflectionClass; use Spatie\TypeScriptTransformer\Structures\TransformedType; +use Spatie\TypeScriptTransformer\Structures\TranspilationResult; use Spatie\TypeScriptTransformer\Transformers\Transformer; class FakeTypeScriptTransformer implements Transformer { - private string $transformed = 'fake'; + private TranspilationResult $transformed; + + public function __construct() { + $this->transformed = TranspilationResult::noDeps('fake'); + } public static function create(): self { diff --git a/tests/Structures/NamespacedTypeTest.php b/tests/Structures/NamespacedTypeTest.php new file mode 100644 index 00000000..7259d681 --- /dev/null +++ b/tests/Structures/NamespacedTypeTest.php @@ -0,0 +1,23 @@ +;a_self_reference:{%fake_class%};an_object:{a_bool:boolean;an_int:number;};}' From bef533fb96fd70ce34d514fc14741a088a094292 Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Tue, 10 Jun 2025 10:30:17 +0700 Subject: [PATCH 5/9] Integrate compactor into module splitting --- src/Writers/ModuleWriter.php | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 79e134e3..328798cb 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -2,12 +2,22 @@ namespace Spatie\TypeScriptTransformer\Writers; +use Spatie\TypeScriptTransformer\Compactors\Compactor; +use Spatie\TypeScriptTransformer\Compactors\ConfigCompactor; use Spatie\TypeScriptTransformer\Structures\NamespacedType; use Spatie\TypeScriptTransformer\Structures\TransformedType; use Spatie\TypeScriptTransformer\Structures\TypesCollection; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; class ModuleWriter implements Writer { + + protected Compactor $compactor; + + public function __construct(TypeScriptTransformerConfig $config) { + $this->compactor = new ConfigCompactor($config); + } + public function format(TypesCollection $collection): string { $output = ''; @@ -18,7 +28,7 @@ public function format(TypesCollection $collection): string { return strcmp($a->name, $b->name); }); - $currentModuleNamespace = null; + $currentModuleTsNamespace = null; /** @var NamespacedType[] $typesByNamespace */ $typesByNamespace = []; foreach ($iterator as $type) { @@ -26,8 +36,15 @@ public function format(TypesCollection $collection): string { if ($type->isInline) { continue; } - if ($currentModuleNamespace === null) { - $currentModuleNamespace = trim(NamespacedType::namespace($type->reflection->name), '\\'); + + if ($currentModuleTsNamespace === null) { + $currentModuleTsNamespace = + $this->compactor->removePrefix( + trim( + NamespacedType::namespace($type->reflection->name), + '\\' + ) + ); } $output .= "export {$type->toString()}" . PHP_EOL; @@ -42,8 +59,11 @@ public function format(TypesCollection $collection): string { $import = ''; foreach ($typesByNamespace as $namespace => $types) { - $namespace = trim($namespace, '\\'); - if ($namespace === $currentModuleNamespace) { + $tsNamespace = $this->compactor->removePrefix( + trim($namespace, '\\') + ); + + if ($tsNamespace === $currentModuleTsNamespace) { continue; } $import .= 'import {'; @@ -54,9 +74,9 @@ public function format(TypesCollection $collection): string { $types ) ); - $commonPrefix = NamespacedType::commonPrefix($namespace, $currentModuleNamespace); - $thatRest = ltrim(substr($namespace, strlen($commonPrefix)), '\\'); - $currentRest = ltrim(substr($currentModuleNamespace, strlen($commonPrefix)), '\\'); + $commonPrefix = NamespacedType::commonPrefix($tsNamespace, $currentModuleTsNamespace); + $thatRest = ltrim(substr($tsNamespace, strlen($commonPrefix)), '\\'); + $currentRest = ltrim(substr($currentModuleTsNamespace, strlen($commonPrefix)), '\\'); $sourceModulePath = join( '/', From f7a53338c43dd6e52d38d17c294e216b3f562509 Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Tue, 10 Jun 2025 10:38:46 +0700 Subject: [PATCH 6/9] Fix module paths generation --- src/Writers/ModuleWriter.php | 12 +++++++----- ...rationTest__it_can_transform_to_es_modules__1.txt | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 328798cb..0f65e626 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -75,17 +75,19 @@ public function format(TypesCollection $collection): string { ) ); $commonPrefix = NamespacedType::commonPrefix($tsNamespace, $currentModuleTsNamespace); - $thatRest = ltrim(substr($tsNamespace, strlen($commonPrefix)), '\\'); + $importedRest = ltrim(substr($tsNamespace, strlen($commonPrefix)), '\\'); $currentRest = ltrim(substr($currentModuleTsNamespace, strlen($commonPrefix)), '\\'); + $backParts = array_fill(0, substr_count($currentRest, '\\'), '..'); $sourceModulePath = - join( - '/', - array_fill(0, substr_count($currentRest, '\\') + 1, '..') + ( + count($backParts) === 0 + ? '.' + : join('/', $backParts) ) . '/' . join( '/', - explode('\\', $thatRest) + explode('\\', $importedRest) ); $import .= '} from "' . $sourceModulePath . "\";\n"; diff --git a/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt b/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt index 34164097..aa47f5f8 100644 --- a/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt +++ b/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt @@ -1,5 +1,5 @@ -import {RegularEnum} from "../Enum"; -import {YetAnotherDto} from "../LevelUp"; +import {RegularEnum} from "./Enum"; +import {YetAnotherDto} from "./LevelUp"; export type Dto = { string: string; nullbable: string | null; From 257c8a1c406cb3740ee187cbe2d1d1e5bda2187e Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Tue, 10 Jun 2025 10:46:01 +0700 Subject: [PATCH 7/9] Fix common prefix computation so it only goes up to the last backslash --- src/Structures/NamespacedType.php | 5 ++++- tests/Structures/NamespacedTypeTest.php | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Structures/NamespacedType.php b/src/Structures/NamespacedType.php index 05a429b3..c16f36a5 100644 --- a/src/Structures/NamespacedType.php +++ b/src/Structures/NamespacedType.php @@ -34,7 +34,10 @@ public static function commonPrefix(string $a, string $b): string { $i++; } - return substr($a, 0, $i); + $prefix = substr($a, 0, $i); + $lastSlashPos = strrpos($prefix, '\\'); + + return $lastSlashPos !== false ? substr($prefix, 0, $lastSlashPos + 1) : ''; } public static function shortName(string|ReflectionClass $type): string { diff --git a/tests/Structures/NamespacedTypeTest.php b/tests/Structures/NamespacedTypeTest.php index 7259d681..3f879a1e 100644 --- a/tests/Structures/NamespacedTypeTest.php +++ b/tests/Structures/NamespacedTypeTest.php @@ -21,3 +21,19 @@ ) ); }); +it('common prefix goes up to last backslash even with another nesting level', function () { + assertEquals( + 'app\\', // not 'app\\co' + NamespacedType::commonPrefix( + 'app\\companies', + 'app\\countries' + ) + ); + assertEquals( + 'app\\', + NamespacedType::commonPrefix( + 'app\\data', + 'app\\data\\stuff' + ) + ); +}); From 5dfc1b42f820055f6c613a69306f5611a8f17181 Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Wed, 11 Jun 2025 07:29:39 +0700 Subject: [PATCH 8/9] Remove imported module name suffix with compactor --- src/Writers/ModuleWriter.php | 22 +++++++------- tests/Compactors/ConfigCompactorTest.php | 30 +++++++++++++++++++ ...est__it_can_transform_to_es_modules__1.txt | 2 +- 3 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 tests/Compactors/ConfigCompactorTest.php diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 0f65e626..80046f0c 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -70,7 +70,7 @@ public function format(TypesCollection $collection): string { $import .= join( ', ', array_map( - fn(NamespacedType $type) => $type->shortName, + fn(NamespacedType $type) => $this->compactor->removeSuffix($type->shortName), $types ) ); @@ -79,16 +79,16 @@ public function format(TypesCollection $collection): string { $currentRest = ltrim(substr($currentModuleTsNamespace, strlen($commonPrefix)), '\\'); $backParts = array_fill(0, substr_count($currentRest, '\\'), '..'); $sourceModulePath = - ( - count($backParts) === 0 - ? '.' - : join('/', $backParts) - ) - . '/' - . join( - '/', - explode('\\', $importedRest) - ); + ( + count($backParts) === 0 + ? '.' + : join('/', $backParts) + ) + . '/' + . join( + '/', + explode('\\', $importedRest) + ); $import .= '} from "' . $sourceModulePath . "\";\n"; } diff --git a/tests/Compactors/ConfigCompactorTest.php b/tests/Compactors/ConfigCompactorTest.php new file mode 100644 index 00000000..ffabc54d --- /dev/null +++ b/tests/Compactors/ConfigCompactorTest.php @@ -0,0 +1,30 @@ +compactorSuffixes(['Data', 'Dto']) + ); + assertEquals( + 'Hello\User', + $compactor->removeSuffix('Hello\UserData') + ); +}); +it('removes prefix', function () { + $compactor = new ConfigCompactor( + (new TypeScriptTransformerConfig()) + ->compactorPrefixes(['App\Data', 'App\Tests\Data']) + ); + assertEquals( + 'Product', + $compactor->removePrefix('App\Data\Product') + ); + assertEquals( + 'User', + $compactor->removePrefix('App\Tests\Data\User') + ); +}); diff --git a/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt b/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt index aa47f5f8..0ac23418 100644 --- a/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt +++ b/tests/__snapshots__/IntegrationTest__it_can_transform_to_es_modules__1.txt @@ -1,5 +1,5 @@ import {RegularEnum} from "./Enum"; -import {YetAnotherDto} from "./LevelUp"; +import {YetAnotherDto} from "./Integration/LevelUp"; export type Dto = { string: string; nullbable: string | null; From dc1e8c5cf12b31953e974126a5dcd885fc6607a4 Mon Sep 17 00:00:00 2001 From: Georgiy Vlasov Date: Sat, 5 Jul 2025 01:14:19 +0700 Subject: [PATCH 9/9] Fix identifiers getting imported for the same module multiple times when they are used in multiple places --- src/Writers/ModuleWriter.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Writers/ModuleWriter.php b/src/Writers/ModuleWriter.php index 80046f0c..b5462b0d 100644 --- a/src/Writers/ModuleWriter.php +++ b/src/Writers/ModuleWriter.php @@ -69,9 +69,11 @@ public function format(TypesCollection $collection): string { $import .= 'import {'; $import .= join( ', ', - array_map( - fn(NamespacedType $type) => $this->compactor->removeSuffix($type->shortName), - $types + array_unique( + array_map( + fn(NamespacedType $type) => $this->compactor->removeSuffix($type->shortName), + $types + ) ) ); $commonPrefix = NamespacedType::commonPrefix($tsNamespace, $currentModuleTsNamespace);